32 changed files with 3116 additions and 2039 deletions
@ -0,0 +1,804 @@ |
|||||
|
// !$*UTF8*$! |
||||
|
{ |
||||
|
archiveVersion = 1; |
||||
|
classes = { |
||||
|
}; |
||||
|
objectVersion = 54; |
||||
|
objects = { |
||||
|
|
||||
|
/* Begin PBXBuildFile section */ |
||||
|
0AD3C3772DDE122500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C3762DDE122500FD4F41 /* Opus */; }; |
||||
|
0AD3C37C2DDE142500FD4F41 /* Opus in Frameworks */ = {isa = PBXBuildFile; productRef = 0AD3C37B2DDE142500FD4F41 /* Opus */; }; |
||||
|
0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */; }; |
||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; |
||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; |
||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; |
||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; |
||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; |
||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; |
||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; |
||||
|
B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */; }; |
||||
|
B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */; }; |
||||
|
/* End PBXBuildFile section */ |
||||
|
|
||||
|
/* Begin PBXContainerItemProxy section */ |
||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { |
||||
|
isa = PBXContainerItemProxy; |
||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */; |
||||
|
proxyType = 1; |
||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D; |
||||
|
remoteInfo = Runner; |
||||
|
}; |
||||
|
/* End PBXContainerItemProxy section */ |
||||
|
|
||||
|
/* Begin PBXCopyFilesBuildPhase section */ |
||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = { |
||||
|
isa = PBXCopyFilesBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
dstPath = ""; |
||||
|
dstSubfolderSpec = 10; |
||||
|
files = ( |
||||
|
); |
||||
|
name = "Embed Frameworks"; |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
}; |
||||
|
/* End PBXCopyFilesBuildPhase section */ |
||||
|
|
||||
|
/* Begin PBXFileReference section */ |
||||
|
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; }; |
||||
|
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AccessorySetupKit.framework; path = System/Library/Frameworks/AccessorySetupKit.framework; sourceTree = SDKROOT; }; |
||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; |
||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; |
||||
|
1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; |
||||
|
209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; }; |
||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; |
||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; |
||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; |
||||
|
3CFCAD0427BE38C23B922B35 /* 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>"; }; |
||||
|
6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; |
||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; |
||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; |
||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; |
||||
|
85F42A75E97670A970955821 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; }; |
||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; |
||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; |
||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; |
||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; |
||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; |
||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; |
||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; |
||||
|
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; }; |
||||
|
E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; }; |
||||
|
F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; |
||||
|
FD4B933141D4D3B35A169527 /* 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>"; }; |
||||
|
/* End PBXFileReference section */ |
||||
|
|
||||
|
/* Begin PBXFrameworksBuildPhase section */ |
||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = { |
||||
|
isa = PBXFrameworksBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
0AD3C3772DDE122500FD4F41 /* Opus in Frameworks */, |
||||
|
0AD3C37C2DDE142500FD4F41 /* Opus in Frameworks */, |
||||
|
B11F80E69458BE0D0F2DA890 /* Pods_Runner.framework in Frameworks */, |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
}; |
||||
|
B3515CD9628E3332AFC88CD4 /* Frameworks */ = { |
||||
|
isa = PBXFrameworksBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
B3D1A46E14BD3B07986EF8CD /* Pods_RunnerTests.framework in Frameworks */, |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
}; |
||||
|
/* End PBXFrameworksBuildPhase section */ |
||||
|
|
||||
|
/* Begin PBXGroup section */ |
||||
|
1ACB686EC7DC36C6E33D372F /* Pods */ = { |
||||
|
isa = PBXGroup; |
||||
|
children = ( |
||||
|
3CFCAD0427BE38C23B922B35 /* Pods-Runner.debug.xcconfig */, |
||||
|
85F42A75E97670A970955821 /* Pods-Runner.release.xcconfig */, |
||||
|
1DE8AF4A64ABDD75043084FC /* Pods-Runner.profile.xcconfig */, |
||||
|
209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */, |
||||
|
E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */, |
||||
|
FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */, |
||||
|
); |
||||
|
path = Pods; |
||||
|
sourceTree = "<group>"; |
||||
|
}; |
||||
|
1E5DDD8701F6FB27AA502080 /* Frameworks */ = { |
||||
|
isa = PBXGroup; |
||||
|
children = ( |
||||
|
0AD944132DDA0C0D00557D21 /* AccessorySetupKit.framework */, |
||||
|
6DB4CB5146DA70CF43D67A5B /* Pods_Runner.framework */, |
||||
|
F9C3583A880BAF5E80CA0405 /* Pods_RunnerTests.framework */, |
||||
|
); |
||||
|
name = Frameworks; |
||||
|
sourceTree = "<group>"; |
||||
|
}; |
||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = { |
||||
|
isa = PBXGroup; |
||||
|
children = ( |
||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */, |
||||
|
); |
||||
|
path = RunnerTests; |
||||
|
sourceTree = "<group>"; |
||||
|
}; |
||||
|
9740EEB11CF90186004384FC /* Flutter */ = { |
||||
|
isa = PBXGroup; |
||||
|
children = ( |
||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, |
||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */, |
||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, |
||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */, |
||||
|
); |
||||
|
name = Flutter; |
||||
|
sourceTree = "<group>"; |
||||
|
}; |
||||
|
97C146E51CF9000F007C117D = { |
||||
|
isa = PBXGroup; |
||||
|
children = ( |
||||
|
9740EEB11CF90186004384FC /* Flutter */, |
||||
|
97C146F01CF9000F007C117D /* Runner */, |
||||
|
97C146EF1CF9000F007C117D /* Products */, |
||||
|
331C8082294A63A400263BE5 /* RunnerTests */, |
||||
|
1ACB686EC7DC36C6E33D372F /* Pods */, |
||||
|
1E5DDD8701F6FB27AA502080 /* Frameworks */, |
||||
|
); |
||||
|
sourceTree = "<group>"; |
||||
|
}; |
||||
|
97C146EF1CF9000F007C117D /* Products */ = { |
||||
|
isa = PBXGroup; |
||||
|
children = ( |
||||
|
97C146EE1CF9000F007C117D /* Runner.app */, |
||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */, |
||||
|
); |
||||
|
name = Products; |
||||
|
sourceTree = "<group>"; |
||||
|
}; |
||||
|
97C146F01CF9000F007C117D /* Runner */ = { |
||||
|
isa = PBXGroup; |
||||
|
children = ( |
||||
|
0AD944112DD9DB5C00557D21 /* GoogleService-Info.plist */, |
||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */, |
||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */, |
||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, |
||||
|
97C147021CF9000F007C117D /* Info.plist */, |
||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, |
||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, |
||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */, |
||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, |
||||
|
D2958658C06A5ADF62D6ABA2 /* Runner.entitlements */, |
||||
|
); |
||||
|
path = Runner; |
||||
|
sourceTree = "<group>"; |
||||
|
}; |
||||
|
/* End PBXGroup section */ |
||||
|
|
||||
|
/* Begin PBXNativeTarget section */ |
||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = { |
||||
|
isa = PBXNativeTarget; |
||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; |
||||
|
buildPhases = ( |
||||
|
B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */, |
||||
|
331C807D294A63A400263BE5 /* Sources */, |
||||
|
331C807F294A63A400263BE5 /* Resources */, |
||||
|
B3515CD9628E3332AFC88CD4 /* Frameworks */, |
||||
|
); |
||||
|
buildRules = ( |
||||
|
); |
||||
|
dependencies = ( |
||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */, |
||||
|
); |
||||
|
name = RunnerTests; |
||||
|
productName = RunnerTests; |
||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; |
||||
|
productType = "com.apple.product-type.bundle.unit-test"; |
||||
|
}; |
||||
|
97C146ED1CF9000F007C117D /* Runner */ = { |
||||
|
isa = PBXNativeTarget; |
||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; |
||||
|
buildPhases = ( |
||||
|
B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */, |
||||
|
9740EEB61CF901F6004384FC /* Run Script */, |
||||
|
97C146EA1CF9000F007C117D /* Sources */, |
||||
|
97C146EB1CF9000F007C117D /* Frameworks */, |
||||
|
97C146EC1CF9000F007C117D /* Resources */, |
||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */, |
||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */, |
||||
|
1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */, |
||||
|
C482126191919ADDA94A189A /* [CP] Copy Pods Resources */, |
||||
|
); |
||||
|
buildRules = ( |
||||
|
); |
||||
|
dependencies = ( |
||||
|
); |
||||
|
name = Runner; |
||||
|
productName = Runner; |
||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */; |
||||
|
productType = "com.apple.product-type.application"; |
||||
|
}; |
||||
|
/* End PBXNativeTarget section */ |
||||
|
|
||||
|
/* Begin PBXProject section */ |
||||
|
97C146E61CF9000F007C117D /* Project object */ = { |
||||
|
isa = PBXProject; |
||||
|
attributes = { |
||||
|
BuildIndependentTargetsInParallel = YES; |
||||
|
LastUpgradeCheck = 1510; |
||||
|
ORGANIZATIONNAME = ""; |
||||
|
TargetAttributes = { |
||||
|
331C8080294A63A400263BE5 = { |
||||
|
CreatedOnToolsVersion = 14.0; |
||||
|
TestTargetID = 97C146ED1CF9000F007C117D; |
||||
|
}; |
||||
|
97C146ED1CF9000F007C117D = { |
||||
|
CreatedOnToolsVersion = 7.3.1; |
||||
|
LastSwiftMigration = 1100; |
||||
|
}; |
||||
|
}; |
||||
|
}; |
||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; |
||||
|
compatibilityVersion = "Xcode 9.3"; |
||||
|
developmentRegion = en; |
||||
|
hasScannedForEncodings = 0; |
||||
|
knownRegions = ( |
||||
|
en, |
||||
|
Base, |
||||
|
); |
||||
|
mainGroup = 97C146E51CF9000F007C117D; |
||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */; |
||||
|
projectDirPath = ""; |
||||
|
projectRoot = ""; |
||||
|
targets = ( |
||||
|
97C146ED1CF9000F007C117D /* Runner */, |
||||
|
331C8080294A63A400263BE5 /* RunnerTests */, |
||||
|
); |
||||
|
}; |
||||
|
/* End PBXProject section */ |
||||
|
|
||||
|
/* Begin PBXResourcesBuildPhase section */ |
||||
|
331C807F294A63A400263BE5 /* Resources */ = { |
||||
|
isa = PBXResourcesBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
}; |
||||
|
97C146EC1CF9000F007C117D /* Resources */ = { |
||||
|
isa = PBXResourcesBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, |
||||
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, |
||||
|
0AD944122DD9DB5C00557D21 /* GoogleService-Info.plist in Resources */, |
||||
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, |
||||
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
}; |
||||
|
/* End PBXResourcesBuildPhase section */ |
||||
|
|
||||
|
/* Begin PBXShellScriptBuildPhase section */ |
||||
|
1D3D5881CA405E420076D1F6 /* [CP] Embed Pods Frameworks */ = { |
||||
|
isa = PBXShellScriptBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
); |
||||
|
inputFileListPaths = ( |
||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", |
||||
|
); |
||||
|
name = "[CP] Embed Pods Frameworks"; |
||||
|
outputFileListPaths = ( |
||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
shellPath = /bin/sh; |
||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; |
||||
|
showEnvVarsInLog = 0; |
||||
|
}; |
||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { |
||||
|
isa = PBXShellScriptBuildPhase; |
||||
|
alwaysOutOfDate = 1; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
); |
||||
|
inputPaths = ( |
||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", |
||||
|
); |
||||
|
name = "Thin Binary"; |
||||
|
outputPaths = ( |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
shellPath = /bin/sh; |
||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; |
||||
|
}; |
||||
|
9740EEB61CF901F6004384FC /* Run Script */ = { |
||||
|
isa = PBXShellScriptBuildPhase; |
||||
|
alwaysOutOfDate = 1; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
); |
||||
|
inputPaths = ( |
||||
|
); |
||||
|
name = "Run Script"; |
||||
|
outputPaths = ( |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
shellPath = /bin/sh; |
||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; |
||||
|
}; |
||||
|
B6E8833064FA9BB82F279A0E /* [CP] Check Pods Manifest.lock */ = { |
||||
|
isa = PBXShellScriptBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
); |
||||
|
inputFileListPaths = ( |
||||
|
); |
||||
|
inputPaths = ( |
||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock", |
||||
|
"${PODS_ROOT}/Manifest.lock", |
||||
|
); |
||||
|
name = "[CP] Check Pods Manifest.lock"; |
||||
|
outputFileListPaths = ( |
||||
|
); |
||||
|
outputPaths = ( |
||||
|
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
shellPath = /bin/sh; |
||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; |
||||
|
showEnvVarsInLog = 0; |
||||
|
}; |
||||
|
B77B7F8E420C3F7B653C6205 /* [CP] Check Pods Manifest.lock */ = { |
||||
|
isa = PBXShellScriptBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
); |
||||
|
inputFileListPaths = ( |
||||
|
); |
||||
|
inputPaths = ( |
||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock", |
||||
|
"${PODS_ROOT}/Manifest.lock", |
||||
|
); |
||||
|
name = "[CP] Check Pods Manifest.lock"; |
||||
|
outputFileListPaths = ( |
||||
|
); |
||||
|
outputPaths = ( |
||||
|
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
shellPath = /bin/sh; |
||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; |
||||
|
showEnvVarsInLog = 0; |
||||
|
}; |
||||
|
C482126191919ADDA94A189A /* [CP] Copy Pods Resources */ = { |
||||
|
isa = PBXShellScriptBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
); |
||||
|
inputFileListPaths = ( |
||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", |
||||
|
); |
||||
|
name = "[CP] Copy Pods Resources"; |
||||
|
outputFileListPaths = ( |
||||
|
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
shellPath = /bin/sh; |
||||
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; |
||||
|
showEnvVarsInLog = 0; |
||||
|
}; |
||||
|
/* End PBXShellScriptBuildPhase section */ |
||||
|
|
||||
|
/* Begin PBXSourcesBuildPhase section */ |
||||
|
331C807D294A63A400263BE5 /* Sources */ = { |
||||
|
isa = PBXSourcesBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
}; |
||||
|
97C146EA1CF9000F007C117D /* Sources */ = { |
||||
|
isa = PBXSourcesBuildPhase; |
||||
|
buildActionMask = 2147483647; |
||||
|
files = ( |
||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, |
||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, |
||||
|
); |
||||
|
runOnlyForDeploymentPostprocessing = 0; |
||||
|
}; |
||||
|
/* End PBXSourcesBuildPhase section */ |
||||
|
|
||||
|
/* Begin PBXTargetDependency section */ |
||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = { |
||||
|
isa = PBXTargetDependency; |
||||
|
target = 97C146ED1CF9000F007C117D /* Runner */; |
||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; |
||||
|
}; |
||||
|
/* End PBXTargetDependency section */ |
||||
|
|
||||
|
/* Begin PBXVariantGroup section */ |
||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = { |
||||
|
isa = PBXVariantGroup; |
||||
|
children = ( |
||||
|
97C146FB1CF9000F007C117D /* Base */, |
||||
|
); |
||||
|
name = Main.storyboard; |
||||
|
sourceTree = "<group>"; |
||||
|
}; |
||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { |
||||
|
isa = PBXVariantGroup; |
||||
|
children = ( |
||||
|
97C147001CF9000F007C117D /* Base */, |
||||
|
); |
||||
|
name = LaunchScreen.storyboard; |
||||
|
sourceTree = "<group>"; |
||||
|
}; |
||||
|
/* End PBXVariantGroup section */ |
||||
|
|
||||
|
/* Begin XCBuildConfiguration section */ |
||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = { |
||||
|
isa = XCBuildConfiguration; |
||||
|
buildSettings = { |
||||
|
ALWAYS_SEARCH_USER_PATHS = NO; |
||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; |
||||
|
CLANG_ANALYZER_NONNULL = YES; |
||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; |
||||
|
CLANG_CXX_LIBRARY = "libc++"; |
||||
|
CLANG_ENABLE_MODULES = YES; |
||||
|
CLANG_ENABLE_OBJC_ARC = YES; |
||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; |
||||
|
CLANG_WARN_BOOL_CONVERSION = YES; |
||||
|
CLANG_WARN_COMMA = YES; |
||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES; |
||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; |
||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; |
||||
|
CLANG_WARN_EMPTY_BODY = YES; |
||||
|
CLANG_WARN_ENUM_CONVERSION = YES; |
||||
|
CLANG_WARN_INFINITE_RECURSION = YES; |
||||
|
CLANG_WARN_INT_CONVERSION = YES; |
||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; |
||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; |
||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; |
||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; |
||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; |
||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; |
||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES; |
||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES; |
||||
|
CLANG_WARN_UNREACHABLE_CODE = YES; |
||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; |
||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; |
||||
|
COPY_PHASE_STRIP = NO; |
||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; |
||||
|
ENABLE_NS_ASSERTIONS = NO; |
||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES; |
||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO; |
||||
|
GCC_C_LANGUAGE_STANDARD = gnu99; |
||||
|
GCC_NO_COMMON_BLOCKS = YES; |
||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; |
||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; |
||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES; |
||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
||||
|
GCC_WARN_UNUSED_FUNCTION = YES; |
||||
|
GCC_WARN_UNUSED_VARIABLE = YES; |
||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
||||
|
MTL_ENABLE_DEBUG_INFO = NO; |
||||
|
SDKROOT = iphoneos; |
||||
|
SUPPORTED_PLATFORMS = iphoneos; |
||||
|
TARGETED_DEVICE_FAMILY = "1,2"; |
||||
|
VALIDATE_PRODUCT = YES; |
||||
|
}; |
||||
|
name = Profile; |
||||
|
}; |
||||
|
249021D4217E4FDB00AE95B9 /* Profile */ = { |
||||
|
isa = XCBuildConfiguration; |
||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; |
||||
|
buildSettings = { |
||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
||||
|
CLANG_ENABLE_MODULES = YES; |
||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; |
||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
||||
|
DEVELOPMENT_TEAM = 42C82SZUVX; |
||||
|
ENABLE_BITCODE = NO; |
||||
|
INFOPLIST_FILE = Runner/Info.plist; |
||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
||||
|
LD_RUNPATH_SEARCH_PATHS = ( |
||||
|
"$(inherited)", |
||||
|
"@executable_path/Frameworks", |
||||
|
); |
||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; |
||||
|
PRODUCT_NAME = "$(TARGET_NAME)"; |
||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
||||
|
SWIFT_VERSION = 5.0; |
||||
|
VERSIONING_SYSTEM = "apple-generic"; |
||||
|
}; |
||||
|
name = Profile; |
||||
|
}; |
||||
|
331C8088294A63A400263BE5 /* Debug */ = { |
||||
|
isa = XCBuildConfiguration; |
||||
|
baseConfigurationReference = 209BB2477C918E1C69F5F1E0 /* Pods-RunnerTests.debug.xcconfig */; |
||||
|
buildSettings = { |
||||
|
BUNDLE_LOADER = "$(TEST_HOST)"; |
||||
|
CODE_SIGN_STYLE = Automatic; |
||||
|
CURRENT_PROJECT_VERSION = 1; |
||||
|
GENERATE_INFOPLIST_FILE = YES; |
||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
||||
|
MARKETING_VERSION = 1.0; |
||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; |
||||
|
PRODUCT_NAME = "$(TARGET_NAME)"; |
||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; |
||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; |
||||
|
SWIFT_VERSION = 5.0; |
||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; |
||||
|
}; |
||||
|
name = Debug; |
||||
|
}; |
||||
|
331C8089294A63A400263BE5 /* Release */ = { |
||||
|
isa = XCBuildConfiguration; |
||||
|
baseConfigurationReference = E75F8D421A56891E8C81AD82 /* Pods-RunnerTests.release.xcconfig */; |
||||
|
buildSettings = { |
||||
|
BUNDLE_LOADER = "$(TEST_HOST)"; |
||||
|
CODE_SIGN_STYLE = Automatic; |
||||
|
CURRENT_PROJECT_VERSION = 1; |
||||
|
GENERATE_INFOPLIST_FILE = YES; |
||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
||||
|
MARKETING_VERSION = 1.0; |
||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; |
||||
|
PRODUCT_NAME = "$(TARGET_NAME)"; |
||||
|
SWIFT_VERSION = 5.0; |
||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; |
||||
|
}; |
||||
|
name = Release; |
||||
|
}; |
||||
|
331C808A294A63A400263BE5 /* Profile */ = { |
||||
|
isa = XCBuildConfiguration; |
||||
|
baseConfigurationReference = FD4B933141D4D3B35A169527 /* Pods-RunnerTests.profile.xcconfig */; |
||||
|
buildSettings = { |
||||
|
BUNDLE_LOADER = "$(TEST_HOST)"; |
||||
|
CODE_SIGN_STYLE = Automatic; |
||||
|
CURRENT_PROJECT_VERSION = 1; |
||||
|
GENERATE_INFOPLIST_FILE = YES; |
||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
||||
|
MARKETING_VERSION = 1.0; |
||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound.RunnerTests; |
||||
|
PRODUCT_NAME = "$(TARGET_NAME)"; |
||||
|
SWIFT_VERSION = 5.0; |
||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; |
||||
|
}; |
||||
|
name = Profile; |
||||
|
}; |
||||
|
97C147031CF9000F007C117D /* Debug */ = { |
||||
|
isa = XCBuildConfiguration; |
||||
|
buildSettings = { |
||||
|
ALWAYS_SEARCH_USER_PATHS = NO; |
||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; |
||||
|
CLANG_ANALYZER_NONNULL = YES; |
||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; |
||||
|
CLANG_CXX_LIBRARY = "libc++"; |
||||
|
CLANG_ENABLE_MODULES = YES; |
||||
|
CLANG_ENABLE_OBJC_ARC = YES; |
||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; |
||||
|
CLANG_WARN_BOOL_CONVERSION = YES; |
||||
|
CLANG_WARN_COMMA = YES; |
||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES; |
||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; |
||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; |
||||
|
CLANG_WARN_EMPTY_BODY = YES; |
||||
|
CLANG_WARN_ENUM_CONVERSION = YES; |
||||
|
CLANG_WARN_INFINITE_RECURSION = YES; |
||||
|
CLANG_WARN_INT_CONVERSION = YES; |
||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; |
||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; |
||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; |
||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; |
||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; |
||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; |
||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES; |
||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES; |
||||
|
CLANG_WARN_UNREACHABLE_CODE = YES; |
||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; |
||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; |
||||
|
COPY_PHASE_STRIP = NO; |
||||
|
DEBUG_INFORMATION_FORMAT = dwarf; |
||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES; |
||||
|
ENABLE_TESTABILITY = YES; |
||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO; |
||||
|
GCC_C_LANGUAGE_STANDARD = gnu99; |
||||
|
GCC_DYNAMIC_NO_PIC = NO; |
||||
|
GCC_NO_COMMON_BLOCKS = YES; |
||||
|
GCC_OPTIMIZATION_LEVEL = 0; |
||||
|
GCC_PREPROCESSOR_DEFINITIONS = ( |
||||
|
"DEBUG=1", |
||||
|
"$(inherited)", |
||||
|
); |
||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; |
||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; |
||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES; |
||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
||||
|
GCC_WARN_UNUSED_FUNCTION = YES; |
||||
|
GCC_WARN_UNUSED_VARIABLE = YES; |
||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
||||
|
MTL_ENABLE_DEBUG_INFO = YES; |
||||
|
ONLY_ACTIVE_ARCH = YES; |
||||
|
SDKROOT = iphoneos; |
||||
|
TARGETED_DEVICE_FAMILY = "1,2"; |
||||
|
}; |
||||
|
name = Debug; |
||||
|
}; |
||||
|
97C147041CF9000F007C117D /* Release */ = { |
||||
|
isa = XCBuildConfiguration; |
||||
|
buildSettings = { |
||||
|
ALWAYS_SEARCH_USER_PATHS = NO; |
||||
|
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; |
||||
|
CLANG_ANALYZER_NONNULL = YES; |
||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; |
||||
|
CLANG_CXX_LIBRARY = "libc++"; |
||||
|
CLANG_ENABLE_MODULES = YES; |
||||
|
CLANG_ENABLE_OBJC_ARC = YES; |
||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; |
||||
|
CLANG_WARN_BOOL_CONVERSION = YES; |
||||
|
CLANG_WARN_COMMA = YES; |
||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES; |
||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; |
||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; |
||||
|
CLANG_WARN_EMPTY_BODY = YES; |
||||
|
CLANG_WARN_ENUM_CONVERSION = YES; |
||||
|
CLANG_WARN_INFINITE_RECURSION = YES; |
||||
|
CLANG_WARN_INT_CONVERSION = YES; |
||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; |
||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; |
||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; |
||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; |
||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; |
||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; |
||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES; |
||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES; |
||||
|
CLANG_WARN_UNREACHABLE_CODE = YES; |
||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; |
||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; |
||||
|
COPY_PHASE_STRIP = NO; |
||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; |
||||
|
ENABLE_NS_ASSERTIONS = NO; |
||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES; |
||||
|
ENABLE_USER_SCRIPT_SANDBOXING = NO; |
||||
|
GCC_C_LANGUAGE_STANDARD = gnu99; |
||||
|
GCC_NO_COMMON_BLOCKS = YES; |
||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES; |
||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; |
||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES; |
||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; |
||||
|
GCC_WARN_UNUSED_FUNCTION = YES; |
||||
|
GCC_WARN_UNUSED_VARIABLE = YES; |
||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
||||
|
MTL_ENABLE_DEBUG_INFO = NO; |
||||
|
SDKROOT = iphoneos; |
||||
|
SUPPORTED_PLATFORMS = iphoneos; |
||||
|
SWIFT_COMPILATION_MODE = wholemodule; |
||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O"; |
||||
|
TARGETED_DEVICE_FAMILY = "1,2"; |
||||
|
VALIDATE_PRODUCT = YES; |
||||
|
}; |
||||
|
name = Release; |
||||
|
}; |
||||
|
97C147061CF9000F007C117D /* Debug */ = { |
||||
|
isa = XCBuildConfiguration; |
||||
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; |
||||
|
buildSettings = { |
||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
||||
|
CLANG_ENABLE_MODULES = YES; |
||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; |
||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
||||
|
DEVELOPMENT_TEAM = 42C82SZUVX; |
||||
|
ENABLE_BITCODE = NO; |
||||
|
INFOPLIST_FILE = Runner/Info.plist; |
||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
||||
|
LD_RUNPATH_SEARCH_PATHS = ( |
||||
|
"$(inherited)", |
||||
|
"@executable_path/Frameworks", |
||||
|
); |
||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; |
||||
|
PRODUCT_NAME = "$(TARGET_NAME)"; |
||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; |
||||
|
SWIFT_VERSION = 5.0; |
||||
|
VERSIONING_SYSTEM = "apple-generic"; |
||||
|
}; |
||||
|
name = Debug; |
||||
|
}; |
||||
|
97C147071CF9000F007C117D /* Release */ = { |
||||
|
isa = XCBuildConfiguration; |
||||
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; |
||||
|
buildSettings = { |
||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; |
||||
|
CLANG_ENABLE_MODULES = YES; |
||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; |
||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; |
||||
|
DEVELOPMENT_TEAM = 42C82SZUVX; |
||||
|
ENABLE_BITCODE = NO; |
||||
|
INFOPLIST_FILE = Runner/Info.plist; |
||||
|
IPHONEOS_DEPLOYMENT_TARGET = 18.0; |
||||
|
LD_RUNPATH_SEARCH_PATHS = ( |
||||
|
"$(inherited)", |
||||
|
"@executable_path/Frameworks", |
||||
|
); |
||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.yunqiinnovation.deepsound; |
||||
|
PRODUCT_NAME = "$(TARGET_NAME)"; |
||||
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; |
||||
|
SWIFT_VERSION = 5.0; |
||||
|
VERSIONING_SYSTEM = "apple-generic"; |
||||
|
}; |
||||
|
name = Release; |
||||
|
}; |
||||
|
/* End XCBuildConfiguration section */ |
||||
|
|
||||
|
/* Begin XCConfigurationList section */ |
||||
|
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { |
||||
|
isa = XCConfigurationList; |
||||
|
buildConfigurations = ( |
||||
|
331C8088294A63A400263BE5 /* Debug */, |
||||
|
331C8089294A63A400263BE5 /* Release */, |
||||
|
331C808A294A63A400263BE5 /* Profile */, |
||||
|
); |
||||
|
defaultConfigurationIsVisible = 0; |
||||
|
defaultConfigurationName = Release; |
||||
|
}; |
||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { |
||||
|
isa = XCConfigurationList; |
||||
|
buildConfigurations = ( |
||||
|
97C147031CF9000F007C117D /* Debug */, |
||||
|
97C147041CF9000F007C117D /* Release */, |
||||
|
249021D3217E4FDB00AE95B9 /* Profile */, |
||||
|
); |
||||
|
defaultConfigurationIsVisible = 0; |
||||
|
defaultConfigurationName = Release; |
||||
|
}; |
||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { |
||||
|
isa = XCConfigurationList; |
||||
|
buildConfigurations = ( |
||||
|
97C147061CF9000F007C117D /* Debug */, |
||||
|
97C147071CF9000F007C117D /* Release */, |
||||
|
249021D4217E4FDB00AE95B9 /* Profile */, |
||||
|
); |
||||
|
defaultConfigurationIsVisible = 0; |
||||
|
defaultConfigurationName = Release; |
||||
|
}; |
||||
|
/* End XCConfigurationList section */ |
||||
|
|
||||
|
/* Begin XCRemoteSwiftPackageReference section */ |
||||
|
0AD3C3752DDE122500FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */ = { |
||||
|
isa = XCRemoteSwiftPackageReference; |
||||
|
repositoryURL = "https://github.com/alta/swift-opus"; |
||||
|
requirement = { |
||||
|
kind = upToNextMajorVersion; |
||||
|
minimumVersion = 0.0.2; |
||||
|
}; |
||||
|
}; |
||||
|
0AD3C37A2DDE139600FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */ = { |
||||
|
isa = XCRemoteSwiftPackageReference; |
||||
|
repositoryURL = "https://github.com/alta/swift-opus"; |
||||
|
requirement = { |
||||
|
kind = upToNextMajorVersion; |
||||
|
minimumVersion = 0.0.2; |
||||
|
}; |
||||
|
}; |
||||
|
/* End XCRemoteSwiftPackageReference section */ |
||||
|
|
||||
|
/* Begin XCSwiftPackageProductDependency section */ |
||||
|
0AD3C3762DDE122500FD4F41 /* Opus */ = { |
||||
|
isa = XCSwiftPackageProductDependency; |
||||
|
package = 0AD3C3752DDE122500FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */; |
||||
|
productName = Opus; |
||||
|
}; |
||||
|
0AD3C37B2DDE142500FD4F41 /* Opus */ = { |
||||
|
isa = XCSwiftPackageProductDependency; |
||||
|
package = 0AD3C37A2DDE139600FD4F41 /* XCRemoteSwiftPackageReference "swift-opus" */; |
||||
|
productName = Opus; |
||||
|
}; |
||||
|
/* End XCSwiftPackageProductDependency section */ |
||||
|
}; |
||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */; |
||||
|
} |
||||
@ -0,0 +1,15 @@ |
|||||
|
{ |
||||
|
"originHash" : "bd9b3a14091d57af541f2718cbec3c63a0b618a9a7c0674743c7cf43998a352d", |
||||
|
"pins" : [ |
||||
|
{ |
||||
|
"identity" : "swift-opus", |
||||
|
"kind" : "remoteSourceControl", |
||||
|
"location" : "https://github.com/alta/swift-opus", |
||||
|
"state" : { |
||||
|
"revision" : "5f0164b8915fb9001265c54413aa0f0efbd3df06", |
||||
|
"version" : "0.0.2" |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
"version" : 3 |
||||
|
} |
||||
@ -1,994 +0,0 @@ |
|||||
import Foundation |
|
||||
import MicrosoftCognitiveServicesSpeech |
|
||||
import open_ai_service |
|
||||
import chat_storage |
|
||||
|
|
||||
/// 代理服务 - 处理语音识别、TTS和AI对话相关逻辑 |
|
||||
/// |
|
||||
/// 负责集成Azure语音服务、OpenAI服务和本地存储服务, |
|
||||
/// 提供语音识别、语音合成、AI对话等功能 |
|
||||
class AgentService { |
|
||||
// 常量定义 |
|
||||
private let TAG = "AgentService" |
|
||||
|
|
||||
// 上下文和监听器 |
|
||||
private var listeners = NSHashTable<AnyObject>.weakObjects() |
|
||||
|
|
||||
// 配置参数 |
|
||||
private var azureSpeechKey: String = "" |
|
||||
private var azureSpeechRegion: String = "" |
|
||||
private var openaiApiKey: String = "" |
|
||||
private var openaiBaseUrl: String = "" |
|
||||
private var openaiModel: String = "gpt-3.5-turbo" |
|
||||
private var mcpServer: String = "" |
|
||||
|
|
||||
// Azure服务 |
|
||||
private var azureAsrHelper: AzureAsrHelper? |
|
||||
private var azureTtsHelper: AzureTtsHelper? |
|
||||
|
|
||||
// OpenAI服务 |
|
||||
private var openAIService: OpenAIServiceBridge? |
|
||||
|
|
||||
// 聊天存储服务 |
|
||||
private var chatStorageHelper: ChatStorageHelperBridge? |
|
||||
|
|
||||
// 会话ID,用于区分不同聊天上下文 |
|
||||
private let sessionId = "default_agent" |
|
||||
|
|
||||
// 历史聊天消息缓存 |
|
||||
private var historyMessages = [[String: Any]]() |
|
||||
|
|
||||
// 系统提示词 |
|
||||
private var systemPrompt = "" |
|
||||
|
|
||||
// 状态 |
|
||||
private var isInitialized = false |
|
||||
private var isRecognitionActive = false |
|
||||
private var isTtsSpeaking = false |
|
||||
private var hasSpeechDetected = false |
|
||||
private var isAiStreaming = false |
|
||||
|
|
||||
// AI流生成相关 |
|
||||
private var currentAiTask: DispatchWorkItem? |
|
||||
|
|
||||
// 空闲检测相关 |
|
||||
private var idleCheckTimer: Timer? |
|
||||
private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数 |
|
||||
|
|
||||
/// 初始化 |
|
||||
init() { |
|
||||
// 创建OpenAI服务桥接器 |
|
||||
openAIService = OpenAIServiceBridge() |
|
||||
|
|
||||
// 创建聊天存储桥接器 |
|
||||
chatStorageHelper = ChatStorageHelperBridge() |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 初始化系统提示词 |
|
||||
*/ |
|
||||
private func initSystemPrompt() { |
|
||||
systemPrompt = """ |
|
||||
你是一个友好、专业的语音助手,名叫"小语"。你的目标是通过对话为用户提供帮助、解答问题和完成任务。 |
|
||||
遵循以下指导原则: |
|
||||
1. 保持简短精炼的回答,因为用户是通过语音与你交流 |
|
||||
2. 优先使用中文回复,除非用户明确要求使用其他语言 |
|
||||
3. 当用户问题不明确时,礼貌地请求更多信息 |
|
||||
4. 避免过长的列表,尽量将信息分成小段 |
|
||||
5. 不要使用需要视觉展示的元素(如表格、图表或代码块) |
|
||||
6. 记住用户之前的对话内容,保持对话连贯 |
|
||||
7. 如果用户发送了图片,请根据图片内容和文字要求回答问题 |
|
||||
你不仅可以回答知识性问题,还可以帮助用户设置提醒、提供建议,或进行轻松愉快的对话。 |
|
||||
无论遇到什么问题,都要尽力以温暖、贴心的语气提供最佳帮助。 |
|
||||
""" |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 初始化 |
|
||||
* |
|
||||
* - Parameter config: 配置参数,包含所需的所有API密钥和设置 |
|
||||
* - Returns: 是否初始化成功 |
|
||||
*/ |
|
||||
func initialize(config: [String: Any]) -> Bool { |
|
||||
if isInitialized { return true } |
|
||||
|
|
||||
do { |
|
||||
// 从配置中获取参数 |
|
||||
if let azureKey = config["azureSpeechKey"] as? String { |
|
||||
azureSpeechKey = azureKey |
|
||||
} |
|
||||
|
|
||||
if let azureRegion = config["azureSpeechRegion"] as? String { |
|
||||
azureSpeechRegion = azureRegion |
|
||||
} |
|
||||
|
|
||||
if let openaiKey = config["openaiApiKey"] as? String { |
|
||||
openaiApiKey = openaiKey |
|
||||
} |
|
||||
|
|
||||
if let baseUrl = config["openaiBaseUrl"] as? String { |
|
||||
openaiBaseUrl = baseUrl |
|
||||
} |
|
||||
|
|
||||
if let model = config["openaiModel"] as? String { |
|
||||
openaiModel = model |
|
||||
} |
|
||||
|
|
||||
if let server = config["mcpServer"] as? String { |
|
||||
mcpServer = server |
|
||||
} |
|
||||
|
|
||||
// 自定义系统提示词 |
|
||||
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { |
|
||||
systemPrompt = customSystemPrompt |
|
||||
} else { |
|
||||
// 使用默认系统提示词 |
|
||||
initSystemPrompt() |
|
||||
} |
|
||||
|
|
||||
// 检查必要参数 |
|
||||
if azureSpeechKey.isEmpty || azureSpeechRegion.isEmpty || openaiApiKey.isEmpty { |
|
||||
print("\(TAG): 初始化失败:关键配置参数缺失") |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
// 初始化OpenAI服务 |
|
||||
openAIService?.initialize(apiKey: openaiApiKey, baseUrl: openaiBaseUrl, model: openaiModel, mcpServer: mcpServer) |
|
||||
|
|
||||
// 初始化Azure ASR |
|
||||
azureAsrHelper = AzureAsrHelper(self) |
|
||||
let asrInitResult = azureAsrHelper?.initialize( |
|
||||
subscriptionKey: azureSpeechKey, |
|
||||
region: azureSpeechRegion, |
|
||||
audioSourceType: .microphone |
|
||||
) ?? false |
|
||||
|
|
||||
if !asrInitResult { |
|
||||
print("\(TAG): Azure ASR初始化失败") |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
// 初始化Azure TTS |
|
||||
azureTtsHelper = AzureTtsHelper(self) |
|
||||
let ttsInitResult = azureTtsHelper?.initialize( |
|
||||
subscriptionKey: azureSpeechKey, |
|
||||
region: azureSpeechRegion, |
|
||||
callback: self |
|
||||
) ?? false |
|
||||
|
|
||||
if !ttsInitResult { |
|
||||
print("\(TAG): Azure TTS初始化失败") |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
// 加载最近的聊天记录 |
|
||||
loadChatHistory() |
|
||||
|
|
||||
isInitialized = true |
|
||||
print("\(TAG): 代理服务初始化成功") |
|
||||
return true |
|
||||
} catch { |
|
||||
print("\(TAG): 初始化失败:\(error.localizedDescription)") |
|
||||
return false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 添加事件监听器 |
|
||||
* |
|
||||
* - Parameter listener: 要添加的监听器 |
|
||||
*/ |
|
||||
func addListener(_ listener: AgentServiceListener) { |
|
||||
listeners.add(listener as AnyObject) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 设置事件监听器(替换现有监听器) |
|
||||
* |
|
||||
* - Parameter listener: 要设置的监听器 |
|
||||
*/ |
|
||||
func setListener(_ listener: AgentServiceListener) { |
|
||||
listeners.removeAllObjects() |
|
||||
listeners.add(listener as AnyObject) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 移除事件监听器 |
|
||||
* |
|
||||
* - Parameter listener: 要移除的监听器 |
|
||||
*/ |
|
||||
func removeListener(_ listener: AgentServiceListener) { |
|
||||
listeners.remove(listener as AnyObject) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 移除所有事件监听器 |
|
||||
*/ |
|
||||
func clearListeners() { |
|
||||
listeners.removeAllObjects() |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 启动空闲检测 |
|
||||
*/ |
|
||||
private func startIdleCheck() { |
|
||||
stopIdleCheck() // 先停止现有的检查 |
|
||||
|
|
||||
if !isRecognitionActive { return } |
|
||||
|
|
||||
// 创建定时器 |
|
||||
idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in |
|
||||
guard let self = self else { return } |
|
||||
|
|
||||
// 如果状态仍然是空闲的,则停止识别 |
|
||||
if self.isRecognitionActive && !self.hasSpeechDetected && !self.isTtsSpeaking && !self.isAiStreaming { |
|
||||
self.stopRecognition() |
|
||||
self.sendEvent("auto_stop", ["reason": "idle_timeout", "seconds": self.maxIdleSeconds]) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 停止空闲检测 |
|
||||
*/ |
|
||||
private func stopIdleCheck() { |
|
||||
idleCheckTimer?.invalidate() |
|
||||
idleCheckTimer = nil |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 重启空闲检测 |
|
||||
* 当状态发生变化时调用 |
|
||||
*/ |
|
||||
private func restartIdleCheck() { |
|
||||
if isRecognitionActive { |
|
||||
startIdleCheck() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 开始语音识别 |
|
||||
* |
|
||||
* - Returns: 是否成功开始识别 |
|
||||
*/ |
|
||||
func startRecognition() -> Bool { |
|
||||
if !isInitialized { |
|
||||
print("\(TAG): 服务未初始化") |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
if isRecognitionActive { return true } |
|
||||
|
|
||||
isRecognitionActive = true |
|
||||
hasSpeechDetected = false |
|
||||
|
|
||||
do { |
|
||||
// 创建连续识别回调 |
|
||||
class ContinuousRecognizeCallbackImpl: AzureAsrHelper.ContinuousRecognizeCallback { |
|
||||
private weak var service: AgentService? |
|
||||
|
|
||||
init(_ service: AgentService) { |
|
||||
self.service = service |
|
||||
} |
|
||||
|
|
||||
func onRecognizing(recognizing: String, detectedLanguage: String) { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
if !recognizing.isEmpty { |
|
||||
// 检测到语音,更新状态 |
|
||||
let previousHasSpeech = service.hasSpeechDetected |
|
||||
service.hasSpeechDetected = true |
|
||||
|
|
||||
// 状态发生变化时重启空闲检测 |
|
||||
if !previousHasSpeech { |
|
||||
service.restartIdleCheck() |
|
||||
} |
|
||||
|
|
||||
service.sendEvent("recognizing", [ |
|
||||
"text": recognizing, |
|
||||
"language": detectedLanguage |
|
||||
]) |
|
||||
|
|
||||
// 如果TTS正在播放或AI正在生成,则触发打断 |
|
||||
if service.isTtsSpeaking || service.isAiStreaming { |
|
||||
service.interruptCurrentResponse() |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func onResult(text: String, detectedLanguage: String) { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
if !text.isEmpty { |
|
||||
service.sendEvent("recognition_result", [ |
|
||||
"text": text, |
|
||||
"language": detectedLanguage |
|
||||
]) |
|
||||
|
|
||||
service.processWithOpenAI(text: text) |
|
||||
} |
|
||||
|
|
||||
// 重置状态,继续识别 |
|
||||
let previousHasSpeech = service.hasSpeechDetected |
|
||||
service.hasSpeechDetected = false |
|
||||
|
|
||||
// 状态发生变化时重启空闲检测 |
|
||||
if previousHasSpeech { |
|
||||
service.restartIdleCheck() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func onSessionStarted() { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
service.sendEvent("recognition_started", ["status": "started"]) |
|
||||
// 启动空闲检测 |
|
||||
service.startIdleCheck() |
|
||||
} |
|
||||
|
|
||||
func onSessionStopped() { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
service.isRecognitionActive = false |
|
||||
service.stopIdleCheck() |
|
||||
service.sendEvent("recognition_stopped", ["status": "stopped"]) |
|
||||
} |
|
||||
|
|
||||
func onCanceled(reason: String, errorDetails: String) { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
service.isRecognitionActive = false |
|
||||
service.stopIdleCheck() |
|
||||
service.sendEvent("recognition_canceled", [ |
|
||||
"reason": reason, |
|
||||
"details": errorDetails |
|
||||
]) |
|
||||
} |
|
||||
|
|
||||
func onError(error: String) { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
service.isRecognitionActive = false |
|
||||
service.stopIdleCheck() |
|
||||
print("\(service.TAG): 语音识别出错: \(error)") |
|
||||
service.sendEvent("error", [ |
|
||||
"code": "RECOGNITION_ERROR", |
|
||||
"message": error |
|
||||
]) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
let callback = ContinuousRecognizeCallbackImpl(self) |
|
||||
let result = azureAsrHelper?.startContinuousRecognition(callback) ?? false |
|
||||
|
|
||||
if !result { |
|
||||
isRecognitionActive = false |
|
||||
print("\(TAG): 启动语音识别失败") |
|
||||
sendEvent("error", [ |
|
||||
"code": "RECOGNITION_START_ERROR", |
|
||||
"message": "启动语音识别失败" |
|
||||
]) |
|
||||
} |
|
||||
|
|
||||
return result |
|
||||
} catch { |
|
||||
isRecognitionActive = false |
|
||||
print("\(TAG): 启动语音识别失败: \(error.localizedDescription)") |
|
||||
sendEvent("error", [ |
|
||||
"code": "RECOGNITION_START_ERROR", |
|
||||
"message": error.localizedDescription |
|
||||
]) |
|
||||
return false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 停止语音识别 |
|
||||
*/ |
|
||||
func stopRecognition() { |
|
||||
if !isRecognitionActive { return } |
|
||||
|
|
||||
print("\(TAG): 停止语音识别") |
|
||||
|
|
||||
// 停止识别 |
|
||||
let _ = azureAsrHelper?.stopContinuousRecognition() |
|
||||
isRecognitionActive = false |
|
||||
stopIdleCheck() |
|
||||
print("\(TAG): 语音识别已停止") |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 打断当前响应 |
|
||||
* 停止TTS播放和AI流输出 |
|
||||
*/ |
|
||||
func interruptCurrentResponse() { |
|
||||
if isAiStreaming || isTtsSpeaking { |
|
||||
// 停止TTS播放 |
|
||||
stopTts() |
|
||||
|
|
||||
// 停止AI流输出 |
|
||||
stopAiStream() |
|
||||
|
|
||||
// 发送打断事件 |
|
||||
sendEvent("response_interrupted", ["status": "interrupted"]) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 停止AI流输出 |
|
||||
*/ |
|
||||
private func stopAiStream() { |
|
||||
if isAiStreaming { |
|
||||
// 取消当前AI生成任务 |
|
||||
currentAiTask?.cancel() |
|
||||
currentAiTask = nil |
|
||||
|
|
||||
// 通知OpenAI服务终止当前流式请求 |
|
||||
openAIService?.cancelCurrentStream() |
|
||||
|
|
||||
// 更新状态 |
|
||||
isAiStreaming = false |
|
||||
|
|
||||
// 记录日志 |
|
||||
print("\(TAG): AI流输出已停止") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 处理文本输入 |
|
||||
* 作为语音输入的补充,直接处理文本并通过事件返回结果 |
|
||||
* |
|
||||
* - Parameters: |
|
||||
* - text: 用户输入文本 |
|
||||
* - speakResponse: 是否朗读回复,默认为false |
|
||||
* - Returns: 是否成功开始处理 |
|
||||
*/ |
|
||||
func processTextInput(text: String, speakResponse: Bool = false) -> Bool { |
|
||||
if !isInitialized { |
|
||||
print("\(TAG): 服务未初始化") |
|
||||
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"]) |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
if text.isEmpty { |
|
||||
print("\(TAG): 文本输入不能为空") |
|
||||
sendEvent("error", ["code": "EMPTY_TEXT", "message": "文本输入不能为空"]) |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
// 使用OpenAI处理文本 |
|
||||
processWithOpenAI(text: text, speakResponse: speakResponse) |
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 使用OpenAI处理语音识别结果 |
|
||||
* |
|
||||
* - Parameter text: 用户输入文本 |
|
||||
*/ |
|
||||
private func processWithOpenAI(text: String) { |
|
||||
processWithOpenAI(text: text, speakResponse: true) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 使用OpenAI处理文本消息 |
|
||||
* |
|
||||
* - Parameters: |
|
||||
* - text: 用户输入文本 |
|
||||
* - speakResponse: 是否使用TTS朗读回复 |
|
||||
*/ |
|
||||
private func processWithOpenAI(text: String, speakResponse: Bool = true) { |
|
||||
print("\(TAG): 用户问题: \(text)") |
|
||||
|
|
||||
// 创建用户文本消息并处理 |
|
||||
guard let userMessage = openAIService?.createUserMessage(text: text) else { |
|
||||
print("\(TAG): 创建用户消息失败") |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
processWithOpenAIInternal(userMessage: userMessage, displayText: text, speakResponse: speakResponse) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 使用OpenAI处理图片 |
|
||||
* |
|
||||
* - Parameters: |
|
||||
* - imagePath: 图片文件路径 |
|
||||
* - text: 可选的文本描述或问题 |
|
||||
* - speakResponse: 是否朗读回复 |
|
||||
* - Returns: 是否成功开始处理 |
|
||||
*/ |
|
||||
func processImageInput(imagePath: String, text: String = "", speakResponse: Bool = false) -> Bool { |
|
||||
if !isInitialized { |
|
||||
print("\(TAG): 服务未初始化") |
|
||||
sendEvent("error", ["code": "NOT_INITIALIZED", "message": "服务未初始化"]) |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
if imagePath.isEmpty { |
|
||||
print("\(TAG): 图片路径不能为空") |
|
||||
sendEvent("error", ["code": "EMPTY_IMAGE_PATH", "message": "图片路径不能为空"]) |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
// 通知开始处理图片 |
|
||||
sendEvent("image_processing", [ |
|
||||
"status": "processing", |
|
||||
"imagePath": imagePath |
|
||||
]) |
|
||||
|
|
||||
// 异步处理图片 |
|
||||
DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
|
||||
guard let self = self else { return } |
|
||||
|
|
||||
// 将图片转换为Base64格式 |
|
||||
guard let imageBase64 = self.openAIService?.fileToBase64(filePath: imagePath) else { |
|
||||
DispatchQueue.main.async { |
|
||||
print("\(self.TAG): 图片转换失败: \(imagePath)") |
|
||||
self.sendEvent("error", [ |
|
||||
"code": "IMAGE_CONVERSION_FAILED", |
|
||||
"message": "图片转换失败" |
|
||||
]) |
|
||||
} |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 通知图片准备完成 |
|
||||
DispatchQueue.main.async { |
|
||||
self.sendEvent("image_ready", [ |
|
||||
"status": "ready", |
|
||||
"imagePath": imagePath |
|
||||
]) |
|
||||
|
|
||||
// 处理包含图片的消息 |
|
||||
self.processImageWithOpenAI(imageBase64: imageBase64, text: text, speakResponse: speakResponse) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return true |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 使用OpenAI处理图片 |
|
||||
* |
|
||||
* - Parameters: |
|
||||
* - imageBase64: Base64编码的图片数据 |
|
||||
* - text: 可选的文本描述或问题 |
|
||||
* - speakResponse: 是否朗读回复 |
|
||||
*/ |
|
||||
private func processImageWithOpenAI(imageBase64: String, text: String = "", speakResponse: Bool = false) { |
|
||||
print("\(TAG): 处理图片输入: \(text.isEmpty ? "无附加文本" : "附带文本: \(text)")") |
|
||||
|
|
||||
// 创建带图片的用户消息并处理 |
|
||||
guard let userMessage = openAIService?.createUserMessageWithImage(text: text, imageBase64: imageBase64) else { |
|
||||
print("\(TAG): 创建带图片的用户消息失败") |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 图片描述用于存储 |
|
||||
let displayText = text.isEmpty ? "[图片]" : text |
|
||||
processWithOpenAIInternal(userMessage: userMessage, displayText: displayText, speakResponse: speakResponse, hasImage: true) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 内部方法:通用的OpenAI处理逻辑 |
|
||||
* |
|
||||
* - Parameters: |
|
||||
* - userMessage: 用户消息(可以是文本或图片格式) |
|
||||
* - displayText: 用于显示和存储的文本 |
|
||||
* - speakResponse: 是否朗读回复 |
|
||||
* - hasImage: 是否包含图片 |
|
||||
*/ |
|
||||
private func processWithOpenAIInternal(userMessage: [String: Any], displayText: String, speakResponse: Bool = true, hasImage: Bool = false) { |
|
||||
// 如果有正在进行的AI流式输出,先停止它 |
|
||||
stopAiStream() |
|
||||
|
|
||||
// 创建AI任务 |
|
||||
let workItem = DispatchWorkItem { [weak self] in |
|
||||
guard let self = self else { return } |
|
||||
|
|
||||
// 设置状态为正在流式输出 |
|
||||
self.isAiStreaming = true |
|
||||
|
|
||||
// 使用历史记录作为上下文发送到OpenAI |
|
||||
var responseBuilder = "" |
|
||||
|
|
||||
// 添加系统提示词到历史消息的副本中 |
|
||||
var messagesWithSystemPrompt: [[String: Any]] = [] |
|
||||
|
|
||||
// 先添加系统提示词 |
|
||||
if !self.systemPrompt.isEmpty { |
|
||||
if let systemMessage = self.openAIService?.createSystemMessage(text: self.systemPrompt) { |
|
||||
messagesWithSystemPrompt.append(systemMessage) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// 再添加历史消息 |
|
||||
messagesWithSystemPrompt.append(contentsOf: self.historyMessages) |
|
||||
messagesWithSystemPrompt.append(userMessage) |
|
||||
|
|
||||
// 将用户消息添加到历史记录 |
|
||||
self.addToHistoryMessages(userMessage) |
|
||||
|
|
||||
// 流式回调 |
|
||||
class StreamCallbackBridge: NSObject, OpenAIStreamCallback { |
|
||||
private weak var service: AgentService? |
|
||||
private var responseBuilder: String |
|
||||
private let speakResponse: Bool |
|
||||
private let displayText: String |
|
||||
private let hasImage: Bool |
|
||||
|
|
||||
init(_ service: AgentService, responseBuilder: String = "", speakResponse: Bool, displayText: String, hasImage: Bool) { |
|
||||
self.service = service |
|
||||
self.responseBuilder = responseBuilder |
|
||||
self.speakResponse = speakResponse |
|
||||
self.displayText = displayText |
|
||||
self.hasImage = hasImage |
|
||||
super.init() |
|
||||
} |
|
||||
|
|
||||
func onToken(token: String) { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
responseBuilder.append(token) |
|
||||
if speakResponse { |
|
||||
_ = service.azureTtsHelper?.speakStream(token) |
|
||||
} |
|
||||
// 发送流式回复token |
|
||||
service.sendEvent("assistant_token", ["token": token]) |
|
||||
} |
|
||||
|
|
||||
func onComplete() { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
// 视情况决定是否朗读回复 |
|
||||
if speakResponse { |
|
||||
_ = service.azureTtsHelper?.flushStream() |
|
||||
} |
|
||||
|
|
||||
if !responseBuilder.isEmpty { |
|
||||
// 发送完整回复,包含是否有图片的标记 |
|
||||
var responseData: [String: Any] = [ |
|
||||
"text": responseBuilder, |
|
||||
"userInput": displayText |
|
||||
] |
|
||||
|
|
||||
if hasImage { |
|
||||
responseData["hasImage"] = true |
|
||||
} |
|
||||
|
|
||||
service.sendEvent("assistant_response", responseData) |
|
||||
|
|
||||
// 添加AI回复到历史记录 |
|
||||
if let assistantMessage = service.openAIService?.createAssistantMessage(text: responseBuilder) { |
|
||||
service.addToHistoryMessages(assistantMessage) |
|
||||
} |
|
||||
|
|
||||
// 保存聊天记录 |
|
||||
service.saveChatMessage(userMessage: displayText, assistantMessage: responseBuilder) |
|
||||
} |
|
||||
|
|
||||
// 标记AI流式输出已完成 |
|
||||
service.isAiStreaming = false |
|
||||
service.currentAiTask = nil |
|
||||
} |
|
||||
|
|
||||
func onError(error: Error) { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
print("\(service.TAG): AI处理出错: \(error.localizedDescription)") |
|
||||
service.sendEvent("error", [ |
|
||||
"code": "AI_ERROR", |
|
||||
"message": error.localizedDescription |
|
||||
]) |
|
||||
|
|
||||
// 标记AI流式输出已完成 |
|
||||
service.isAiStreaming = false |
|
||||
service.currentAiTask = nil |
|
||||
} |
|
||||
|
|
||||
func onFunctionCall(call: [String: Any]) { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
if let name = call["name"] as? String { |
|
||||
service.sendEvent("function_call", [ |
|
||||
"name": name, |
|
||||
"arguments": call |
|
||||
]) |
|
||||
|
|
||||
if name == "exit_interaction" { |
|
||||
service.stopRecognition() |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) { |
|
||||
guard let service = service else { return } |
|
||||
|
|
||||
service.sendEvent("function_call_result", [ |
|
||||
"function_call": functionCall, |
|
||||
"result": functionCallResult, |
|
||||
]) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
let callback = StreamCallbackBridge(self, responseBuilder: responseBuilder, speakResponse: speakResponse, displayText: displayText, hasImage: hasImage) |
|
||||
|
|
||||
self.openAIService?.sendMessageStream(messages: messagesWithSystemPrompt, callback: callback) |
|
||||
} |
|
||||
|
|
||||
// 保存任务引用并在全局队列中执行 |
|
||||
currentAiTask = workItem |
|
||||
DispatchQueue.global(qos: .userInitiated).async(execute: workItem) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 加载最近的聊天历史记录 |
|
||||
*/ |
|
||||
private func loadChatHistory() { |
|
||||
// 清空当前历史记录 |
|
||||
historyMessages.removeAll() |
|
||||
|
|
||||
// 使用ChatStorageHelper直接获取最近消息 |
|
||||
guard let recentMessages = chatStorageHelper?.getRecentMessages(sessionId: sessionId, count: 10) else { |
|
||||
print("\(TAG): 没有找到历史记录") |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 将消息添加到历史记录 |
|
||||
for message in recentMessages { |
|
||||
if let sender = message["sender"] as? String, |
|
||||
let content = message["message"] as? String { |
|
||||
|
|
||||
if sender == "user" { |
|
||||
if let userMessage = openAIService?.createUserMessage(text: content) { |
|
||||
addToHistoryMessages(userMessage) |
|
||||
} |
|
||||
} else if sender == "assistant" { |
|
||||
if let assistantMessage = openAIService?.createAssistantMessage(text: content) { |
|
||||
addToHistoryMessages(assistantMessage) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
print("\(TAG): 已加载\(recentMessages.count)条历史记录") |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 添加消息到历史记录,保持最近10条 |
|
||||
*/ |
|
||||
private func addToHistoryMessages(_ message: [String: Any]) { |
|
||||
// 添加新消息 |
|
||||
historyMessages.append(message) |
|
||||
|
|
||||
// 如果超过10条,删除最早的消息 |
|
||||
while historyMessages.count > 10 { |
|
||||
historyMessages.removeFirst() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* TTS播放函数 |
|
||||
* |
|
||||
* - Parameter text: 要播放的文本 |
|
||||
*/ |
|
||||
func speakText(text: String) { |
|
||||
if text.isEmpty { return } |
|
||||
|
|
||||
// 更新状态 |
|
||||
isTtsSpeaking = true |
|
||||
restartIdleCheck() // 状态变化,重启检测 |
|
||||
|
|
||||
// 播放文本 |
|
||||
_ = azureTtsHelper?.speakText(text) |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 停止TTS播放 |
|
||||
*/ |
|
||||
func stopTts() { |
|
||||
if isTtsSpeaking { |
|
||||
_ = azureTtsHelper?.stopSpeaking() |
|
||||
isTtsSpeaking = false |
|
||||
restartIdleCheck() // 状态变化,重启检测 |
|
||||
sendEvent("tts_stopped", ["status": "stopped"]) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 保存聊天消息 |
|
||||
*/ |
|
||||
private func saveChatMessage(userMessage: String, assistantMessage: String) { |
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in |
|
||||
guard let self = self else { return } |
|
||||
|
|
||||
// 保存用户消息 |
|
||||
let userMessageId = self.chatStorageHelper?.saveMessage( |
|
||||
sessionId: self.sessionId, |
|
||||
message: userMessage, |
|
||||
sender: "user" |
|
||||
) ?? -1 |
|
||||
|
|
||||
if userMessageId != -1 { |
|
||||
// 保存AI回复 |
|
||||
let assistantMessageId = self.chatStorageHelper?.saveMessage( |
|
||||
sessionId: self.sessionId, |
|
||||
message: assistantMessage, |
|
||||
sender: "assistant" |
|
||||
) ?? -1 |
|
||||
|
|
||||
if assistantMessageId == -1 { |
|
||||
print("\(self.TAG): 保存助手消息失败") |
|
||||
} |
|
||||
} else { |
|
||||
print("\(self.TAG): 保存用户消息失败") |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 清除聊天历史 |
|
||||
*/ |
|
||||
func clearChatHistory(completion: @escaping (Bool) -> Void) { |
|
||||
DispatchQueue.global(qos: .background).async { [weak self] in |
|
||||
guard let self = self else { |
|
||||
DispatchQueue.main.async { |
|
||||
completion(false) |
|
||||
} |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 清除指定会话的所有消息 |
|
||||
let success = self.chatStorageHelper?.deleteMessages(sessionId: self.sessionId) ?? false |
|
||||
|
|
||||
if success { |
|
||||
// 清空内存中的历史记录 |
|
||||
self.historyMessages.removeAll() |
|
||||
print("\(self.TAG): 聊天历史已清除") |
|
||||
} else { |
|
||||
print("\(self.TAG): 清除聊天历史失败") |
|
||||
} |
|
||||
|
|
||||
DispatchQueue.main.async { |
|
||||
completion(success) |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 发送事件 |
|
||||
*/ |
|
||||
private func sendEvent(_ eventName: String, _ data: [String: Any]) { |
|
||||
// 向所有监听器发送事件 |
|
||||
let allListeners = listeners.allObjects |
|
||||
for case let listener as AgentServiceListener in allListeners { |
|
||||
listener.onEvent(eventName: eventName, data: data) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 释放资源 |
|
||||
*/ |
|
||||
func dispose() { |
|
||||
stopRecognition() |
|
||||
stopTts() |
|
||||
stopAiStream() |
|
||||
stopIdleCheck() |
|
||||
|
|
||||
azureAsrHelper?.dispose() |
|
||||
azureTtsHelper?.dispose() |
|
||||
|
|
||||
// 清除所有监听器 |
|
||||
clearListeners() |
|
||||
|
|
||||
// 重置单例状态,以便下次使用时可以重新初始化 |
|
||||
isInitialized = false |
|
||||
|
|
||||
print("\(TAG): 代理服务资源已释放") |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// MARK: - AzureTtsHelper.TtsCallback |
|
||||
extension AgentService: AzureTtsHelper.TtsCallback { |
|
||||
func onSynthesisStarted() { |
|
||||
isTtsSpeaking = true |
|
||||
// 状态变化,重置空闲检测 |
|
||||
restartIdleCheck() |
|
||||
sendEvent("tts_started", ["status": "started"]) |
|
||||
} |
|
||||
|
|
||||
func onSynthesizing() { |
|
||||
// 可以在此添加TTS合成中的处理逻辑 |
|
||||
} |
|
||||
|
|
||||
func onSynthesisCompleted() { |
|
||||
isTtsSpeaking = false |
|
||||
// 状态变化,重启空闲检测 |
|
||||
restartIdleCheck() |
|
||||
sendEvent("tts_completed", ["status": "completed"]) |
|
||||
} |
|
||||
|
|
||||
func onSynthesisCanceled() { |
|
||||
isTtsSpeaking = false |
|
||||
// 状态变化,重启空闲检测 |
|
||||
restartIdleCheck() |
|
||||
sendEvent("tts_canceled", ["status": "canceled"]) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// MARK: - 外部插件桥接器 |
|
||||
|
|
||||
/// 桥接OpenAIService插件 |
|
||||
class OpenAIServiceBridge { |
|
||||
private let service = OpenAIService() |
|
||||
|
|
||||
/// 初始化服务 |
|
||||
func initialize(apiKey: String, baseUrl: String, model: String, mcpServer: String) { |
|
||||
service.initialize(apiKey: apiKey, baseUrl: baseUrl, model: model, mcpServer: mcpServer) |
|
||||
} |
|
||||
|
|
||||
/// 创建系统消息 |
|
||||
func createSystemMessage(text: String) -> [String: Any] { |
|
||||
return service.createSystemMessage(text) |
|
||||
} |
|
||||
|
|
||||
/// 创建用户消息 |
|
||||
func createUserMessage(text: String) -> [String: Any] { |
|
||||
return service.createUserMessage(text) |
|
||||
} |
|
||||
|
|
||||
/// 创建助手消息 |
|
||||
func createAssistantMessage(text: String) -> [String: Any] { |
|
||||
return service.createAssistantMessage(text) |
|
||||
} |
|
||||
|
|
||||
/// 创建带图片的用户消息 |
|
||||
func createUserMessageWithImage(text: String, imageBase64: String) -> [String: Any]? { |
|
||||
return service.createUserMessageWithImage(text: text, imageBase64: imageBase64) |
|
||||
} |
|
||||
|
|
||||
/// 文件转Base64 |
|
||||
func fileToBase64(filePath: String) -> String? { |
|
||||
return service.fileToBase64(filePath: filePath) |
|
||||
} |
|
||||
|
|
||||
/// 发送流式消息 |
|
||||
func sendMessageStream(messages: [[String: Any]], callback: OpenAIStreamCallback) { |
|
||||
service.sendMessageStream(messages: messages, callback: callback) |
|
||||
} |
|
||||
|
|
||||
/// 取消当前流 |
|
||||
func cancelCurrentStream() { |
|
||||
service.cancelCurrentStream() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// OpenAI流式回调协议 |
|
||||
@objc protocol OpenAIStreamCallback { |
|
||||
func onToken(token: String) |
|
||||
func onComplete() |
|
||||
func onError(error: Error) |
|
||||
func onFunctionCall(call: [String: Any]) |
|
||||
func onFunctionCallResult(functionCall: [String: Any], functionCallResult: [String: Any]) |
|
||||
} |
|
||||
|
|
||||
/// 桥接ChatStorageHelper插件 |
|
||||
class ChatStorageHelperBridge { |
|
||||
private let helper = ChatStorageHelper() |
|
||||
|
|
||||
/// 保存消息 |
|
||||
func saveMessage(sessionId: String, message: String, sender: String) -> Int64 { |
|
||||
return helper.saveMessage(sessionId: sessionId, message: message, sender: sender) |
|
||||
} |
|
||||
|
|
||||
/// 获取最近消息 |
|
||||
func getRecentMessages(sessionId: String, count: Int) -> [[String: Any]]? { |
|
||||
return helper.getRecentMessages(sessionId: sessionId, count: count) |
|
||||
} |
|
||||
|
|
||||
/// 删除消息 |
|
||||
func deleteMessages(sessionId: String) -> Bool { |
|
||||
return helper.deleteMessages(sessionId: sessionId) |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,797 @@ |
|||||
|
import Foundation |
||||
|
import AVFoundation |
||||
|
import ble_service |
||||
|
import speech |
||||
|
|
||||
|
/// 代理服务事件监听器接口 |
||||
|
protocol AgentServiceListener { |
||||
|
/// 当事件发生时调用 |
||||
|
/// - Parameters: |
||||
|
/// - eventName: 事件名称 |
||||
|
/// - data: 事件数据 |
||||
|
func onEvent(eventName: String, data: [String: Any]) |
||||
|
} |
||||
|
|
||||
|
/// 代理服务实现类,负责语音识别、OpenAI对话和语音合成 |
||||
|
class AgentServiceImpl: NSObject { |
||||
|
// MARK: - 常量 |
||||
|
private let TAG = "AgentServiceImpl" |
||||
|
|
||||
|
// MARK: - 单例实现 |
||||
|
static let shared = AgentServiceImpl() |
||||
|
|
||||
|
// MARK: - 属性 |
||||
|
|
||||
|
// 监听器集合 |
||||
|
private var listeners = Set<AgentServiceListener>() |
||||
|
private let listenersLock = NSLock() |
||||
|
|
||||
|
// Azure 语音相关 |
||||
|
private var azureSpeechKey: String = "" |
||||
|
private var azureSpeechRegion: String = "" |
||||
|
private var isRecognizing: Bool = false |
||||
|
private var isSpeaking: Bool = false |
||||
|
|
||||
|
// 语音识别和合成服务 |
||||
|
private var azureAsrHelper: AzureAsrHelper? |
||||
|
private var azureTtsHelper: AzureTtsHelper? |
||||
|
|
||||
|
// OpenAI相关 |
||||
|
private var apiKey: String = "" |
||||
|
private var baseUrl: String = "https://api.openai.com" |
||||
|
private var model: String = "gpt-3.5-turbo" |
||||
|
private var systemPrompt: String = "" |
||||
|
private var chatHistory: [[String: Any]] = [] |
||||
|
|
||||
|
// MCP服务相关 |
||||
|
private var mcpServer: String = "" |
||||
|
|
||||
|
// 状态标志 |
||||
|
private var isInitialized: Bool = false |
||||
|
private var hasSpeechDetected: Bool = false |
||||
|
private var isAiStreaming: Bool = false |
||||
|
|
||||
|
// 空闲检测相关 |
||||
|
private var idleCheckTimer: Timer? |
||||
|
private let maxIdleSeconds: TimeInterval = 10 // 最大空闲秒数 |
||||
|
|
||||
|
// 音频播放器 |
||||
|
private var audioPlayer: AudioPlayer? |
||||
|
|
||||
|
// MARK: - 初始化 |
||||
|
|
||||
|
/// 私有初始化方法 |
||||
|
private override init() { |
||||
|
super.init() |
||||
|
|
||||
|
// 设置音频会话 |
||||
|
setupAudioSession() |
||||
|
|
||||
|
// 初始化音频播放器 |
||||
|
audioPlayer = AudioPlayer() |
||||
|
|
||||
|
// 初始化语音助手服务 |
||||
|
azureAsrHelper = AzureAsrHelper() |
||||
|
azureTtsHelper = AzureTtsHelper() |
||||
|
|
||||
|
// 设置BleService回调 |
||||
|
BleService.shared.setDelegate(self) |
||||
|
} |
||||
|
|
||||
|
// MARK: - 监听器管理 |
||||
|
|
||||
|
/// 添加事件监听器 |
||||
|
/// - Parameter listener: 要添加的监听器 |
||||
|
func addListener(_ listener: AgentServiceListener) { |
||||
|
listenersLock.lock() |
||||
|
listeners.insert(listener) |
||||
|
listenersLock.unlock() |
||||
|
} |
||||
|
|
||||
|
/// 移除事件监听器 |
||||
|
/// - Parameter listener: 要移除的监听器 |
||||
|
func removeListener(_ listener: AgentServiceListener) { |
||||
|
listenersLock.lock() |
||||
|
listeners.remove(listener) |
||||
|
listenersLock.unlock() |
||||
|
} |
||||
|
|
||||
|
/// 清除所有监听器 |
||||
|
func clearListeners() { |
||||
|
listenersLock.lock() |
||||
|
listeners.removeAll() |
||||
|
listenersLock.unlock() |
||||
|
} |
||||
|
|
||||
|
/// 向所有监听器发送事件 |
||||
|
/// - Parameters: |
||||
|
/// - eventName: 事件名称 |
||||
|
/// - data: 事件数据 |
||||
|
private func sendEvent(name eventName: String, data: [String: Any]) { |
||||
|
listenersLock.lock() |
||||
|
let currentListeners = self.listeners |
||||
|
listenersLock.unlock() |
||||
|
|
||||
|
for listener in currentListeners { |
||||
|
listener.onEvent(eventName: eventName, data: data) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 发送错误事件 |
||||
|
/// - Parameter message: 错误信息 |
||||
|
private func sendError(_ message: String, code: String = "ERROR") { |
||||
|
sendEvent(name: "error", data: ["code": code, "message": message]) |
||||
|
} |
||||
|
|
||||
|
// MARK: - 初始化配置 |
||||
|
|
||||
|
/// 初始化系统提示词 |
||||
|
private func initSystemPrompt() { |
||||
|
systemPrompt = """ |
||||
|
你是小言,一个具备专业能力的智能助手,需根据用户场景灵活切换回答模式,确保服务精准高效。 |
||||
|
[系统提示词内容省略...] |
||||
|
""" |
||||
|
} |
||||
|
|
||||
|
/// 初始化配置 |
||||
|
/// - Parameter config: 配置参数 |
||||
|
/// - Returns: 是否初始化成功 |
||||
|
func initialize(config: [String: Any]) -> Bool { |
||||
|
if isInitialized { return true } |
||||
|
|
||||
|
// 设置OpenAI配置 |
||||
|
if let openaiApiKey = config["openaiApiKey"] as? String { |
||||
|
self.apiKey = openaiApiKey |
||||
|
} else { |
||||
|
sendError("OpenAI API密钥缺失", code: "CONFIG_ERROR") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if let openaiBaseUrl = config["openaiBaseUrl"] as? String { |
||||
|
self.baseUrl = openaiBaseUrl |
||||
|
} |
||||
|
|
||||
|
if let openaiModel = config["openaiModel"] as? String { |
||||
|
self.model = openaiModel |
||||
|
} |
||||
|
|
||||
|
// 设置系统提示词 |
||||
|
if let customSystemPrompt = config["systemPrompt"] as? String, !customSystemPrompt.isEmpty { |
||||
|
self.systemPrompt = customSystemPrompt |
||||
|
} else { |
||||
|
initSystemPrompt() |
||||
|
} |
||||
|
|
||||
|
// 设置Azure语音配置 |
||||
|
if let azureSpeechKey = config["azureSpeechKey"] as? String { |
||||
|
self.azureSpeechKey = azureSpeechKey |
||||
|
} else { |
||||
|
sendError("Azure语音密钥缺失", code: "CONFIG_ERROR") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if let azureSpeechRegion = config["azureSpeechRegion"] as? String { |
||||
|
self.azureSpeechRegion = azureSpeechRegion |
||||
|
} else { |
||||
|
sendError("Azure语音区域缺失", code: "CONFIG_ERROR") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 设置MCP服务 |
||||
|
if let mcpServer = config["mcpServer"] as? String { |
||||
|
self.mcpServer = mcpServer |
||||
|
} |
||||
|
|
||||
|
// 初始化Azure语音服务 |
||||
|
let asrInitSuccess = initializeAzureSpeech() |
||||
|
|
||||
|
isInitialized = asrInitSuccess |
||||
|
return asrInitSuccess |
||||
|
} |
||||
|
|
||||
|
// MARK: - 音频会话设置 |
||||
|
|
||||
|
/// 设置音频会话 |
||||
|
private func setupAudioSession() { |
||||
|
let audioSession = AVAudioSession.sharedInstance() |
||||
|
do { |
||||
|
try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth]) |
||||
|
try audioSession.setActive(true, options: .notifyOthersOnDeactivation) |
||||
|
} catch { |
||||
|
sendError("设置音频会话失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - Azure 语音服务 |
||||
|
|
||||
|
/// 初始化Azure语音服务 |
||||
|
private func initializeAzureSpeech() -> Bool { |
||||
|
// 添加事件监听器 |
||||
|
azureAsrHelper?.addListener(self) |
||||
|
azureTtsHelper?.addListener(self) |
||||
|
|
||||
|
// 初始化ASR服务 |
||||
|
guard let asrSuccess = azureAsrHelper?.initialize( |
||||
|
subscriptionKey: azureSpeechKey, |
||||
|
region: azureSpeechRegion, |
||||
|
supportedLanguages: ["zh-CN"] |
||||
|
), asrSuccess else { |
||||
|
sendError("初始化语音识别服务失败", code: "ASR_INIT_ERROR") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 初始化TTS服务 |
||||
|
guard let ttsSuccess = azureTtsHelper?.initialize( |
||||
|
ttsAppId: "", // Azure TTS不需要appId |
||||
|
ttsAppToken: azureSpeechKey, |
||||
|
ttsResource: azureSpeechRegion, |
||||
|
language: "zh-CN" |
||||
|
), ttsSuccess else { |
||||
|
sendError("初始化语音合成服务失败", code: "TTS_INIT_ERROR") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 设置TTS语音 |
||||
|
_ = azureTtsHelper?.setVoice("zh-CN-XiaoxiaoNeural") |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// MARK: - 空闲检测 |
||||
|
|
||||
|
/// 启动空闲检测 |
||||
|
private func startIdleCheck() { |
||||
|
stopIdleCheck() // 先停止现有的检查 |
||||
|
|
||||
|
if !isRecognizing { return } |
||||
|
|
||||
|
// 创建一个新的定时器 |
||||
|
idleCheckTimer = Timer.scheduledTimer(withTimeInterval: maxIdleSeconds, repeats: false) { [weak self] _ in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
if self.isRecognizing && !self.hasSpeechDetected && !self.isSpeaking && !self.isAiStreaming { |
||||
|
self.stopRecognition() |
||||
|
self.sendEvent(name: "auto_stop", data: [ |
||||
|
"reason": "idle_timeout", |
||||
|
"seconds": self.maxIdleSeconds |
||||
|
]) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 停止空闲检测 |
||||
|
private func stopIdleCheck() { |
||||
|
idleCheckTimer?.invalidate() |
||||
|
idleCheckTimer = nil |
||||
|
} |
||||
|
|
||||
|
/// 重启空闲检测 |
||||
|
private func restartIdleCheck() { |
||||
|
if isRecognizing { |
||||
|
startIdleCheck() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 语音识别 |
||||
|
|
||||
|
/// 开始语音识别 |
||||
|
/// - Returns: 是否成功启动识别 |
||||
|
func startRecognition() -> Bool { |
||||
|
if !isInitialized { |
||||
|
sendError("服务未初始化", code: "NOT_INITIALIZED") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 防止重复启动 |
||||
|
if isRecognizing { |
||||
|
stopRecognition() |
||||
|
} |
||||
|
|
||||
|
// 启动语音识别 |
||||
|
guard let success = azureAsrHelper?.startContinuousRecognition(), success else { |
||||
|
sendError("启动语音识别失败", code: "RECOGNITION_START_ERROR") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/// 停止语音识别 |
||||
|
/// - Returns: 是否成功停止识别 |
||||
|
func stopRecognition() -> Bool { |
||||
|
if !isRecognizing { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// 停止语音识别 |
||||
|
guard let success = azureAsrHelper?.stopContinuousRecognition(), success else { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/// 推送音频数据 |
||||
|
/// - Parameter audioData: 音频数据 |
||||
|
/// - Returns: 是否成功推送 |
||||
|
func pushAudioData(_ audioData: Data) -> Bool { |
||||
|
if !isRecognizing || !isInitialized { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 将音频数据推送到语音识别引擎 |
||||
|
return azureAsrHelper?.pushAudioData(audioData: audioData) ?? false |
||||
|
} |
||||
|
|
||||
|
// MARK: - 语音合成 |
||||
|
|
||||
|
/// 语音合成播放文本 |
||||
|
/// - Parameter text: 要播放的文本 |
||||
|
/// - Returns: 是否成功开始播放 |
||||
|
func speakText(_ text: String) -> Bool { |
||||
|
if !isInitialized { |
||||
|
sendError("服务未初始化", code: "NOT_INITIALIZED") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if text.isEmpty { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 使用TTS服务进行语音合成 |
||||
|
return azureTtsHelper?.speakOnce(text) ?? false |
||||
|
} |
||||
|
|
||||
|
/// 停止语音合成 |
||||
|
/// - Returns: 是否成功停止合成 |
||||
|
func stopTts() -> Bool { |
||||
|
if !isSpeaking { |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// 停止TTS服务 |
||||
|
return azureTtsHelper?.stop() ?? false |
||||
|
} |
||||
|
|
||||
|
/// 打断当前响应 |
||||
|
/// - Returns: 是否成功打断 |
||||
|
func interruptCurrentResponse() -> Bool { |
||||
|
var interrupted = false |
||||
|
|
||||
|
// 停止TTS播放 |
||||
|
if isSpeaking { |
||||
|
interrupted = stopTts() || interrupted |
||||
|
} |
||||
|
|
||||
|
// 停止AI流输出 |
||||
|
if isAiStreaming { |
||||
|
isAiStreaming = false |
||||
|
interrupted = true |
||||
|
} |
||||
|
|
||||
|
// 发送打断事件 |
||||
|
if interrupted { |
||||
|
sendEvent(name: "response_interrupted", data: ["status": "interrupted"]) |
||||
|
} |
||||
|
|
||||
|
return interrupted |
||||
|
} |
||||
|
|
||||
|
// MARK: - 文本处理 |
||||
|
|
||||
|
/// 处理文本输入 |
||||
|
/// - Parameters: |
||||
|
/// - text: 文本内容 |
||||
|
/// - speakResponse: 是否朗读响应 |
||||
|
/// - Returns: 是否成功处理 |
||||
|
func processTextInput(_ text: String, speakResponse: Bool) -> Bool { |
||||
|
if !isInitialized { |
||||
|
sendError("服务未初始化", code: "NOT_INITIALIZED") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if text.isEmpty { |
||||
|
sendError("文本输入不能为空", code: "EMPTY_TEXT") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// TODO: 实现与OpenAI的通信 |
||||
|
// 这里只是模拟处理 |
||||
|
|
||||
|
// 将用户消息添加到聊天历史 |
||||
|
let userMessage: [String: Any] = [ |
||||
|
"role": "user", |
||||
|
"content": text |
||||
|
] |
||||
|
chatHistory.append(userMessage) |
||||
|
|
||||
|
// 更新状态 |
||||
|
isAiStreaming = true |
||||
|
|
||||
|
// 模拟助手响应 |
||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
// 模拟流式输出 |
||||
|
let response = "这是对"\(text)"的回复。我是AI助手,很高兴为您服务。" |
||||
|
|
||||
|
// 逐词输出模拟 |
||||
|
let words = response.split(separator: " ") |
||||
|
for (index, word) in words.enumerated() { |
||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) { [weak self] in |
||||
|
guard let self = self, self.isAiStreaming else { return } |
||||
|
self.sendEvent(name: "assistant_token", data: ["token": String(word) + " "]) |
||||
|
|
||||
|
// 如果需要语音播报,则合成语音 |
||||
|
if speakResponse { |
||||
|
self.azureTtsHelper?.speakStream(String(word) + " ") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 完整响应 |
||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + Double(words.count) * 0.1 + 0.2) { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
// 结束AI流式输出 |
||||
|
self.isAiStreaming = false |
||||
|
|
||||
|
// 发送完整响应 |
||||
|
self.sendEvent(name: "assistant_response", data: ["text": response]) |
||||
|
|
||||
|
// 将助手消息添加到聊天历史 |
||||
|
let assistantMessage: [String: Any] = [ |
||||
|
"role": "assistant", |
||||
|
"content": response |
||||
|
] |
||||
|
self.chatHistory.append(assistantMessage) |
||||
|
|
||||
|
// 如果需要语音播报,则完成语音流 |
||||
|
if speakResponse { |
||||
|
self.azureTtsHelper?.flushStream() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// MARK: - 图片处理 |
||||
|
|
||||
|
/// 处理图片输入 |
||||
|
/// - Parameters: |
||||
|
/// - imagePath: 图片路径 |
||||
|
/// - text: 文本描述 |
||||
|
/// - speakResponse: 是否朗读响应 |
||||
|
/// - Returns: 是否成功处理 |
||||
|
func processImageInput(imagePath: String, text: String, speakResponse: Bool) -> Bool { |
||||
|
if !isInitialized { |
||||
|
sendError("服务未初始化", code: "NOT_INITIALIZED") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
if imagePath.isEmpty { |
||||
|
sendError("图片路径不能为空", code: "EMPTY_IMAGE_PATH") |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 发送图片处理中事件 |
||||
|
sendEvent(name: "image_processing", data: [:]) |
||||
|
|
||||
|
// TODO: 实现与OpenAI Vision API的通信 |
||||
|
// 这里只是模拟处理 |
||||
|
|
||||
|
// 模拟图片处理 |
||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
// 图片就绪事件 |
||||
|
self.sendEvent(name: "image_ready", data: [:]) |
||||
|
|
||||
|
// 更新状态 |
||||
|
self.isAiStreaming = true |
||||
|
|
||||
|
// 模拟AI分析结果 |
||||
|
let imageDescription = "这是一张图片分析结果。" |
||||
|
let additionalText = text.isEmpty ? "" : "关于您提问的"\(text)"," |
||||
|
let response = "\(imageDescription)\(additionalText)我可以看到这是一张图片。" |
||||
|
|
||||
|
// 发送AI助手token流 |
||||
|
let words = response.split(separator: " ") |
||||
|
for (index, word) in words.enumerated() { |
||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + Double(index) * 0.1) { [weak self] in |
||||
|
guard let self = self, self.isAiStreaming else { return } |
||||
|
|
||||
|
let token = String(word) + " " |
||||
|
self.sendEvent(name: "assistant_token", data: ["token": token]) |
||||
|
|
||||
|
// 如果需要语音播报,则合成语音 |
||||
|
if speakResponse { |
||||
|
self.azureTtsHelper?.speakStream(token) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 发送完整响应 |
||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + Double(words.count) * 0.1 + 0.2) { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
// 结束AI流式输出 |
||||
|
self.isAiStreaming = false |
||||
|
|
||||
|
self.sendEvent(name: "assistant_response", data: [ |
||||
|
"text": response, |
||||
|
"userInput": text.isEmpty ? "[图片]" : text, |
||||
|
"hasImage": true |
||||
|
]) |
||||
|
|
||||
|
// 如果需要语音播报,则完成语音 |
||||
|
if speakResponse { |
||||
|
self.azureTtsHelper?.flushStream() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// MARK: - 聊天历史管理 |
||||
|
|
||||
|
/// 清除聊天历史 |
||||
|
/// - Returns: 是否成功清除 |
||||
|
func clearChatHistory() -> Bool { |
||||
|
chatHistory.removeAll() |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// MARK: - 资源释放 |
||||
|
|
||||
|
/// 释放资源 |
||||
|
/// - Returns: 是否成功释放 |
||||
|
func dispose() -> Bool { |
||||
|
// 停止语音识别 |
||||
|
if isRecognizing { |
||||
|
stopRecognition() |
||||
|
} |
||||
|
|
||||
|
// 停止语音合成 |
||||
|
if isSpeaking { |
||||
|
stopTts() |
||||
|
} |
||||
|
|
||||
|
// 停止空闲检测 |
||||
|
stopIdleCheck() |
||||
|
|
||||
|
// 移除BleService代理 |
||||
|
BleService.shared.setDelegate(nil) |
||||
|
|
||||
|
// 释放Azure Speech资源 |
||||
|
azureAsrHelper?.removeListener(self) |
||||
|
azureTtsHelper?.removeListener(self) |
||||
|
azureAsrHelper?.dispose() |
||||
|
azureTtsHelper?.dispose() |
||||
|
|
||||
|
// 释放音频播放器 |
||||
|
audioPlayer = nil |
||||
|
|
||||
|
// 清除监听器 |
||||
|
clearListeners() |
||||
|
|
||||
|
isInitialized = false |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - AsrEventListener 实现 |
||||
|
extension AgentServiceImpl: AsrEventListener { |
||||
|
func onEvent(_ event: AsrEvent) { |
||||
|
switch event.type { |
||||
|
case .sessionStarted: |
||||
|
sendEvent(name: "recognition_started", data: [:]) |
||||
|
isRecognizing = true |
||||
|
startIdleCheck() |
||||
|
audioPlayer?.playStartSound() |
||||
|
|
||||
|
case .sessionStopped: |
||||
|
sendEvent(name: "recognition_stopped", data: [:]) |
||||
|
isRecognizing = false |
||||
|
stopIdleCheck() |
||||
|
audioPlayer?.playStopSound() |
||||
|
|
||||
|
case .recognizing: |
||||
|
if let text = event.params["text"] as? String { |
||||
|
// 检测到语音,更新状态 |
||||
|
let previousHasSpeech = hasSpeechDetected |
||||
|
hasSpeechDetected = true |
||||
|
|
||||
|
if !previousHasSpeech { |
||||
|
restartIdleCheck() |
||||
|
} |
||||
|
|
||||
|
var data: [String: Any] = ["text": text] |
||||
|
if let language = event.params["language"] as? String, !language.isEmpty { |
||||
|
data["language"] = language |
||||
|
} |
||||
|
|
||||
|
sendEvent(name: "recognizing", data: data) |
||||
|
|
||||
|
// 如果TTS正在播放或AI正在生成,则触发打断 |
||||
|
if isSpeaking || isAiStreaming { |
||||
|
interruptCurrentResponse() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
case .recognized: |
||||
|
if let text = event.params["text"] as? String, !text.isEmpty { |
||||
|
var data: [String: Any] = ["text": text] |
||||
|
if let language = event.params["language"] as? String, !language.isEmpty { |
||||
|
data["language"] = language |
||||
|
} |
||||
|
|
||||
|
sendEvent(name: "recognition_result", data: data) |
||||
|
|
||||
|
// 处理识别结果,发送到OpenAI |
||||
|
processTextInput(text, speakResponse: true) |
||||
|
} |
||||
|
|
||||
|
// 重置状态,继续识别 |
||||
|
let previousHasSpeech = hasSpeechDetected |
||||
|
hasSpeechDetected = false |
||||
|
|
||||
|
if previousHasSpeech { |
||||
|
restartIdleCheck() |
||||
|
} |
||||
|
|
||||
|
case .canceled: |
||||
|
var data: [String: Any] = [:] |
||||
|
if let reason = event.params["reason"] as? String { |
||||
|
data["reason"] = reason |
||||
|
} |
||||
|
if let errorDetails = event.params["errorDetails"] as? String { |
||||
|
data["details"] = errorDetails |
||||
|
} |
||||
|
|
||||
|
sendEvent(name: "recognition_canceled", data: data) |
||||
|
isRecognizing = false |
||||
|
stopIdleCheck() |
||||
|
|
||||
|
case .error: |
||||
|
var data: [String: Any] = [:] |
||||
|
if let errorCode = event.params["errorCode"] as? String { |
||||
|
data["errorCode"] = errorCode |
||||
|
} |
||||
|
if let message = event.params["message"] as? String { |
||||
|
data["message"] = message |
||||
|
} else if let error = event.params["error"] as? String { |
||||
|
data["message"] = error |
||||
|
} else { |
||||
|
data["message"] = "未知错误" |
||||
|
} |
||||
|
|
||||
|
sendEvent(name: "recognition_error", data: data) |
||||
|
isRecognizing = false |
||||
|
stopIdleCheck() |
||||
|
|
||||
|
default: |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - TtsEventListener 实现 |
||||
|
extension AgentServiceImpl: TtsEventListener { |
||||
|
func onEvent(_ event: TtsEvent) { |
||||
|
switch event.type { |
||||
|
case .synthesisStarted: |
||||
|
isSpeaking = true |
||||
|
restartIdleCheck() |
||||
|
sendEvent(name: "tts_started", data: [:]) |
||||
|
|
||||
|
case .synthesisCompleted: |
||||
|
isSpeaking = false |
||||
|
restartIdleCheck() |
||||
|
sendEvent(name: "tts_completed", data: [:]) |
||||
|
|
||||
|
case .synthesisCanceled: |
||||
|
isSpeaking = false |
||||
|
restartIdleCheck() |
||||
|
|
||||
|
var data: [String: Any] = [:] |
||||
|
if let reason = event.params["reason"] as? String { |
||||
|
data["reason"] = reason |
||||
|
} |
||||
|
if let errorDetails = event.params["errorDetails"] as? String { |
||||
|
data["details"] = errorDetails |
||||
|
} |
||||
|
|
||||
|
sendEvent(name: "tts_canceled", data: data) |
||||
|
|
||||
|
case .error: |
||||
|
isSpeaking = false |
||||
|
restartIdleCheck() |
||||
|
|
||||
|
var data: [String: Any] = [:] |
||||
|
if let errorCode = event.params["errorCode"] as? String { |
||||
|
data["errorCode"] = errorCode |
||||
|
} |
||||
|
if let errorMessage = event.params["errorMessage"] as? String { |
||||
|
data["message"] = errorMessage |
||||
|
} else { |
||||
|
data["message"] = "TTS错误" |
||||
|
} |
||||
|
|
||||
|
sendEvent(name: "error", data: data) |
||||
|
|
||||
|
default: |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - BleService.Callback 实现 |
||||
|
extension AgentServiceImpl: BleService.Callback { |
||||
|
func onScanResult(devices: [[String: Any]]) { |
||||
|
// 不处理扫描结果 |
||||
|
} |
||||
|
|
||||
|
func onConnectionStateChanged(state: Int) { |
||||
|
// 可以在这里处理连接状态变化,例如通知UI更新 |
||||
|
} |
||||
|
|
||||
|
func onAudioDataReceived(data: Data) { |
||||
|
// 直接将音频数据推送到Azure Speech进行识别 |
||||
|
pushAudioData(data) |
||||
|
} |
||||
|
|
||||
|
func onWakeupSignalReceived() { |
||||
|
NSLog("\(TAG): 收到唤醒信号") |
||||
|
|
||||
|
// 停止当前TTS,避免冲突 |
||||
|
stopTts() |
||||
|
|
||||
|
// 如果已经在识别,则重新启动 |
||||
|
stopRecognition() |
||||
|
startRecognition() |
||||
|
} |
||||
|
|
||||
|
func onDeviceInfoReceived(infoType: Int, infoData: [String: Any]) { |
||||
|
// 可以处理设备信息,例如电量、设备名称等 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 音频播放器 |
||||
|
|
||||
|
/// 简单的音频播放器,用于播放提示音 |
||||
|
class AudioPlayer { |
||||
|
private var player: AVAudioPlayer? |
||||
|
|
||||
|
/// 播放开始识别提示音 |
||||
|
func playStartSound() { |
||||
|
playSound(named: "start") |
||||
|
} |
||||
|
|
||||
|
/// 播放停止识别提示音 |
||||
|
func playStopSound() { |
||||
|
playSound(named: "stop") |
||||
|
} |
||||
|
|
||||
|
/// 播放音频文件 |
||||
|
/// - Parameter named: 音频文件名 |
||||
|
private func playSound(named: String) { |
||||
|
guard let path = Bundle.main.path(forResource: named, ofType: "wav") else { |
||||
|
print("找不到音频文件: \(named).wav") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
let url = URL(fileURLWithPath: path) |
||||
|
|
||||
|
do { |
||||
|
player = try AVAudioPlayer(contentsOf: url) |
||||
|
player?.prepareToPlay() |
||||
|
player?.play() |
||||
|
} catch { |
||||
|
print("播放音频失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,11 +0,0 @@ |
|||||
import Foundation |
|
||||
|
|
||||
/// 代理服务事件监听器协议 |
|
||||
/// 用于接收代理服务发送的各类事件 |
|
||||
protocol AgentServiceListener: AnyObject { |
|
||||
/// 事件回调 |
|
||||
/// - Parameters: |
|
||||
/// - eventName: 事件名称 |
|
||||
/// - data: 事件数据 |
|
||||
func onEvent(eventName: String, data: [String: Any]) |
|
||||
} |
|
||||
@ -0,0 +1,227 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
import AVFoundation |
||||
|
|
||||
|
/// 代理服务插件,负责连接Flutter端与原生服务 |
||||
|
public class AgentServicePlugin: NSObject, FlutterPlugin { |
||||
|
private let TAG = "AgentServicePlugin" |
||||
|
private var eventSink: FlutterEventSink? |
||||
|
|
||||
|
// MARK: - 插件注册 |
||||
|
|
||||
|
/// 插件注册方法 |
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger()) |
||||
|
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger()) |
||||
|
|
||||
|
let instance = AgentServicePlugin() |
||||
|
|
||||
|
registrar.addMethodCallDelegate(instance, channel: methodChannel) |
||||
|
eventChannel.setStreamHandler(instance) |
||||
|
|
||||
|
// 添加监听器 |
||||
|
AgentServiceImpl.shared.addListener(instance) |
||||
|
|
||||
|
NSLog("\(instance.TAG): 插件已加载") |
||||
|
} |
||||
|
|
||||
|
// MARK: - 方法处理 |
||||
|
|
||||
|
/// 处理来自Flutter的方法调用 |
||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
NSLog("\(TAG): 收到方法调用 - \(call.method)") |
||||
|
|
||||
|
switch call.method { |
||||
|
case "initialize": |
||||
|
handleInitialize(call, result: result) |
||||
|
case "startAgentService": |
||||
|
handleStartAgentService(call, result: result) |
||||
|
case "stopAgentService": |
||||
|
handleStopAgentService(result: result) |
||||
|
case "startConversation": |
||||
|
handleStartConversation(result: result) |
||||
|
case "stopConversation": |
||||
|
handleStopConversation(result: result) |
||||
|
case "processTextInput": |
||||
|
handleProcessTextInput(call, result: result) |
||||
|
case "speakText": |
||||
|
handleSpeakText(call, result: result) |
||||
|
case "stopTts": |
||||
|
handleStopTts(result: result) |
||||
|
case "clearChatHistory": |
||||
|
handleClearChatHistory(result: result) |
||||
|
case "interruptCurrentResponse": |
||||
|
handleInterruptCurrentResponse(result: result) |
||||
|
case "processImageInput": |
||||
|
handleProcessImageInput(call, result: result) |
||||
|
case "dispose": |
||||
|
handleDispose(result: result) |
||||
|
default: |
||||
|
result(FlutterMethodNotImplemented) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 方法实现 |
||||
|
|
||||
|
private func handleInitialize(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let config = args["config"] as? [String: Any] else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数无效", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 初始化AgentServiceImpl |
||||
|
let success = AgentServiceImpl.shared.initialize(config: config) |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
private func handleStartAgentService(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let config = args["config"] as? [String: Any] else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "参数无效", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
NSLog("\(TAG): 启动AgentService,配置参数: \(config)") |
||||
|
|
||||
|
// 初始化AgentServiceImpl |
||||
|
let success = AgentServiceImpl.shared.initialize(config: config) |
||||
|
|
||||
|
if success { |
||||
|
result(true) |
||||
|
} else { |
||||
|
result(FlutterError(code: "SERVICE_START_ERROR", message: "AgentService启动失败", details: nil)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func handleStopAgentService(result: @escaping FlutterResult) { |
||||
|
NSLog("\(TAG): 停止AgentService") |
||||
|
let success = AgentServiceImpl.shared.dispose() |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
private func handleStartConversation(result: @escaping FlutterResult) { |
||||
|
NSLog("\(TAG): 开始对话") |
||||
|
let success = AgentServiceImpl.shared.startRecognition() |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
private func handleStopConversation(result: @escaping FlutterResult) { |
||||
|
NSLog("\(TAG): 停止对话") |
||||
|
let success = AgentServiceImpl.shared.stopRecognition() |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
private func handleProcessTextInput(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let text = args["text"] as? String else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
let speakResponse = args["speakResponse"] as? Bool ?? false |
||||
|
|
||||
|
NSLog("\(TAG): 处理文本输入 - \(text)") |
||||
|
let success = AgentServiceImpl.shared.processTextInput(text, speakResponse: speakResponse) |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
private func handleSpeakText(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let text = args["text"] as? String else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "文本不能为空", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
NSLog("\(TAG): 语音合成 - \(text)") |
||||
|
let success = AgentServiceImpl.shared.speakText(text) |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
private func handleStopTts(result: @escaping FlutterResult) { |
||||
|
NSLog("\(TAG): 停止语音合成") |
||||
|
let success = AgentServiceImpl.shared.stopTts() |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
private func handleClearChatHistory(result: @escaping FlutterResult) { |
||||
|
NSLog("\(TAG): 清除聊天历史") |
||||
|
let success = AgentServiceImpl.shared.clearChatHistory() |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
private func handleInterruptCurrentResponse(result: @escaping FlutterResult) { |
||||
|
NSLog("\(TAG): 中断当前响应") |
||||
|
let success = AgentServiceImpl.shared.interruptCurrentResponse() |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
private func handleProcessImageInput(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
guard let args = call.arguments as? [String: Any], |
||||
|
let imagePath = args["imagePath"] as? String else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENTS", message: "图片路径不能为空", details: nil)) |
||||
|
return |
||||
|
} |
||||
|
let text = args["text"] as? String ?? "" |
||||
|
let speakResponse = args["speakResponse"] as? Bool ?? false |
||||
|
|
||||
|
NSLog("\(TAG): 处理图片输入 - \(imagePath)") |
||||
|
let success = AgentServiceImpl.shared.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse) |
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
private func handleDispose(result: @escaping FlutterResult) { |
||||
|
NSLog("\(TAG): 释放资源") |
||||
|
|
||||
|
// 移除监听器并释放资源 |
||||
|
AgentServiceImpl.shared.removeListener(self) |
||||
|
let success = AgentServiceImpl.shared.dispose() |
||||
|
|
||||
|
result(success) |
||||
|
} |
||||
|
|
||||
|
// MARK: - 实例析构 |
||||
|
|
||||
|
deinit { |
||||
|
// 确保在实例销毁时移除监听器 |
||||
|
AgentServiceImpl.shared.removeListener(self) |
||||
|
NSLog("\(TAG): 插件已卸载") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - AgentServiceListener 实现 |
||||
|
extension AgentServicePlugin: AgentServiceListener { |
||||
|
public func onEvent(eventName: String, data: [String: Any]) { |
||||
|
guard let eventSink = eventSink else { return } |
||||
|
|
||||
|
// 将事件转换为JSON字符串 |
||||
|
let eventData: [String: Any] = [ |
||||
|
"event": eventName, |
||||
|
"data": data |
||||
|
] |
||||
|
|
||||
|
do { |
||||
|
let jsonData = try JSONSerialization.data(withJSONObject: eventData, options: []) |
||||
|
if let jsonString = String(data: jsonData, encoding: .utf8) { |
||||
|
// 确保在主线程发送事件到Flutter |
||||
|
DispatchQueue.main.async { |
||||
|
eventSink(jsonString) |
||||
|
} |
||||
|
} |
||||
|
} catch { |
||||
|
NSLog("\(TAG): 错误 - 事件数据序列化失败: \(error.localizedDescription)") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - FlutterStreamHandler 实现 |
||||
|
extension AgentServicePlugin: 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 |
||||
|
} |
||||
|
} |
||||
@ -1,259 +0,0 @@ |
|||||
import Flutter |
|
||||
import UIKit |
|
||||
import open_ai_service |
|
||||
import chat_storage |
|
||||
|
|
||||
/// Swift版Agent Service插件 |
|
||||
/// 处理Flutter与原生层的通信 |
|
||||
public class SwiftAgentServicePlugin: NSObject, FlutterPlugin { |
|
||||
// Flutter方法通道 |
|
||||
private var methodChannel: FlutterMethodChannel? |
|
||||
|
|
||||
// Flutter事件通道 |
|
||||
private var eventChannel: FlutterEventChannel? |
|
||||
|
|
||||
// 事件sink,用于向Flutter发送事件 |
|
||||
private var eventSink: FlutterEventSink? |
|
||||
|
|
||||
// 核心服务实例 |
|
||||
private var agentService: AgentService? |
|
||||
|
|
||||
/// 注册插件 |
|
||||
public static func register(with registrar: FlutterPluginRegistrar) { |
|
||||
let instance = SwiftAgentServicePlugin() |
|
||||
|
|
||||
// 初始化方法通道 |
|
||||
let methodChannel = FlutterMethodChannel(name: "com.yunqiinnovation.agent_service", binaryMessenger: registrar.messenger()) |
|
||||
registrar.addMethodCallDelegate(instance, channel: methodChannel) |
|
||||
instance.methodChannel = methodChannel |
|
||||
|
|
||||
// 初始化事件通道 |
|
||||
let eventChannel = FlutterEventChannel(name: "com.yunqiinnovation.agent_service/events", binaryMessenger: registrar.messenger()) |
|
||||
eventChannel.setStreamHandler(instance) |
|
||||
instance.eventChannel = eventChannel |
|
||||
} |
|
||||
|
|
||||
/// 处理方法调用 |
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
|
||||
switch call.method { |
|
||||
case "initialize": |
|
||||
handleInitialize(call, result) |
|
||||
|
|
||||
case "startConversation": |
|
||||
handleStartConversation(result) |
|
||||
|
|
||||
case "stopConversation": |
|
||||
handleStopConversation(result) |
|
||||
|
|
||||
case "processTextInput": |
|
||||
handleProcessTextInput(call, result) |
|
||||
|
|
||||
case "speakText": |
|
||||
handleSpeakText(call, result) |
|
||||
|
|
||||
case "stopTts": |
|
||||
handleStopTts(result) |
|
||||
|
|
||||
case "clearChatHistory": |
|
||||
handleClearChatHistory(result) |
|
||||
|
|
||||
case "interruptCurrentResponse": |
|
||||
handleInterruptCurrentResponse(result) |
|
||||
|
|
||||
case "processImageInput": |
|
||||
handleProcessImageInput(call, result) |
|
||||
|
|
||||
case "dispose": |
|
||||
handleDispose(result) |
|
||||
|
|
||||
default: |
|
||||
result(FlutterMethodNotImplemented) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// MARK: - 方法处理 |
|
||||
|
|
||||
/// 初始化 |
|
||||
private func handleInitialize(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|
||||
guard let args = call.arguments as? [String: Any], |
|
||||
let config = args["config"] as? [String: Any] else { |
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "缺少配置参数", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
// 初始化代理服务 |
|
||||
if agentService == nil { |
|
||||
agentService = AgentService() |
|
||||
|
|
||||
// 设置事件监听器 |
|
||||
agentService?.setListener(self) |
|
||||
} |
|
||||
|
|
||||
// 执行初始化 |
|
||||
let success = agentService?.initialize(config: config) ?? false |
|
||||
result(success) |
|
||||
} |
|
||||
|
|
||||
/// 开始对话 |
|
||||
private func handleStartConversation(_ result: @escaping FlutterResult) { |
|
||||
guard let service = agentService else { |
|
||||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
let success = service.startRecognition() |
|
||||
result(success) |
|
||||
} |
|
||||
|
|
||||
/// 停止对话 |
|
||||
private func handleStopConversation(_ result: @escaping FlutterResult) { |
|
||||
guard let service = agentService else { |
|
||||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
service.stopRecognition() |
|
||||
result(true) |
|
||||
} |
|
||||
|
|
||||
/// 处理文本输入 |
|
||||
private func handleProcessTextInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|
||||
guard let service = agentService else { |
|
||||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
guard let args = call.arguments as? [String: Any], |
|
||||
let text = args["text"] as? String else { |
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
let speakResponse = (args["speakResponse"] as? Bool) ?? false |
|
||||
let success = service.processTextInput(text: text, speakResponse: speakResponse) |
|
||||
result(success) |
|
||||
} |
|
||||
|
|
||||
/// 朗读文本 |
|
||||
private func handleSpeakText(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|
||||
guard let service = agentService else { |
|
||||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
guard let args = call.arguments as? [String: Any], |
|
||||
let text = args["text"] as? String else { |
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的文本参数", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
service.speakText(text: text) |
|
||||
result(true) |
|
||||
} |
|
||||
|
|
||||
/// 停止语音合成 |
|
||||
private func handleStopTts(_ result: @escaping FlutterResult) { |
|
||||
guard let service = agentService else { |
|
||||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
service.stopTts() |
|
||||
result(true) |
|
||||
} |
|
||||
|
|
||||
/// 清除聊天历史 |
|
||||
private func handleClearChatHistory(_ result: @escaping FlutterResult) { |
|
||||
guard let service = agentService else { |
|
||||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
service.clearChatHistory { success in |
|
||||
result(success) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// 中断当前响应 |
|
||||
private func handleInterruptCurrentResponse(_ result: @escaping FlutterResult) { |
|
||||
guard let service = agentService else { |
|
||||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
service.interruptCurrentResponse() |
|
||||
result(true) |
|
||||
} |
|
||||
|
|
||||
/// 处理图片输入 |
|
||||
private func handleProcessImageInput(_ call: FlutterMethodCall, _ result: @escaping FlutterResult) { |
|
||||
guard let service = agentService else { |
|
||||
result(FlutterError(code: "NOT_INITIALIZED", message: "AgentService尚未初始化", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
guard let args = call.arguments as? [String: Any], |
|
||||
let imagePath = args["imagePath"] as? String else { |
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "无效的图片路径参数", details: nil)) |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
let text = (args["text"] as? String) ?? "" |
|
||||
let speakResponse = (args["speakResponse"] as? Bool) ?? false |
|
||||
|
|
||||
let success = service.processImageInput(imagePath: imagePath, text: text, speakResponse: speakResponse) |
|
||||
result(success) |
|
||||
} |
|
||||
|
|
||||
/// 释放资源 |
|
||||
private func handleDispose(_ result: @escaping FlutterResult) { |
|
||||
guard let service = agentService else { |
|
||||
result(true) // 已经是nil,直接返回成功 |
|
||||
return |
|
||||
} |
|
||||
|
|
||||
service.dispose() |
|
||||
agentService = nil |
|
||||
|
|
||||
result(true) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// MARK: - FlutterStreamHandler |
|
||||
extension SwiftAgentServicePlugin: 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: - AgentServiceListener |
|
||||
extension SwiftAgentServicePlugin: AgentServiceListener { |
|
||||
func onEvent(eventName: String, data: [String: Any]) { |
|
||||
guard let sink = eventSink else { return } |
|
||||
|
|
||||
do { |
|
||||
// 构建事件JSON |
|
||||
let eventMap: [String: Any] = [ |
|
||||
"event": eventName, |
|
||||
"data": data |
|
||||
] |
|
||||
|
|
||||
// 将事件转换为JSON字符串 |
|
||||
let jsonData = try JSONSerialization.data(withJSONObject: eventMap, options: []) |
|
||||
if let jsonString = String(data: jsonData, encoding: .utf8) { |
|
||||
// 发送到Flutter |
|
||||
DispatchQueue.main.async { |
|
||||
sink(jsonString) |
|
||||
} |
|
||||
} |
|
||||
} catch { |
|
||||
print("事件序列化失败: \(error.localizedDescription)") |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,25 @@ |
|||||
|
# |
||||
|
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. |
||||
|
# Run `pod lib lint agent_service.podspec` to validate before publishing. |
||||
|
# |
||||
|
Pod::Spec.new do |s| |
||||
|
s.name = 'agent_service' |
||||
|
s.version = '0.0.1' |
||||
|
s.summary = 'A Flutter plugin for agent service with voice interaction capabilities.' |
||||
|
s.description = <<-DESC |
||||
|
A Flutter plugin that provides voice interaction, AI conversation and text-to-speech functionalities. |
||||
|
DESC |
||||
|
s.homepage = 'http://example.com' |
||||
|
s.license = { :file => '../LICENSE' } |
||||
|
s.author = { 'Your Company' => 'email@example.com' } |
||||
|
s.source = { :path => '.' } |
||||
|
s.source_files = 'Classes/**/*' |
||||
|
s.dependency 'Flutter' |
||||
|
s.dependency 'azure_speech' |
||||
|
s.dependency 'ble_service' |
||||
|
s.platform = :ios, '11.0' |
||||
|
|
||||
|
# Flutter.framework does not contain a i386 slice. |
||||
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } |
||||
|
s.swift_version = '5.0' |
||||
|
end |
||||
@ -1,4 +1,17 @@ |
|||||
|
//
|
||||
|
// AzureSpeechPlugin.h
|
||||
|
// azure_speech
|
||||
|
//
|
||||
|
// Created for azure_speech plugin compatibility.
|
||||
|
//
|
||||
|
|
||||
#import <Flutter/Flutter.h> |
#import <Flutter/Flutter.h> |
||||
|
|
||||
@interface AzureSpeechPlugin : NSObject<FlutterPlugin> |
/**
|
||||
|
* Azure Speech Plugin 头文件 |
||||
|
* |
||||
|
* 注:实际实现使用 Swift,此头文件仅用于 Flutter 框架兼容 |
||||
|
*/ |
||||
|
@interface AzureSpeechPlugin : NSObject <FlutterPlugin> |
||||
|
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar; |
||||
@end |
@end |
||||
@ -1,12 +0,0 @@ |
|||||
#import "AzureSpeechPlugin.h" |
|
||||
#if __has_include(<azure_speech/azure_speech-Swift.h>) |
|
||||
#import <azure_speech/azure_speech-Swift.h> |
|
||||
#else |
|
||||
#import "azure_speech-Swift.h" |
|
||||
#endif |
|
||||
|
|
||||
@implementation AzureSpeechPlugin |
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { |
|
||||
[SwiftAzureSpeechPlugin registerWithRegistrar:registrar]; |
|
||||
} |
|
||||
@end |
|
||||
@ -1,6 +0,0 @@ |
|||||
#ifndef azure_speech_Bridging_Header_h |
|
||||
#define azure_speech_Bridging_Header_h |
|
||||
|
|
||||
#import <Foundation/Foundation.h> |
|
||||
|
|
||||
#endif /* azure_speech_Bridging_Header_h */ |
|
||||
@ -1,5 +0,0 @@ |
|||||
#import <Foundation/Foundation.h> |
|
||||
|
|
||||
#import "AzureSpeechPlugin.h" |
|
||||
|
|
||||
// 添加其他公共头文件导出
|
|
||||
@ -0,0 +1,128 @@ |
|||||
|
import Foundation |
||||
|
import SpeexKit |
||||
|
import os.log |
||||
|
|
||||
|
/// 基于杰理SpeexKit的Opus音频处理器 |
||||
|
@available(iOS 13.0, *) |
||||
|
class OpusAudioProcessor: NSObject { |
||||
|
|
||||
|
// MARK: - 回调代理协议 |
||||
|
protocol AudioDataCallback: AnyObject { |
||||
|
/// 接收到解码后的PCM音频数据 |
||||
|
func onAudioDataReceived(data: Data) |
||||
|
} |
||||
|
|
||||
|
// MARK: - 属性 |
||||
|
private var isDecodingActive = false |
||||
|
private weak var callback: AudioDataCallback? |
||||
|
private let logger = OSLog(subsystem: "com.yunqiinnovation.ble_service", category: "OpusAudioProcessor") |
||||
|
|
||||
|
// MARK: - 初始化 |
||||
|
override init() { |
||||
|
super.init() |
||||
|
|
||||
|
// 注册通知 - 这是SDK设计要求的 |
||||
|
NotificationCenter.default.addObserver( |
||||
|
self, |
||||
|
selector: #selector(onOpusDecodedData(_:)), |
||||
|
name: NSNotification.Name(rawValue: kOPUS_DECODE_DATA), |
||||
|
object: nil |
||||
|
) |
||||
|
|
||||
|
// 启用日志 |
||||
|
OpusUnit.opusIsLog(true) |
||||
|
} |
||||
|
|
||||
|
deinit { |
||||
|
stopDecoding() |
||||
|
NotificationCenter.default.removeObserver(self) |
||||
|
} |
||||
|
|
||||
|
// MARK: - 通知回调 |
||||
|
@objc private func onOpusDecodedData(_ notification: Notification) { |
||||
|
guard let pcmData = notification.object as? Data else { |
||||
|
os_log("解码回调收到无效数据", log: logger, type: .error) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 传递给回调 |
||||
|
DispatchQueue.main.async { [weak self] in |
||||
|
self?.callback?.onAudioDataReceived(data: pcmData) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MARK: - 公共接口 |
||||
|
|
||||
|
/// 设置回调 |
||||
|
func setCallback(_ callback: AudioDataCallback?) { |
||||
|
self.callback = callback |
||||
|
} |
||||
|
|
||||
|
/// 开始Opus解码 |
||||
|
func startDecoding(sampleRate: Int32 = 16000, channels: Int32 = 1, frameSize: Int32 = 640) -> Bool { |
||||
|
// 如果已经在解码,直接返回成功 |
||||
|
if isDecodingActive { |
||||
|
os_log("Opus解码器已经在运行", log: logger, type: .debug) |
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
// 设置音频参数 |
||||
|
OpusUnit.opusSetSampleRate(Int32(sampleRate), kbps: 16000, channels: Int32(channels)) |
||||
|
|
||||
|
// 必须在后台线程启动解码器(SDK要求) |
||||
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
// 启动解码器 |
||||
|
let result = OpusUnit.opusDecoderRun() |
||||
|
if result == 0 { |
||||
|
os_log("Opus解码器启动成功: 采样率=%d Hz", log: self.logger, type: .info, sampleRate) |
||||
|
self.isDecodingActive = true |
||||
|
} else { |
||||
|
os_log("Opus解码器启动失败: 错误=%d", log: self.logger, type: .error, result) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/// 停止Opus解码 |
||||
|
func stopDecoding() -> Bool { |
||||
|
if !isDecodingActive { |
||||
|
return false |
||||
|
} |
||||
|
|
||||
|
// 必须在后台线程停止解码器 |
||||
|
DispatchQueue.global(qos: .userInitiated).async { [weak self] in |
||||
|
guard let self = self else { return } |
||||
|
|
||||
|
let result = OpusUnit.opusDecoderStop() |
||||
|
os_log("Opus解码器停止: %d", log: self.logger, type: .info, result) |
||||
|
self.isDecodingActive = false |
||||
|
} |
||||
|
|
||||
|
return true |
||||
|
} |
||||
|
|
||||
|
/// 处理Opus编码数据 |
||||
|
func processAudioData(_ data: Data) { |
||||
|
if !isDecodingActive { |
||||
|
os_log("Opus解码器未启动", log: logger, type: .error) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// 将Opus数据送入解码器 |
||||
|
OpusUnit.opusWrite(data) |
||||
|
// 解码结果将通过通知回调给onOpusDecodedData方法 |
||||
|
} |
||||
|
|
||||
|
/// 判断解码器是否运行中 |
||||
|
func isDecoding() -> Bool { |
||||
|
return isDecodingActive |
||||
|
} |
||||
|
|
||||
|
/// 清理资源 |
||||
|
func cleanup() { |
||||
|
stopDecoding() |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
//
|
||||
|
// OpusUnit.h
|
||||
|
// QCY_Demo
|
||||
|
//
|
||||
|
// Created by 杰理科技 on 2021/7/5.
|
||||
|
// Copyright © 2021 杰理科技. All rights reserved.
|
||||
|
//
|
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
NS_ASSUME_NONNULL_BEGIN |
||||
|
|
||||
|
/**
|
||||
|
* 通知:流式编码回调Opus数据 |
||||
|
* 通知数据类型:NSData(Opus) |
||||
|
*/ |
||||
|
extern NSString *kOPUS_ENCODE_DATA; |
||||
|
/**
|
||||
|
* 通知:流式解码回调PCM数据 |
||||
|
* 通知数据类型:NSData(PCM) |
||||
|
*/ |
||||
|
extern NSString *kOPUS_DECODE_DATA; |
||||
|
|
||||
|
|
||||
|
@interface OpusUnit : NSObject |
||||
|
|
||||
|
+ (void)opusIsLog:(BOOL)log; |
||||
|
|
||||
|
#pragma mark - Opus参数 |
||||
|
/**
|
||||
|
* 设置参数 |
||||
|
* @param rate 采样率 例如:48000、24000、16000、12000、8000(默认 16000) |
||||
|
* @param kbps 比特率 例如:16000、32000、64000(默认 16000) |
||||
|
* @param channel 声道 例如:1、2 (默认 1) |
||||
|
*/ |
||||
|
+(void)opusSetSampleRate:(int)rate Kbps:(int)kbps Channels:(int)channel; |
||||
|
|
||||
|
#pragma mark - Opus解码 |
||||
|
|
||||
|
/**
|
||||
|
* 直接【opus文件】转换成【pcm文件】 |
||||
|
* @param path_opus opus文件路径 |
||||
|
* @param path_pcm pcm文件路径 |
||||
|
*/ |
||||
|
+ (int)opusDecodeOPUS:(NSString *)path_opus PCM:(NSString *)path_pcm; |
||||
|
|
||||
|
/**
|
||||
|
* 流式解码【开启】 |
||||
|
*/ |
||||
|
+ (int)opusDecoderRun; |
||||
|
|
||||
|
/**
|
||||
|
* 输入Opus的数据 |
||||
|
* 通知监听“kOPUS_DECODE_DATA”获得解码后数据 |
||||
|
* 通知数据类型:NSNotification.object => NSData(PCM格式) |
||||
|
* @param data opus数据流(长度:1024) |
||||
|
*/ |
||||
|
+ (void)opusWriteData:(NSData*)data; |
||||
|
|
||||
|
/**
|
||||
|
* 流式解码【关闭】 |
||||
|
*/ |
||||
|
+ (int)opusDecoderStop; |
||||
|
|
||||
|
#pragma mark - Opus编码 |
||||
|
|
||||
|
/**
|
||||
|
* 【pcm文件】转换成【opus文件】 |
||||
|
*/ |
||||
|
+ (int)opusEncodePCM:(NSString *)path_pcm OPUS:(NSString *)path_opus; |
||||
|
|
||||
|
/**
|
||||
|
* 流式编码【开启】 |
||||
|
*/ |
||||
|
+ (int)opusEecoderRun; |
||||
|
|
||||
|
/**
|
||||
|
* 输入pcm的数据 |
||||
|
* 通知监听“kOPUS_DECODE_DATA”获得解码后数据 |
||||
|
* 通知数据类型:NSNotification.object => NSData(opus格式) |
||||
|
* @param data opus数据流(长度:1024) |
||||
|
*/ |
||||
|
+ (void)pcmWriteData:(NSData*)data; |
||||
|
|
||||
|
/**
|
||||
|
* 流式编码【关闭】 |
||||
|
*/ |
||||
|
+ (int)opusEncoderStop; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
NS_ASSUME_NONNULL_END |
||||
@ -0,0 +1,20 @@ |
|||||
|
//
|
||||
|
// SpeexKit.h
|
||||
|
// SpeexKit
|
||||
|
//
|
||||
|
// Created by zhihui liang on 2018/8/20.
|
||||
|
// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
|
||||
|
//
|
||||
|
|
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
//! Project version number for SpeexKit.
|
||||
|
FOUNDATION_EXPORT double SpeexKitVersionNumber; |
||||
|
|
||||
|
//! Project version string for SpeexKit.
|
||||
|
FOUNDATION_EXPORT const unsigned char SpeexKitVersionString[]; |
||||
|
|
||||
|
// In this header, you should import all the public headers of your framework using statements like #import <SpeexKit/PublicHeader.h>
|
||||
|
|
||||
|
#import <SpeexKit/SpeexUnit.h> |
||||
|
#import <SpeexKit/OpusUnit.h> |
||||
@ -0,0 +1,40 @@ |
|||||
|
//
|
||||
|
// SpeexUnit.h
|
||||
|
// SpeexKit
|
||||
|
//
|
||||
|
// Created by zhihui liang on 2018/8/20.
|
||||
|
// Copyright © 2018年 www.zh-jieli.com. All rights reserved.
|
||||
|
//
|
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
/**
|
||||
|
* 流式解码回调PCM数据 |
||||
|
*/ |
||||
|
extern NSString *kSPEEX_DECODE_DATA; //speex decode
|
||||
|
|
||||
|
@interface SpeexUnit : NSObject |
||||
|
|
||||
|
+(void)speexIsLog:(BOOL)log; |
||||
|
|
||||
|
/**
|
||||
|
* 直接【speex文件】转换成【pcm文件】 |
||||
|
*/ |
||||
|
+(int)speexDecodeSPX:(NSString *)path_spx PCM:(NSString *)path_pcm; |
||||
|
|
||||
|
/**
|
||||
|
* 流式解码【开启】 |
||||
|
*/ |
||||
|
+(int)speexDecoderRun; |
||||
|
|
||||
|
/**
|
||||
|
* 输入Speex的数据 |
||||
|
*/ |
||||
|
+(void)speexWriteData:(NSData*)data; |
||||
|
|
||||
|
/**
|
||||
|
* 流式解码【关闭】 |
||||
|
*/ |
||||
|
+(int)speexDecoderStop; |
||||
|
|
||||
|
@end |
||||
Binary file not shown.
@ -0,0 +1,6 @@ |
|||||
|
framework module SpeexKit { |
||||
|
umbrella header "SpeexKit.h" |
||||
|
|
||||
|
export * |
||||
|
module * { export * } |
||||
|
} |
||||
Binary file not shown.
@ -1,26 +1,72 @@ |
|||||
name: ble_service |
name: ble_service |
||||
description: BLE服务插件,提供蓝牙扫描、连接和通信功能 |
description: 蓝牙低功耗服务插件,提供蓝牙设备连接、通信功能 |
||||
version: 0.0.1 |
version: 0.0.1 |
||||
homepage: |
homepage: |
||||
|
|
||||
environment: |
environment: |
||||
sdk: ">=2.16.0 <3.0.0" |
sdk: ">=2.18.0 <4.0.0" |
||||
flutter: ">=2.5.0" |
flutter: ">=3.3.0" |
||||
|
|
||||
dependencies: |
dependencies: |
||||
flutter: |
flutter: |
||||
sdk: flutter |
sdk: flutter |
||||
|
# 添加其他依赖项 |
||||
|
|
||||
dev_dependencies: |
dev_dependencies: |
||||
flutter_test: |
flutter_test: |
||||
sdk: flutter |
sdk: flutter |
||||
flutter_lints: ^1.0.0 |
flutter_lints: ^2.0.0 |
||||
|
|
||||
|
# For information on the generic Dart part of this file, see the |
||||
|
# following page: https://dart.dev/tools/pub/pubspec |
||||
|
|
||||
|
# The following section is specific to Flutter packages. |
||||
flutter: |
flutter: |
||||
|
# This section identifies this Flutter project as a plugin project. |
||||
|
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) |
||||
|
# which should be registered in the plugin registry. This is required for |
||||
|
# using method channels. |
||||
|
# The Android 'package' specifies package in which the registered class is. |
||||
|
# This is required for using method channels on Android. |
||||
|
# The 'ffiPlugin' specifies that native code should be built and bundled. |
||||
|
# This is required for using `dart:ffi`. |
||||
|
# All these are used by the tooling to maintain consistency when |
||||
|
# adding or updating assets for this project. |
||||
plugin: |
plugin: |
||||
platforms: |
platforms: |
||||
android: |
android: |
||||
package: com.yunqiinnovation.ble_service |
package: com.yunqiinnovation.ble_service |
||||
pluginClass: BleServicePlugin |
pluginClass: BleServicePlugin |
||||
ios: |
ios: |
||||
pluginClass: BleServicePlugin |
pluginClass: BleServicePlugin |
||||
|
|
||||
|
# To add assets to your plugin package, add an assets section, like this: |
||||
|
# assets: |
||||
|
# - images/a_dot_burr.jpeg |
||||
|
# - images/a_dot_ham.jpeg |
||||
|
# |
||||
|
# For details regarding assets in packages, see |
||||
|
# https://flutter.dev/assets-and-images/#from-packages |
||||
|
# |
||||
|
# An image asset can refer to one or more resolution-specific "variants", see |
||||
|
# https://flutter.dev/assets-and-images/#resolution-aware |
||||
|
|
||||
|
# To add custom fonts to your plugin package, add a fonts section here, |
||||
|
# in this "flutter" section. Each entry in this list should have a |
||||
|
# "family" key with the font family name, and a "fonts" key with a |
||||
|
# list giving the asset and other descriptors for the font. For |
||||
|
# example: |
||||
|
# fonts: |
||||
|
# - family: Schyler |
||||
|
# fonts: |
||||
|
# - asset: fonts/Schyler-Regular.ttf |
||||
|
# - asset: fonts/Schyler-Italic.ttf |
||||
|
# style: italic |
||||
|
# - family: Trajan Pro |
||||
|
# fonts: |
||||
|
# - asset: fonts/TrajanPro.ttf |
||||
|
# - asset: fonts/TrajanPro_Bold.ttf |
||||
|
# weight: 700 |
||||
|
# |
||||
|
# For details regarding fonts in packages, see |
||||
|
# https://flutter.dev/custom-fonts/#from-packages |
||||
Loading…
Reference in new issue