优化配置,修复 demo bug
1,network 框架完善 2,websocket 机制完善 3,设计文档整理到架构文档 4,脚本,配置完善
This commit is contained in:
@@ -1,66 +0,0 @@
|
||||
group = "com.example.cipher_guard_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "kotlin-android"
|
||||
|
||||
android {
|
||||
namespace = "com.example.cipher_guard_sdk"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += "src/main/kotlin"
|
||||
test.java.srcDirs += "src/test/kotlin"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
outputs.upToDateWhen {false}
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/cipher_guard_sdk/android/build.gradle.kts
Normal file
71
packages/cipher_guard_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
||||
group = "com.example.cipher_guard_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.cipher_guard_sdk"
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
all {
|
||||
it.useJUnitPlatform()
|
||||
it.outputs.upToDateWhen { false }
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Flutter
|
||||
@preconcurrency import Flutter
|
||||
import UIKit
|
||||
|
||||
public class CipherGuardSdkPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
||||
|
||||
# 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'
|
||||
s.swift_version = '6.2'
|
||||
|
||||
# If your plugin requires a privacy manifest, for example if it uses any
|
||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||
|
||||
@@ -29,23 +29,25 @@ class EncryptionFlutterService {
|
||||
// Get secure random
|
||||
final secureRandom = FortunaRandom();
|
||||
secureRandom.seed(KeyParameter(_generateSecureRandomBytes(32)));
|
||||
|
||||
|
||||
// Create RSA key generator
|
||||
final keyGen = RSAKeyGenerator();
|
||||
keyGen.init(ParametersWithRandom(
|
||||
RSAKeyGeneratorParameters(BigInt.parse('65537'), keySize, 64),
|
||||
secureRandom,
|
||||
));
|
||||
|
||||
keyGen.init(
|
||||
ParametersWithRandom(
|
||||
RSAKeyGeneratorParameters(BigInt.parse('65537'), keySize, 64),
|
||||
secureRandom,
|
||||
),
|
||||
);
|
||||
|
||||
// Generate key pair
|
||||
final keyPair = keyGen.generateKeyPair();
|
||||
final rsaPublicKey = keyPair.publicKey as RSAPublicKey;
|
||||
final rsaPrivateKey = keyPair.privateKey as RSAPrivateKey;
|
||||
|
||||
final rsaPublicKey = keyPair.publicKey;
|
||||
final rsaPrivateKey = keyPair.privateKey;
|
||||
|
||||
// Export to PEM format
|
||||
final publicKeyPem = _encodeRSAPublicKey(rsaPublicKey);
|
||||
final privateKeyPem = _encodeRSAPrivateKey(rsaPrivateKey);
|
||||
|
||||
|
||||
return RsaKeyPairResult(
|
||||
publicKey: publicKeyPem,
|
||||
privateKey: privateKeyPem,
|
||||
@@ -59,18 +61,18 @@ class EncryptionFlutterService {
|
||||
String _encodeRSAPublicKey(RSAPublicKey publicKey) {
|
||||
// Build RSAPublicKeyInfo structure
|
||||
final topSeq = ASN1Sequence();
|
||||
|
||||
|
||||
// AlgorithmIdentifier: OID 1.2.840.113549.1.1.1 + NULL
|
||||
final algoSeq = ASN1Sequence();
|
||||
algoSeq.add(ASN1ObjectIdentifier([1, 2, 840, 113549, 1, 1, 1])); // RSA
|
||||
algoSeq.add(ASN1Null());
|
||||
topSeq.add(algoSeq);
|
||||
|
||||
|
||||
// RSAPublicKey: modulus + publicExponent
|
||||
final keySeq = ASN1Sequence();
|
||||
keySeq.add(ASN1Integer(publicKey.n!));
|
||||
keySeq.add(ASN1Integer(publicKey.exponent!));
|
||||
|
||||
|
||||
// BitString wrapping the key (with 0 unused bits prefix)
|
||||
final keyBytes = keySeq.encodedBytes;
|
||||
final keyList = List<int>.from(keyBytes);
|
||||
@@ -86,27 +88,27 @@ class EncryptionFlutterService {
|
||||
String _encodeRSAPrivateKey(RSAPrivateKey privateKey) {
|
||||
// Build RSAPrivateKey structure (PKCS#8 format)
|
||||
final topSeq = ASN1Sequence();
|
||||
|
||||
|
||||
// Version (0)
|
||||
topSeq.add(ASN1Integer(BigInt.zero));
|
||||
|
||||
|
||||
// Modulus
|
||||
topSeq.add(ASN1Integer(privateKey.n!));
|
||||
|
||||
|
||||
// Public Exponent
|
||||
topSeq.add(ASN1Integer(privateKey.exponent!));
|
||||
|
||||
|
||||
// Private Exponent
|
||||
topSeq.add(ASN1Integer(privateKey.privateExponent!));
|
||||
|
||||
|
||||
// Prime P
|
||||
topSeq.add(ASN1Integer(privateKey.p!));
|
||||
|
||||
|
||||
// Prime Q
|
||||
topSeq.add(ASN1Integer(privateKey.q!));
|
||||
|
||||
|
||||
// (Optional CRT params omitted for simplicity)
|
||||
|
||||
|
||||
final derBytes = topSeq.encodedBytes;
|
||||
final base64 = base64Encode(derBytes.toList());
|
||||
return '-----BEGIN PRIVATE KEY-----\n$base64\n-----END PRIVATE KEY-----';
|
||||
@@ -122,24 +124,24 @@ class EncryptionFlutterService {
|
||||
try {
|
||||
// Generate AES key from MD5(password)
|
||||
final aesKey = _md5Hash(password);
|
||||
|
||||
|
||||
// Generate random IV (16 bytes)
|
||||
final iv = _generateSecureRandomBytes(16);
|
||||
|
||||
|
||||
// AES encrypt using encrypt package
|
||||
final secretKey = encrypt_pkg.Key(aesKey);
|
||||
final encryptor = encrypt_pkg.Encrypter(
|
||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.cbc),
|
||||
);
|
||||
|
||||
|
||||
final encrypted = encryptor.encrypt(privateKey, iv: encrypt_pkg.IV(iv));
|
||||
final encryptedBytes = encrypted.bytes;
|
||||
|
||||
|
||||
// Combine IV + encrypted data
|
||||
final combined = Uint8List(iv.length + encryptedBytes.length);
|
||||
combined.setAll(0, iv);
|
||||
combined.setAll(iv.length, encryptedBytes);
|
||||
|
||||
|
||||
return base64Encode(combined);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to encrypt private key: $e');
|
||||
@@ -154,25 +156,25 @@ class EncryptionFlutterService {
|
||||
try {
|
||||
// Generate AES key from MD5(password)
|
||||
final aesKey = _md5Hash(password);
|
||||
|
||||
|
||||
// Decode Base64
|
||||
final combined = base64Decode(encryptedPrivateKey);
|
||||
|
||||
|
||||
// Extract IV and encrypted data
|
||||
final iv = combined.sublist(0, 16);
|
||||
final encBytes = combined.sublist(16);
|
||||
|
||||
|
||||
// AES decrypt
|
||||
final secretKey = encrypt_pkg.Key(aesKey);
|
||||
final encryptor = encrypt_pkg.Encrypter(
|
||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.cbc),
|
||||
);
|
||||
|
||||
|
||||
final decrypted = encryptor.decrypt(
|
||||
encrypt_pkg.Encrypted(encBytes),
|
||||
iv: encrypt_pkg.IV(iv),
|
||||
);
|
||||
|
||||
|
||||
return decrypted;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to decrypt private key: $e');
|
||||
@@ -185,11 +187,8 @@ class EncryptionFlutterService {
|
||||
SessionKeyResult generateSessionKey({int initialRound = 1}) {
|
||||
final keyBytes = _generateSecureRandomBytes(sessionKeySize);
|
||||
final key = base64Encode(keyBytes);
|
||||
|
||||
return SessionKeyResult(
|
||||
key: key,
|
||||
round: initialRound,
|
||||
);
|
||||
|
||||
return SessionKeyResult(key: key, round: initialRound);
|
||||
}
|
||||
|
||||
/// Encrypt session key with RSA public key
|
||||
@@ -200,11 +199,11 @@ class EncryptionFlutterService {
|
||||
try {
|
||||
// Parse RSA public key
|
||||
final rsaPublicKey = _parsePublicKey(publicKey);
|
||||
|
||||
|
||||
// RSA encrypt using PKCS1 padding (like native implementations)
|
||||
final cipher = PKCS1Encoding(RSAEngine());
|
||||
cipher.init(true, PublicKeyParameter<RSAPublicKey>(rsaPublicKey));
|
||||
|
||||
|
||||
final encryptedBytes = cipher.process(utf8.encode(sessionKey));
|
||||
return base64Encode(encryptedBytes);
|
||||
} catch (e) {
|
||||
@@ -220,11 +219,11 @@ class EncryptionFlutterService {
|
||||
try {
|
||||
// Parse RSA private key
|
||||
final rsaPrivateKey = _parsePrivateKey(privateKey);
|
||||
|
||||
|
||||
// RSA decrypt using PKCS1 padding (like native implementations)
|
||||
final cipher = PKCS1Encoding(RSAEngine());
|
||||
cipher.init(false, PrivateKeyParameter<RSAPrivateKey>(rsaPrivateKey));
|
||||
|
||||
|
||||
final decryptedBytes = cipher.process(base64Decode(encryptedSessionKey));
|
||||
return utf8.decode(decryptedBytes);
|
||||
} catch (e) {
|
||||
@@ -243,30 +242,27 @@ class EncryptionFlutterService {
|
||||
try {
|
||||
// Derive key for round
|
||||
final actualKey = _deriveKeyForRound(sessionKey, round);
|
||||
|
||||
|
||||
// Generate random IV (16 bytes for CTR)
|
||||
final iv = _generateSecureRandomBytes(16);
|
||||
|
||||
|
||||
// AES-CTR encrypt
|
||||
final secretKey = encrypt_pkg.Key(actualKey);
|
||||
final encryptor = encrypt_pkg.Encrypter(
|
||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.ctr),
|
||||
);
|
||||
|
||||
|
||||
final encrypted = encryptor.encrypt(plaintext, iv: encrypt_pkg.IV(iv));
|
||||
final encryptedBytes = encrypted.bytes;
|
||||
|
||||
|
||||
// Combine IV + encrypted data
|
||||
final combined = Uint8List(iv.length + encryptedBytes.length);
|
||||
combined.setAll(0, iv);
|
||||
combined.setAll(iv.length, encryptedBytes);
|
||||
|
||||
|
||||
final data = base64Encode(combined);
|
||||
|
||||
return EncryptedMessageResult(
|
||||
round: round,
|
||||
data: data,
|
||||
);
|
||||
|
||||
return EncryptedMessageResult(round: round, data: data);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to encrypt message: $e');
|
||||
}
|
||||
@@ -281,25 +277,25 @@ class EncryptionFlutterService {
|
||||
try {
|
||||
// Derive key for round
|
||||
final actualKey = _deriveKeyForRound(sessionKey, round);
|
||||
|
||||
|
||||
// Decode Base64
|
||||
final combined = base64Decode(encryptedData);
|
||||
|
||||
|
||||
// Extract IV and encrypted data
|
||||
final iv = combined.sublist(0, 16);
|
||||
final encBytes = combined.sublist(16);
|
||||
|
||||
|
||||
// AES-CTR decrypt
|
||||
final secretKey = encrypt_pkg.Key(actualKey);
|
||||
final encryptor = encrypt_pkg.Encrypter(
|
||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.ctr),
|
||||
);
|
||||
|
||||
|
||||
final decrypted = encryptor.decrypt(
|
||||
encrypt_pkg.Encrypted(encBytes),
|
||||
iv: encrypt_pkg.IV(iv),
|
||||
);
|
||||
|
||||
|
||||
return decrypted;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to decrypt message: $e');
|
||||
@@ -316,36 +312,34 @@ class EncryptionFlutterService {
|
||||
String? _aesSecret;
|
||||
|
||||
/// Decrypt push notification (AES-GCM)
|
||||
String decryptPushNotification({
|
||||
required String encryptedData,
|
||||
}) {
|
||||
String decryptPushNotification({required String encryptedData}) {
|
||||
try {
|
||||
final secret = _aesSecret;
|
||||
if (secret == null) {
|
||||
throw Exception('AES_SECRET not set');
|
||||
}
|
||||
|
||||
|
||||
// Convert hex string to bytes
|
||||
final secretBytes = _hexStringToBytes(secret);
|
||||
|
||||
|
||||
// Decode Base64
|
||||
final combined = base64Decode(encryptedData);
|
||||
|
||||
|
||||
// Extract IV and encrypted data
|
||||
final iv = combined.sublist(0, gcmIvLength);
|
||||
final encBytes = combined.sublist(gcmIvLength);
|
||||
|
||||
|
||||
// AES-GCM decrypt
|
||||
final secretKey = encrypt_pkg.Key(secretBytes);
|
||||
final encryptor = encrypt_pkg.Encrypter(
|
||||
encrypt_pkg.AES(secretKey, mode: encrypt_pkg.AESMode.gcm),
|
||||
);
|
||||
|
||||
|
||||
final decrypted = encryptor.decrypt(
|
||||
encrypt_pkg.Encrypted(encBytes),
|
||||
iv: encrypt_pkg.IV(iv),
|
||||
);
|
||||
|
||||
|
||||
return decrypted;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to decrypt push notification: $e');
|
||||
@@ -375,10 +369,10 @@ class EncryptionFlutterService {
|
||||
Uint8List _deriveKeyForRound(String sessionKey, int targetRound) {
|
||||
// Base64 decode session key
|
||||
final keyBytes = base64Decode(sessionKey);
|
||||
|
||||
|
||||
// Apply MD5 for the round (simplified version)
|
||||
final hash = md5.convert(keyBytes).bytes as Uint8List;
|
||||
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -390,19 +384,19 @@ class EncryptionFlutterService {
|
||||
.replaceAll('\n', '')
|
||||
.trim();
|
||||
final bytes = base64Decode(base64);
|
||||
|
||||
|
||||
// Parse ASN.1 DER format
|
||||
final asn1Parser = ASN1Parser(Uint8List.fromList(bytes));
|
||||
final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
|
||||
|
||||
|
||||
final subjectPublicKeyInfo = topLevelSeq.elements[1] as ASN1BitString;
|
||||
final keyBytes = subjectPublicKeyInfo.contentBytes();
|
||||
final keyParser = ASN1Parser(Uint8List.fromList(keyBytes));
|
||||
final keySeq = keyParser.nextObject() as ASN1Sequence;
|
||||
|
||||
|
||||
final modulus = keySeq.elements[0] as ASN1Integer;
|
||||
final publicExponent = keySeq.elements[1] as ASN1Integer;
|
||||
|
||||
|
||||
return RSAPublicKey(
|
||||
modulus.valueAsBigInteger,
|
||||
publicExponent.valueAsBigInteger,
|
||||
@@ -417,11 +411,11 @@ class EncryptionFlutterService {
|
||||
.replaceAll('\n', '')
|
||||
.trim();
|
||||
final bytes = base64Decode(base64);
|
||||
|
||||
|
||||
// Parse ASN.1 DER format
|
||||
final asn1Parser = ASN1Parser(Uint8List.fromList(bytes));
|
||||
final keySeq = asn1Parser.nextObject() as ASN1Sequence;
|
||||
|
||||
|
||||
final modulus = keySeq.elements[1] as ASN1Integer;
|
||||
final privateExponent = keySeq.elements[3] as ASN1Integer;
|
||||
final p = keySeq.elements[4] as ASN1Integer;
|
||||
@@ -440,7 +434,9 @@ class EncryptionFlutterService {
|
||||
final len = hex.length;
|
||||
final data = Uint8List(len ~/ 2);
|
||||
for (var i = 0; i < len; i += 2) {
|
||||
data[i ~/ 2] = (int.parse(hex[i], radix: 16) << 4) + int.parse(hex[i + 1], radix: 16);
|
||||
data[i ~/ 2] =
|
||||
(int.parse(hex[i], radix: 16) << 4) +
|
||||
int.parse(hex[i + 1], radix: 16);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -468,4 +464,3 @@ class EncryptedMessageResult {
|
||||
|
||||
EncryptedMessageResult({required this.round, required this.data});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ buildscript {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
@@ -23,12 +22,11 @@ allprojects {
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("kotlin-android")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.im_log_sdk"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
@@ -36,17 +34,9 @@ android {
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") {
|
||||
java.srcDirs("src/main/kotlin")
|
||||
}
|
||||
getByName("test") {
|
||||
java.srcDirs("src/test/kotlin")
|
||||
}
|
||||
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@@ -58,9 +48,7 @@ android {
|
||||
isIncludeAndroidResources = true
|
||||
all {
|
||||
it.useJUnitPlatform()
|
||||
|
||||
it.outputs.upToDateWhen { false }
|
||||
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
@@ -70,6 +58,12 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Flutter
|
||||
@preconcurrency import Flutter
|
||||
import UIKit
|
||||
|
||||
public class ImLogSdkPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
||||
|
||||
# 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'
|
||||
s.swift_version = '6.2'
|
||||
|
||||
# If your plugin requires a privacy manifest, for example if it uses any
|
||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
@preconcurrency import FlutterMacOS
|
||||
|
||||
public class ImLogSdkPlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
|
||||
@@ -24,7 +24,7 @@ A new Flutter plugin project.
|
||||
|
||||
s.dependency 'FlutterMacOS'
|
||||
|
||||
s.platform = :osx, '10.11'
|
||||
s.platform = :osx, '14.0'
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||
s.swift_version = '5.0'
|
||||
s.swift_version = '6.2'
|
||||
end
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
group = "com.example.l10n_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "kotlin-android"
|
||||
|
||||
android {
|
||||
namespace = "com.example.l10n_sdk"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += "src/main/kotlin"
|
||||
test.java.srcDirs += "src/test/kotlin"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
outputs.upToDateWhen {false}
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/l10n_sdk/android/build.gradle.kts
Normal file
71
packages/l10n_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
||||
group = "com.example.l10n_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.l10n_sdk"
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
all {
|
||||
it.useJUnitPlatform()
|
||||
it.outputs.upToDateWhen { false }
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Flutter
|
||||
@preconcurrency import Flutter
|
||||
import UIKit
|
||||
|
||||
public class L10nSdkPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
||||
|
||||
# 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'
|
||||
s.swift_version = '6.2'
|
||||
|
||||
# If your plugin requires a privacy manifest, for example if it uses any
|
||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
group = "com.example.media_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "kotlin-android"
|
||||
|
||||
android {
|
||||
namespace = "com.example.media_sdk"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += "src/main/kotlin"
|
||||
test.java.srcDirs += "src/test/kotlin"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
outputs.upToDateWhen {false}
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/media_sdk/android/build.gradle.kts
Normal file
71
packages/media_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
||||
group = "com.example.media_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.media_sdk"
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
all {
|
||||
it.useJUnitPlatform()
|
||||
it.outputs.upToDateWhen { false }
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Flutter
|
||||
@preconcurrency import Flutter
|
||||
import UIKit
|
||||
|
||||
public class MediaSdkPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
||||
|
||||
# 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'
|
||||
s.swift_version = '6.2'
|
||||
|
||||
# If your plugin requires a privacy manifest, for example if it uses any
|
||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
group = "com.example.networks_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "kotlin-android"
|
||||
|
||||
android {
|
||||
namespace = "com.example.networks_sdk"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += "src/main/kotlin"
|
||||
test.java.srcDirs += "src/test/kotlin"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
outputs.upToDateWhen {false}
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/networks_sdk/android/build.gradle.kts
Normal file
71
packages/networks_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
||||
group = "com.example.networks_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.networks_sdk"
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
all {
|
||||
it.useJUnitPlatform()
|
||||
it.outputs.upToDateWhen { false }
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Flutter
|
||||
@preconcurrency import Flutter
|
||||
import UIKit
|
||||
|
||||
public class NetworksSdkPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
||||
|
||||
# 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'
|
||||
s.swift_version = '6.2'
|
||||
|
||||
# If your plugin requires a privacy manifest, for example if it uses any
|
||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||
|
||||
@@ -6,8 +6,9 @@ export 'src/presentation/facade/networks_messaging_api.dart';
|
||||
// Wiring - Implementations
|
||||
export 'src/presentation/wiring/networks_messaging_api_impl.dart';
|
||||
|
||||
// Dio 类型重导出(App 层上传 / override decodeResponse 需要,避免直接依赖 dio)
|
||||
export 'package:dio/dio.dart' show FormData, MultipartFile, Response;
|
||||
// Dio 类型重导出(App 层上传 / CancelToken / override decodeResponse 需要,避免直接依赖 dio)
|
||||
export 'package:dio/dio.dart'
|
||||
show FormData, MultipartFile, Response, CancelToken;
|
||||
|
||||
// Config
|
||||
export 'src/presentation/wiring/api_config.dart';
|
||||
@@ -18,6 +19,7 @@ export 'src/presentation/wiring/network_callbacks.dart';
|
||||
export 'src/data/dto/api_requestable.dart';
|
||||
export 'src/data/dto/api_response_wrapper.dart';
|
||||
export 'src/domain/entities/api_error.dart';
|
||||
export 'src/domain/entities/encrypted_request.dart';
|
||||
export 'src/domain/entities/http_method.dart';
|
||||
export 'src/domain/entities/api_request_type.dart';
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/data/datasources/http/interceptor/auth_interceptor.dart';
|
||||
import 'package:networks_sdk/src/data/datasources/http/interceptor/encryption_interceptor.dart';
|
||||
import 'package:networks_sdk/src/data/datasources/http/interceptor/logging_interceptor.dart';
|
||||
import 'package:networks_sdk/src/data/datasources/http/interceptor/retry_interceptor.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/api_error.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
|
||||
/// REST API 客户端
|
||||
/// 基于 Dio,提供 `executeRequest<T>` 唯一入口
|
||||
/// 基于 Dio,提供请求执行入口
|
||||
///
|
||||
/// 拦截器链顺序:Auth → Encryption → 自定义 → Retry → Logging
|
||||
///
|
||||
/// 使用方式:
|
||||
/// ```dart
|
||||
@@ -28,9 +31,10 @@ class ApiClient {
|
||||
receiveTimeout: const Duration(seconds: 60),
|
||||
);
|
||||
|
||||
// 挂载拦截器(顺序:Auth → 自定义 → Retry → Logging)
|
||||
// 挂载拦截器(顺序:Auth → Encryption → 自定义 → Retry → Logging)
|
||||
_dio.interceptors.addAll([
|
||||
AuthInterceptor(config),
|
||||
EncryptionInterceptor(config),
|
||||
if (additionalInterceptors != null) ...additionalInterceptors,
|
||||
RetryInterceptor(config: config, dio: _dio),
|
||||
LoggingInterceptor(onLog: config.onLog),
|
||||
@@ -49,16 +53,16 @@ class ApiClient {
|
||||
return const ApiError.timeout();
|
||||
case DioExceptionType.connectionError:
|
||||
return const ApiError.noNetworkConnection();
|
||||
case DioExceptionType.cancel:
|
||||
return const ApiError.cancelled();
|
||||
default:
|
||||
if (e.response != null) {
|
||||
return ApiError.apiError(
|
||||
code: e.response!.statusCode ?? 0,
|
||||
message: e.response!.statusMessage ??
|
||||
e.message ??
|
||||
'Request failed',
|
||||
message: e.response!.statusMessage ?? e.message ?? 'Request failed',
|
||||
);
|
||||
}
|
||||
return ApiError.networkError(e.message ?? 'Network error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
|
||||
/// 加密拦截器(预留给 cipher_guard_sdk)
|
||||
///
|
||||
/// 在拦截器链中位于 Auth 之后、Retry 之前:
|
||||
/// `Auth → Encryption → Custom → Retry → Logging`
|
||||
///
|
||||
/// 回调为 null 时自动跳过,不影响正常请求流程。
|
||||
/// 后续 cipher_guard_sdk 接入后,App 层在 ApiConfig 中注入
|
||||
/// `onEncryptRequest` / `onDecryptResponse` 即可启用加密。
|
||||
///
|
||||
/// ## 加密能力
|
||||
///
|
||||
/// 与简单的 body 加解密不同,本拦截器支持完整的请求改写:
|
||||
/// - 路径加密(如 `/api/login` → `/api/hex(encrypt(login))`)
|
||||
/// - 请求体加密(Map → base64 字符串)
|
||||
/// - Header 注入(X-Token、X-Signature、secret-key 等)
|
||||
/// - Content-Type 覆盖(application/json → text/plain)
|
||||
///
|
||||
/// 加密回调接收原始 path、headers、body,返回 [EncryptedRequest],
|
||||
/// 拦截器根据非 null 字段覆盖请求。
|
||||
class EncryptionInterceptor extends Interceptor {
|
||||
final ApiConfig _config;
|
||||
|
||||
EncryptionInterceptor(this._config);
|
||||
|
||||
@override
|
||||
void onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) async {
|
||||
final encrypt = _config.onEncryptRequest;
|
||||
if (encrypt == null) {
|
||||
handler.next(options);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 收集当前 headers(转为 Map<String, String>)
|
||||
final currentHeaders = <String, String>{};
|
||||
options.headers.forEach((key, value) {
|
||||
if (value != null) currentHeaders[key] = value.toString();
|
||||
});
|
||||
|
||||
final result = await encrypt(options.path, currentHeaders, options.data);
|
||||
|
||||
// 根据非 null 字段覆盖请求
|
||||
if (result.path != null) {
|
||||
options.path = result.path!;
|
||||
}
|
||||
if (result.body != null) {
|
||||
options.data = result.body;
|
||||
}
|
||||
if (result.headers != null) {
|
||||
options.headers.addAll(result.headers!);
|
||||
}
|
||||
if (result.contentType != null) {
|
||||
options.contentType = result.contentType;
|
||||
}
|
||||
|
||||
_config.onLog?.call(
|
||||
'Request encrypted: ${options.path}',
|
||||
tag: 'Encryption',
|
||||
);
|
||||
|
||||
handler.next(options);
|
||||
} catch (e) {
|
||||
_config.onLog?.call('Request encryption failed: $e', tag: 'Encryption');
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: options,
|
||||
message: 'Request encryption failed: $e',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) async {
|
||||
final decrypt = _config.onDecryptResponse;
|
||||
if (decrypt == null) {
|
||||
handler.next(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 跳过 null 响应
|
||||
if (response.data == null) {
|
||||
handler.next(response);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final decrypted = await decrypt(response.data as Object);
|
||||
response.data = decrypted;
|
||||
|
||||
_config.onLog?.call(
|
||||
'Response decrypted: ${response.requestOptions.path}',
|
||||
tag: 'Encryption',
|
||||
);
|
||||
|
||||
handler.next(response);
|
||||
} catch (e) {
|
||||
_config.onLog?.call('Response decryption failed: $e', tag: 'Encryption');
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: response.requestOptions,
|
||||
response: response,
|
||||
message: 'Response decryption failed: $e',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,41 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/data/datasources/http/token_refresh_manager.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
|
||||
|
||||
/// 重试拦截器
|
||||
///
|
||||
/// 两层重试机制:
|
||||
///
|
||||
/// 1. **Token 刷新重试**(onResponse)
|
||||
/// 检测 Token 过期响应 → 触发刷新回调 → 用新 Token 重试原请求
|
||||
/// 检测 Token 过期响应 → 触发 [TokenRefreshManager] → 用新 Token 重试原请求
|
||||
///
|
||||
/// 2. **瞬态错误重试**(onError)
|
||||
/// 5xx / 超时 / 连接失败 → 指数退避 + jitter → 自动重试
|
||||
/// 由 [ApiConfig.maxRetries] 控制(默认 0 = 不启用)
|
||||
///
|
||||
/// 另外在 onResponse 中处理强制登出码和业务错误码。
|
||||
///
|
||||
/// 两层独立运作,可叠加。
|
||||
class RetryInterceptor extends Interceptor {
|
||||
final ApiConfig config;
|
||||
final Dio dio;
|
||||
|
||||
/// Token 刷新锁(防止多个请求同时刷新)
|
||||
bool _isRefreshing = false;
|
||||
Completer<bool>? _refreshCompleter;
|
||||
final TokenRefreshManager _tokenManager;
|
||||
|
||||
final _random = Random();
|
||||
|
||||
RetryInterceptor({required this.config, required this.dio});
|
||||
RetryInterceptor({required this.config, required this.dio})
|
||||
: _tokenManager = TokenRefreshManager(
|
||||
onTokenRefresh: config.onTokenRefresh,
|
||||
onLog: config.onLog,
|
||||
timeout: config.tokenRefreshTimeout,
|
||||
reuseWindow: config.tokenReuseWindow,
|
||||
onGetTokenExpiry: config.onGetTokenExpiry,
|
||||
proactiveRefreshThreshold: config.proactiveRefreshThreshold,
|
||||
);
|
||||
|
||||
// ── Token 刷新重试 ────────────────────────────────────────────────────────
|
||||
// ── 响应处理(Token 过期 / 强制登出 / 业务错误码)──────────────────────
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
@@ -40,13 +46,12 @@ class RetryInterceptor extends Interceptor {
|
||||
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final code = _parseCode(data['code']);
|
||||
final message = data['message'] as String? ?? '';
|
||||
final requestPath = response.requestOptions.path;
|
||||
|
||||
// 检查强制登出
|
||||
if (config.forceLogoutCodes.contains(code)) {
|
||||
config.onLog?.call(
|
||||
'Force logout detected (code: $code)',
|
||||
tag: 'Network',
|
||||
);
|
||||
config.onLog?.call('Force logout detected (code: $code)', tag: 'Network');
|
||||
config.onForceLogout?.call();
|
||||
handler.reject(
|
||||
DioException(
|
||||
@@ -58,8 +63,9 @@ class RetryInterceptor extends Interceptor {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 Token 过期
|
||||
if (config.tokenExpiredCodes.contains(code)) {
|
||||
// 检查 Token 过期(跳过已标记为 token 重试的请求,防止递归)
|
||||
if (config.tokenExpiredCodes.contains(code) &&
|
||||
response.requestOptions.extra['_isTokenRetry'] != true) {
|
||||
config.onLog?.call(
|
||||
'Token expired (code: $code), refreshing...',
|
||||
tag: 'Network',
|
||||
@@ -68,17 +74,27 @@ class RetryInterceptor extends Interceptor {
|
||||
return;
|
||||
}
|
||||
|
||||
// 业务错误码拦截:非 0 且不在特殊码集合中
|
||||
if (code != 0 && config.onBusinessError != null) {
|
||||
final handled = config.onBusinessError!(code, message, requestPath);
|
||||
if (handled) {
|
||||
// App 层已处理,正常传递响应
|
||||
handler.next(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
/// 处理 Token 过期:刷新 + 重试
|
||||
Future<void> _handleTokenExpired(
|
||||
Response response,
|
||||
ResponseInterceptorHandler handler,
|
||||
) async {
|
||||
final refreshSuccess = await _refreshToken();
|
||||
Response response,
|
||||
ResponseInterceptorHandler handler,
|
||||
) async {
|
||||
final newToken = await _tokenManager.refreshIfNeeded();
|
||||
|
||||
if (!refreshSuccess) {
|
||||
if (newToken == null) {
|
||||
config.onLog?.call('Token refresh failed', tag: 'Network');
|
||||
config.onForceLogout?.call();
|
||||
handler.reject(
|
||||
@@ -91,12 +107,14 @@ class RetryInterceptor extends Interceptor {
|
||||
return;
|
||||
}
|
||||
|
||||
// 刷新成功,用新 token 重试原请求
|
||||
// 刷新成功,更新 config 并用新 token 重试原请求
|
||||
config.updateToken(newToken);
|
||||
config.onLog?.call('Token refreshed, retrying...', tag: 'Network');
|
||||
try {
|
||||
final options = response.requestOptions;
|
||||
// 更新 header 中的 token
|
||||
options.headers['token'] = config.token;
|
||||
options.headers['token'] = newToken;
|
||||
// 标记为 token 重试请求,防止重试后再次进入 _handleTokenExpired 造成递归
|
||||
options.extra['_isTokenRetry'] = true;
|
||||
|
||||
final retryResponse = await dio.fetch(options);
|
||||
handler.resolve(retryResponse);
|
||||
@@ -105,41 +123,6 @@ class RetryInterceptor extends Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Token 刷新(串行锁)
|
||||
/// 多个请求同时过期时,只刷新一次,其余等待
|
||||
Future<bool> _refreshToken() async {
|
||||
if (_isRefreshing) {
|
||||
// 等待正在进行的刷新
|
||||
return _refreshCompleter?.future ?? Future.value(false);
|
||||
}
|
||||
|
||||
_isRefreshing = true;
|
||||
_refreshCompleter = Completer<bool>();
|
||||
|
||||
try {
|
||||
if (config.onTokenRefresh == null) {
|
||||
_refreshCompleter!.complete(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
final newToken = await config.onTokenRefresh!();
|
||||
final success = newToken != null;
|
||||
|
||||
if (success) {
|
||||
config.updateToken(newToken);
|
||||
}
|
||||
|
||||
_refreshCompleter!.complete(success);
|
||||
return success;
|
||||
} catch (e) {
|
||||
_refreshCompleter!.complete(false);
|
||||
return false;
|
||||
} finally {
|
||||
_isRefreshing = false;
|
||||
_refreshCompleter = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 瞬态错误重试(指数退避 + jitter)────────────────────────────────────
|
||||
|
||||
@override
|
||||
@@ -162,7 +145,7 @@ class RetryInterceptor extends Interceptor {
|
||||
final delayMs = _backoffDelay(attempt);
|
||||
config.onLog?.call(
|
||||
'Transient error, retry ${attempt + 1}/${config.maxRetries} '
|
||||
'in ${delayMs}ms: ${options.path}',
|
||||
'in ${delayMs}ms: ${options.path}',
|
||||
tag: 'Retry',
|
||||
);
|
||||
|
||||
@@ -184,7 +167,7 @@ class RetryInterceptor extends Interceptor {
|
||||
case DioExceptionType.connectionError:
|
||||
return true;
|
||||
case DioExceptionType.badResponse:
|
||||
// 5xx 服务端错误可重试
|
||||
// 5xx 服务端错误可重试
|
||||
final statusCode = err.response?.statusCode;
|
||||
return statusCode != null && statusCode >= 500;
|
||||
default:
|
||||
@@ -198,7 +181,9 @@ class RetryInterceptor extends Interceptor {
|
||||
int _backoffDelay(int attempt) {
|
||||
final baseMs = config.retryBaseDelay.inMilliseconds;
|
||||
final exponentialMs = min(baseMs * pow(2, attempt).toInt(), 30000);
|
||||
final jitterMs = _random.nextInt((exponentialMs * 0.25).toInt().clamp(1, 7500));
|
||||
final jitterMs = _random.nextInt(
|
||||
(exponentialMs * 0.25).toInt().clamp(1, 7500),
|
||||
);
|
||||
return exponentialMs + jitterMs;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||
|
||||
/// Token 刷新管理器
|
||||
///
|
||||
/// 两种刷新模式:
|
||||
///
|
||||
/// 1. **被动刷新**([refreshIfNeeded])— 拦截器检测到 token 过期后调用
|
||||
/// 2. **主动刷新**([proactivelyRefreshIfNeeded])— 解析 JWT exp,
|
||||
/// 距过期不足阈值时提前刷新,避免带过期 token 发请求
|
||||
///
|
||||
/// 两种模式共享串行锁和时间窗口保护:
|
||||
/// - **串行锁** — 同一时刻只执行一次刷新,其余请求等待同一 Completer
|
||||
/// - **时间窗口** — 刷新成功后 [reuseWindow] 内再次调用直接返回缓存 token
|
||||
/// - **超时保护** — 刷新回调超过 [timeout] 自动失败,防止死锁
|
||||
class TokenRefreshManager {
|
||||
final OnTokenRefresh? onTokenRefresh;
|
||||
final OnLog? onLog;
|
||||
|
||||
/// 刷新超时时间(防止 onTokenRefresh 卡住导致所有请求阻塞)
|
||||
final Duration timeout;
|
||||
|
||||
/// 时间窗口:刷新成功后此时间内再次调用直接返回缓存 token
|
||||
final Duration reuseWindow;
|
||||
|
||||
/// Token 过期时间解析(App 层注入 JWT exp 解析逻辑)
|
||||
final OnGetTokenExpiry? onGetTokenExpiry;
|
||||
|
||||
/// 主动刷新阈值:距过期不足此时间时提前刷新(默认 1 小时)
|
||||
final Duration proactiveRefreshThreshold;
|
||||
|
||||
/// 当前正在进行的刷新任务(null = 空闲)
|
||||
Completer<String?>? _completer;
|
||||
|
||||
/// 上次刷新成功的时间戳
|
||||
DateTime? _lastRefreshTime;
|
||||
|
||||
/// 上次刷新成功的 token(时间窗口内复用)
|
||||
String? _lastToken;
|
||||
|
||||
TokenRefreshManager({
|
||||
this.onTokenRefresh,
|
||||
this.onLog,
|
||||
this.timeout = const Duration(seconds: 10),
|
||||
this.reuseWindow = const Duration(seconds: 3),
|
||||
this.onGetTokenExpiry,
|
||||
this.proactiveRefreshThreshold = const Duration(hours: 1),
|
||||
});
|
||||
|
||||
/// 执行 token 刷新(如果需要)
|
||||
///
|
||||
/// 返回新 token(刷新成功或在时间窗口内),
|
||||
/// 返回 null = 刷新失败或超时。
|
||||
Future<String?> refreshIfNeeded() async {
|
||||
// 1. 时间窗口:最近刷新过且未超时 → 直接返回缓存的 token
|
||||
if (_isWithinReuseWindow()) {
|
||||
_log('Token refreshed recently, reusing');
|
||||
return _lastToken;
|
||||
}
|
||||
|
||||
// 2. 有正在进行的刷新 → 等待同一 Completer
|
||||
final existing = _completer;
|
||||
if (existing != null) {
|
||||
_log('Waiting for ongoing token refresh');
|
||||
return existing.future;
|
||||
}
|
||||
|
||||
// 3. 发起新的刷新
|
||||
if (onTokenRefresh == null) {
|
||||
_log('No onTokenRefresh callback configured');
|
||||
return null;
|
||||
}
|
||||
|
||||
final completer = Completer<String?>();
|
||||
_completer = completer;
|
||||
|
||||
try {
|
||||
final newToken = await onTokenRefresh!().timeout(
|
||||
timeout,
|
||||
onTimeout: () {
|
||||
_log('Token refresh timed out after ${timeout.inSeconds}s');
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
final success = newToken != null && newToken.isNotEmpty;
|
||||
|
||||
if (success) {
|
||||
_lastRefreshTime = DateTime.now();
|
||||
_lastToken = newToken;
|
||||
_log('Token refreshed successfully');
|
||||
} else {
|
||||
_log('Token refresh failed (null or empty token)');
|
||||
}
|
||||
|
||||
// 先 complete 再清引用,确保等待者能拿到结果
|
||||
completer.complete(success ? newToken : null);
|
||||
return success ? newToken : null;
|
||||
} catch (e) {
|
||||
_log('Token refresh error: $e');
|
||||
completer.complete(null);
|
||||
return null;
|
||||
} finally {
|
||||
// 清理引用(Completer 已 complete,等待者不受影响)
|
||||
_completer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查 token 是否即将过期,是则主动刷新
|
||||
///
|
||||
/// 解析 [currentToken] 的过期时间,距过期不足 [proactiveRefreshThreshold]
|
||||
/// 时调用 [refreshIfNeeded] 刷新。复用串行锁和超时保护。
|
||||
///
|
||||
/// 返回新 token(已刷新)或 null(不需要刷新 / 刷新失败 / 无法解析过期时间)。
|
||||
Future<String?> proactivelyRefreshIfNeeded(String? currentToken) async {
|
||||
if (currentToken == null || onGetTokenExpiry == null) return null;
|
||||
|
||||
final expiry = onGetTokenExpiry!(currentToken);
|
||||
if (expiry == null) return null;
|
||||
|
||||
final remaining = expiry.difference(DateTime.now());
|
||||
if (remaining > proactiveRefreshThreshold) {
|
||||
_log(
|
||||
'Token valid (expires in ${remaining.inMinutes}min), skip proactive refresh',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
_log(
|
||||
'Token expiring soon (${remaining.inMinutes}min left), proactively refreshing',
|
||||
);
|
||||
return refreshIfNeeded();
|
||||
}
|
||||
|
||||
/// 重置状态(登出时调用)
|
||||
void reset() {
|
||||
_lastRefreshTime = null;
|
||||
_lastToken = null;
|
||||
// 不清理 _completer,让正在等待的请求正常结束
|
||||
}
|
||||
|
||||
bool _isWithinReuseWindow() {
|
||||
final lastTime = _lastRefreshTime;
|
||||
if (lastTime == null) return false;
|
||||
return DateTime.now().difference(lastTime) < reuseWindow;
|
||||
}
|
||||
|
||||
void _log(String message) {
|
||||
onLog?.call(message, tag: 'TokenRefresh');
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/data/datasources/http/api_client.dart';
|
||||
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/api_error.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/api_request_type.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||
import '../../../networks_sdk_platform_interface.dart';
|
||||
import '../../domain/entities/http_method.dart';
|
||||
|
||||
class NetworksSdkMethodChannelDataSource
|
||||
{
|
||||
/// 网络层数据源
|
||||
///
|
||||
/// 封装 [ApiClient],提供两种请求入口:
|
||||
/// - [executeRequest] — 统一请求入口(标准 / Upload / 流式)
|
||||
/// - [executeDownload] — 带进度的文件下载(支持断点续传)
|
||||
///
|
||||
/// 流式(SSE)请求也走 [executeRequest],由业务 Request 类 override
|
||||
/// `decodeResponse` 处理 SSE 解析。SDK 内部根据
|
||||
/// `requestType == ApiRequestType.stream` 自动切换 `ResponseType.plain`。
|
||||
class NetworksSdkMethodChannelDataSource {
|
||||
final NetworksSdkPlatform platform;
|
||||
|
||||
late ApiClient apiClient;
|
||||
@@ -16,44 +27,51 @@ class NetworksSdkMethodChannelDataSource
|
||||
NetworksSdkMethodChannelDataSource(this.platform);
|
||||
|
||||
Future<String?> getPlatformVersion() async {
|
||||
return await getPlatformVersion();
|
||||
return await platform.getPlatformVersion();
|
||||
}
|
||||
|
||||
void initialize(ApiConfig apiConfig){
|
||||
void initialize(ApiConfig apiConfig) {
|
||||
apiClient = ApiClient(config: apiConfig);
|
||||
}
|
||||
|
||||
/// 执行 API 请求 — 唯一入口
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 统一请求入口
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// 执行 API 请求 — 统一入口
|
||||
///
|
||||
/// 流程:网络前置检查 → 构建 URL → 设置元数据 → 执行请求 → 解码响应 → 错误映射
|
||||
/// 拦截器负责:header 注入、Token 刷新重试、日志
|
||||
/// 支持三种请求类型,由 `request.requestType` 控制行为:
|
||||
/// - `request` / `login` — 标准 JSON 请求
|
||||
/// - `upload` — 文件上传(FormData / 二进制)
|
||||
/// - `stream` — SSE / chunked,内部用 `ResponseType.plain` 获取原始文本,
|
||||
/// 由业务 Request 类 override `decodeResponse` 处理 SSE 解析
|
||||
///
|
||||
/// 流程:网络前置检查 → 构建 URL → 设置元数据 → 执行请求
|
||||
/// → 响应变换(可选,stream 类型跳过)→ 解码响应 → 错误映射
|
||||
///
|
||||
/// 拦截器负责:header 注入、加密/解密、Token 刷新重试、业务错误拦截、日志
|
||||
///
|
||||
/// Upload 类型支持两种模式:
|
||||
/// - 自有后端上传:path 为相对路径,自动拼接 baseURL
|
||||
/// - S3 presigned URL:path 以 http 开头,直接使用全路径
|
||||
Future<T?> executeRequest<T>(ApiRequestable<T> request) async {
|
||||
// 前置检查:网络不可用时直接抛错,避免无效请求
|
||||
if (apiClient.config.onCheckNetworkAvailable != null) {
|
||||
final available = await apiClient.config.onCheckNetworkAvailable!();
|
||||
if (!available) {
|
||||
apiClient.config.onLog?.call(
|
||||
'Network unavailable, abort request: ${request.path}',
|
||||
tag: 'ApiClient',
|
||||
);
|
||||
throw const ApiError.noNetworkConnection();
|
||||
}
|
||||
}
|
||||
Future<T?> executeRequest<T>(
|
||||
ApiRequestable<T> request, {
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
await _checkNetwork(request.path);
|
||||
|
||||
try {
|
||||
// Upload 且 path 以 http 开头 → 直接用全路径(S3 presigned URL)
|
||||
// 否则 → 拼接 baseURL
|
||||
final isUpload = request.requestType == ApiRequestType.upload;
|
||||
final isStream = request.requestType == ApiRequestType.stream;
|
||||
final path = request.path;
|
||||
final url = (isUpload && path.startsWith('http')) ? path : '${apiClient.config.baseURL}$path';
|
||||
final url = (isUpload && path.startsWith('http'))
|
||||
? path
|
||||
: '${apiClient.config.baseURL}$path';
|
||||
|
||||
// 将请求元数据写入 extra,供拦截器读取
|
||||
final options = Options(
|
||||
method: request.method.value,
|
||||
// 流式请求用 plain,Dio 返回原始文本,由 decodeResponse 解析 SSE
|
||||
responseType: isStream ? ResponseType.plain : null,
|
||||
extra: {
|
||||
'requestType': request.requestType,
|
||||
'includeToken': request.includeToken,
|
||||
@@ -62,19 +80,22 @@ class NetworksSdkMethodChannelDataSource
|
||||
);
|
||||
|
||||
// 访问 parameters 触发代码生成器的 fromJson 注册
|
||||
// (@ApiRequest 生成的 mixin 在 parameters getter 中注册响应类型)
|
||||
final params = request.parameters;
|
||||
|
||||
// GET → queryParameters;POST/PUT/DELETE/PATCH → JSON body;Upload → uploadData
|
||||
final isGet = request.method == HttpMethod.get;
|
||||
final response = await apiClient.dio.request(
|
||||
url,
|
||||
data: isUpload ? request.uploadData : (isGet ? null : params),
|
||||
queryParameters: isGet ? params : null,
|
||||
options: options,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
|
||||
// 解码响应(Upload 类型通常需要 override decodeResponse)
|
||||
// 响应变换:stream 类型由 decodeResponse 自行处理,不做变换
|
||||
if (!isStream) {
|
||||
_applyResponseTransform(response);
|
||||
}
|
||||
|
||||
return request.decodeResponse(response);
|
||||
} on DioException catch (e) {
|
||||
throw apiClient.mapDioError(e);
|
||||
@@ -85,4 +106,162 @@ class NetworksSdkMethodChannelDataSource
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 文件下载
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// 下载文件到本地路径
|
||||
///
|
||||
/// 支持进度回调和断点续传(通过 HTTP Range header 实现)。
|
||||
///
|
||||
/// 非续传模式直接用 Dio.download(高效,内部流式写入)。
|
||||
/// 续传模式用 stream + FileMode.append,因为 Dio.download 始终从
|
||||
/// 文件头部写入,无法正确追加到已下载部分之后。
|
||||
///
|
||||
/// [url] — 下载 URL(完整路径或相对路径,相对路径自动拼接 baseURL)
|
||||
/// [savePath] — 本地保存路径
|
||||
/// [onProgress] — 下载进度回调
|
||||
/// [cancelToken] — 取消令牌
|
||||
/// [resume] — 是否断点续传(文件已存在时从断点继续下载)
|
||||
/// [headers] — 额外请求头
|
||||
Future<void> executeDownload({
|
||||
required String url,
|
||||
required String savePath,
|
||||
OnDownloadProgress? onProgress,
|
||||
CancelToken? cancelToken,
|
||||
bool resume = false,
|
||||
Map<String, String>? headers,
|
||||
}) async {
|
||||
await _checkNetwork(url);
|
||||
|
||||
try {
|
||||
final fullUrl = url.startsWith('http')
|
||||
? url
|
||||
: '${apiClient.config.baseURL}$url';
|
||||
|
||||
final extraHeaders = <String, String>{};
|
||||
if (headers != null) extraHeaders.addAll(headers);
|
||||
|
||||
// 断点续传:读取已下载部分的大小,设置 Range header
|
||||
int startBytes = 0;
|
||||
if (resume) {
|
||||
final file = File(savePath);
|
||||
if (file.existsSync()) {
|
||||
startBytes = file.lengthSync();
|
||||
extraHeaders['Range'] = 'bytes=$startBytes-';
|
||||
}
|
||||
}
|
||||
|
||||
if (resume && startBytes > 0) {
|
||||
// 续传模式:stream + append,确保新数据追加到文件末尾
|
||||
await _downloadWithResume(
|
||||
url: fullUrl,
|
||||
savePath: savePath,
|
||||
startBytes: startBytes,
|
||||
headers: extraHeaders,
|
||||
onProgress: onProgress,
|
||||
cancelToken: cancelToken,
|
||||
);
|
||||
} else {
|
||||
// 普通下载:Dio.download(高效,内部流式写入)
|
||||
await apiClient.dio.download(
|
||||
fullUrl,
|
||||
savePath,
|
||||
cancelToken: cancelToken,
|
||||
deleteOnError: true,
|
||||
options: Options(
|
||||
headers: extraHeaders.isEmpty ? null : extraHeaders,
|
||||
extra: {
|
||||
'requestType': ApiRequestType.download,
|
||||
'includeToken': true,
|
||||
},
|
||||
),
|
||||
onReceiveProgress: onProgress != null
|
||||
? (received, total) => onProgress(received, total)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
throw apiClient.mapDioError(e);
|
||||
} on ApiError {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw ApiError.unknown(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// 断点续传下载:stream 响应 + FileMode.append
|
||||
///
|
||||
/// Dio.download 内部用 FileMode.write(从头覆盖),无法正确续传。
|
||||
/// 这里手动读流并追加写入文件。
|
||||
Future<void> _downloadWithResume({
|
||||
required String url,
|
||||
required String savePath,
|
||||
required int startBytes,
|
||||
required Map<String, String> headers,
|
||||
OnDownloadProgress? onProgress,
|
||||
CancelToken? cancelToken,
|
||||
}) async {
|
||||
final response = await apiClient.dio.get<ResponseBody>(
|
||||
url,
|
||||
cancelToken: cancelToken,
|
||||
options: Options(
|
||||
responseType: ResponseType.stream,
|
||||
headers: headers.isEmpty ? null : headers,
|
||||
extra: {'requestType': ApiRequestType.download, 'includeToken': true},
|
||||
),
|
||||
);
|
||||
|
||||
final stream = response.data?.stream;
|
||||
if (stream == null) return;
|
||||
|
||||
// Content-Length 是本次传输量(不含已下载部分)
|
||||
final contentLength =
|
||||
int.tryParse(response.headers.value('content-length') ?? '') ?? -1;
|
||||
final totalBytes = contentLength > 0 ? contentLength + startBytes : -1;
|
||||
|
||||
final file = File(savePath);
|
||||
final raf = file.openSync(mode: FileMode.writeOnlyAppend);
|
||||
int received = startBytes;
|
||||
|
||||
try {
|
||||
await for (final chunk in stream) {
|
||||
raf.writeFromSync(chunk);
|
||||
received += chunk.length;
|
||||
onProgress?.call(received, totalBytes);
|
||||
}
|
||||
} finally {
|
||||
raf.closeSync();
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 内部辅助
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// 网络前置检查,不可用时直接抛 [ApiError.noNetworkConnection]
|
||||
Future<void> _checkNetwork(String path) async {
|
||||
if (apiClient.config.onCheckNetworkAvailable != null) {
|
||||
final available = await apiClient.config.onCheckNetworkAvailable!();
|
||||
if (!available) {
|
||||
apiClient.config.onLog?.call(
|
||||
'Network unavailable, abort request: $path',
|
||||
tag: 'ApiClient',
|
||||
);
|
||||
throw const ApiError.noNetworkConnection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 应用响应变换(如果 App 层注入了 onTransformResponse)
|
||||
void _applyResponseTransform(Response response) {
|
||||
final transform = apiClient.config.onTransformResponse;
|
||||
if (transform == null) return;
|
||||
if (response.data is! Map<String, dynamic>) return;
|
||||
|
||||
final transformed = transform(response.data as Map<String, dynamic>);
|
||||
if (transformed != null) {
|
||||
response.data = transformed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io' as io;
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
@@ -9,10 +11,12 @@ import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
/// WebSocket 长连接客户端
|
||||
///
|
||||
/// 提供:
|
||||
/// - 连接 / 断连 / 自动重连(指数退避)
|
||||
/// - 连接 / 断连 / 自动重连(指数退避,支持无限重连)
|
||||
/// - 双层心跳(底层 ping + 应用层 heartbeat)
|
||||
/// - Stream 输出(消息、连接状态、错误)
|
||||
/// - Stream 输出(JSON 消息、原始字符串、二进制、连接状态、错误)
|
||||
/// - 生命周期感知(前后台切换)
|
||||
/// - Token 热更新(不断连)
|
||||
/// - 消息加密/解密钩子(预留给 cipher_guard_sdk)
|
||||
///
|
||||
/// ## 使用方式
|
||||
///
|
||||
@@ -28,6 +32,9 @@ import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
/// // 发消息
|
||||
/// await client.send({'type': 'chat', 'data': {...}});
|
||||
///
|
||||
/// // Token 热更新(不断连,下次重连自动使用新 token)
|
||||
/// client.updateToken('new_token');
|
||||
///
|
||||
/// // 断连
|
||||
/// await client.disconnect();
|
||||
/// ```
|
||||
@@ -56,8 +63,9 @@ class SocketClient {
|
||||
// ── Stream Controllers ──
|
||||
final _messageController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _rawMessageController = StreamController<String>.broadcast();
|
||||
final _binaryMessageController = StreamController<Uint8List>.broadcast();
|
||||
final _connectionStateController =
|
||||
StreamController<SocketConnectionState>.broadcast();
|
||||
StreamController<SocketConnectionState>.broadcast();
|
||||
final _errorController = StreamController<SocketError>.broadcast();
|
||||
|
||||
SocketClient({required this.config});
|
||||
@@ -93,12 +101,20 @@ class SocketClient {
|
||||
}
|
||||
|
||||
/// 当前是否已连接
|
||||
bool get isConnected =>
|
||||
_connectionState == SocketConnectionState.connected;
|
||||
bool get isConnected => _connectionState == SocketConnectionState.connected;
|
||||
|
||||
/// 当前连接状态
|
||||
SocketConnectionState get connectionState => _connectionState;
|
||||
|
||||
/// Token 热更新(不断开连接)
|
||||
///
|
||||
/// 仅更新内部持有的 token,下次重连时自动使用新 token。
|
||||
/// 适用于 token 刷新后同步到 WebSocket 的场景。
|
||||
void updateToken(String token) {
|
||||
_currentToken = token;
|
||||
_log('Token updated (no reconnect)');
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// 公开 API — 发送
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
@@ -109,6 +125,8 @@ class SocketClient {
|
||||
}
|
||||
|
||||
/// 发送原始字符串
|
||||
///
|
||||
/// 如果配置了 [SocketConfig.onEncryptMessage],发送前自动加密。
|
||||
Future<bool> sendString(String message) async {
|
||||
if (!isConnected || _channel == null) {
|
||||
_emitError(SocketError.sendFailed('Not connected'));
|
||||
@@ -116,7 +134,27 @@ class SocketClient {
|
||||
}
|
||||
|
||||
try {
|
||||
_channel!.sink.add(message);
|
||||
String payload = message;
|
||||
if (config.onEncryptMessage != null) {
|
||||
payload = await config.onEncryptMessage!(message);
|
||||
}
|
||||
_channel!.sink.add(payload);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_emitError(SocketError.sendFailed(e.toString()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送二进制数据
|
||||
Future<bool> sendBytes(List<int> bytes) async {
|
||||
if (!isConnected || _channel == null) {
|
||||
_emitError(SocketError.sendFailed('Not connected'));
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
_channel!.sink.add(bytes);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_emitError(SocketError.sendFailed(e.toString()));
|
||||
@@ -134,6 +172,9 @@ class SocketClient {
|
||||
/// 原始字符串消息流(JSON 解析失败的也走这里)
|
||||
Stream<String> get rawMessageStream => _rawMessageController.stream;
|
||||
|
||||
/// 二进制消息流
|
||||
Stream<Uint8List> get binaryMessageStream => _binaryMessageController.stream;
|
||||
|
||||
/// 连接状态变化流
|
||||
Stream<SocketConnectionState> get connectionStateStream =>
|
||||
_connectionStateController.stream;
|
||||
@@ -171,6 +212,7 @@ class SocketClient {
|
||||
await _doDisconnect(reason: 'Dispose');
|
||||
await _messageController.close();
|
||||
await _rawMessageController.close();
|
||||
await _binaryMessageController.close();
|
||||
await _connectionStateController.close();
|
||||
await _errorController.close();
|
||||
}
|
||||
@@ -197,24 +239,36 @@ class SocketClient {
|
||||
_log('Connecting to $url');
|
||||
|
||||
try {
|
||||
// 构建最终 URL(拼接 token)
|
||||
final connectUri = _currentToken != null
|
||||
? uri.replace(
|
||||
queryParameters: {
|
||||
...uri.queryParameters,
|
||||
'token': _currentToken!,
|
||||
},
|
||||
)
|
||||
: uri;
|
||||
// 构建最终连接 URL
|
||||
//
|
||||
// 有 onBuildConnectUrl 回调时,App 层完全控制 URL(路径加密、
|
||||
// token 加密、添加 cipher 参数等)。
|
||||
// 无回调时使用默认行为:URL 后追加 ?token=xxx。
|
||||
final String connectUrlString;
|
||||
|
||||
// 创建 WebSocket 连接
|
||||
_channel = IOWebSocketChannel.connect(
|
||||
connectUri,
|
||||
pingInterval: config.pingInterval,
|
||||
);
|
||||
if (config.onBuildConnectUrl != null) {
|
||||
connectUrlString = config.onBuildConnectUrl!(url, _currentToken);
|
||||
} else {
|
||||
final connectUri = _currentToken != null
|
||||
? uri.replace(
|
||||
queryParameters: {
|
||||
...uri.queryParameters,
|
||||
'token': _currentToken!,
|
||||
},
|
||||
)
|
||||
: uri;
|
||||
connectUrlString = connectUri.toString();
|
||||
}
|
||||
|
||||
// 等待连接就绪
|
||||
await _channel!.ready.timeout(config.connectTimeout);
|
||||
// 创建 WebSocket 连接(通过 dart:io 支持压缩选项)
|
||||
final rawSocket = await io.WebSocket.connect(
|
||||
connectUrlString,
|
||||
compression: config.enableCompression
|
||||
? io.CompressionOptions.compressionDefault
|
||||
: io.CompressionOptions.compressionOff,
|
||||
).timeout(config.connectTimeout);
|
||||
rawSocket.pingInterval = config.pingInterval;
|
||||
_channel = IOWebSocketChannel(rawSocket);
|
||||
|
||||
_log('Connected');
|
||||
_updateConnectionState(SocketConnectionState.connected);
|
||||
@@ -270,26 +324,45 @@ class SocketClient {
|
||||
// 内部 — 消息处理
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void _handleMessage(dynamic data) {
|
||||
void _handleMessage(dynamic data) async {
|
||||
// 二进制消息
|
||||
if (data is List<int>) {
|
||||
_binaryMessageController.add(
|
||||
data is Uint8List ? data : Uint8List.fromList(data),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data is! String) {
|
||||
// 非字符串消息(如二进制),走 rawMessageStream
|
||||
_rawMessageController.add(data.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 pong 心跳回复
|
||||
if (data == 'pong') {
|
||||
// 解密(如果配置了解密回调)
|
||||
String text = data;
|
||||
if (config.onDecryptMessage != null) {
|
||||
try {
|
||||
text = await config.onDecryptMessage!(data);
|
||||
} catch (e) {
|
||||
_log('Message decryption failed: $e');
|
||||
_rawMessageController.add(data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 pong 心跳回复(解密后检查,加密场景下也能正确匹配)
|
||||
if (text == 'pong') {
|
||||
_onPongReceived();
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试 JSON 解析
|
||||
try {
|
||||
final json = jsonDecode(data) as Map<String, dynamic>;
|
||||
final json = jsonDecode(text) as Map<String, dynamic>;
|
||||
_messageController.add(json);
|
||||
} catch (_) {
|
||||
// JSON 解析失败,走原始消息流
|
||||
_rawMessageController.add(data);
|
||||
_rawMessageController.add(text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,11 +401,21 @@ class SocketClient {
|
||||
_waitingForPong = false;
|
||||
}
|
||||
|
||||
void _sendPing() {
|
||||
void _sendPing() async {
|
||||
if (_waitingForPong) return;
|
||||
|
||||
_waitingForPong = true;
|
||||
_channel?.sink.add('ping');
|
||||
|
||||
// 加密场景下 ping 也要加密,与 pong 解密对称
|
||||
String pingPayload = 'ping';
|
||||
if (config.onEncryptMessage != null) {
|
||||
try {
|
||||
pingPayload = await config.onEncryptMessage!('ping');
|
||||
} catch (e) {
|
||||
_log('Ping encryption failed: $e');
|
||||
}
|
||||
}
|
||||
_channel?.sink.add(pingPayload);
|
||||
|
||||
// 启动 pong 超时计时器
|
||||
_pongTimeoutTimer = Timer(config.pongTimeout, () {
|
||||
@@ -360,7 +443,9 @@ class SocketClient {
|
||||
if (_manualDisconnect || !config.autoReconnect || _isBackground) return;
|
||||
if (_connectionState == SocketConnectionState.reconnecting) return;
|
||||
|
||||
if (_reconnectAttempts >= config.maxReconnectAttempts) {
|
||||
// 非无限重连模式下检查重连次数上限
|
||||
if (!config.unlimitedReconnect &&
|
||||
_reconnectAttempts >= config.maxReconnectAttempts) {
|
||||
_log('Max reconnect attempts reached ($_reconnectAttempts)');
|
||||
_reconnectAttempts = 0;
|
||||
return;
|
||||
@@ -375,11 +460,16 @@ class SocketClient {
|
||||
pow(2, _reconnectAttempts).toInt() * 1000,
|
||||
config.maxReconnectDelay.inMilliseconds,
|
||||
);
|
||||
final jitterMs = _random.nextInt((baseDelayMs * 0.25).toInt().clamp(1, 7500));
|
||||
final jitterMs = _random.nextInt(
|
||||
(baseDelayMs * 0.25).toInt().clamp(1, 7500),
|
||||
);
|
||||
final delay = Duration(milliseconds: baseDelayMs + jitterMs);
|
||||
|
||||
_log('Reconnecting in ${delay.inMilliseconds}ms '
|
||||
'(attempt $_reconnectAttempts/${config.maxReconnectAttempts})');
|
||||
final attemptsInfo = config.unlimitedReconnect
|
||||
? 'attempt $_reconnectAttempts/unlimited'
|
||||
: 'attempt $_reconnectAttempts/${config.maxReconnectAttempts}';
|
||||
|
||||
_log('Reconnecting in ${delay.inMilliseconds}ms ($attemptsInfo)');
|
||||
|
||||
_reconnectTimer = Timer(delay, () async {
|
||||
// 重连前检查网络
|
||||
@@ -393,6 +483,17 @@ class SocketClient {
|
||||
}
|
||||
}
|
||||
|
||||
// 重连前回调(App 层刷新即将过期的 token 等)
|
||||
if (config.onBeforeReconnect != null) {
|
||||
try {
|
||||
await config.onBeforeReconnect!();
|
||||
} catch (e) {
|
||||
_log('onBeforeReconnect failed: $e, skip this reconnect');
|
||||
_startReconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_doConnect();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:networks_sdk/src/data/datasources/socket/socket_client.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
||||
import 'package:networks_sdk/src/domain/repositories/networks_messaging_repository.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
||||
|
||||
/// Messaging Repository Implementation (Data)
|
||||
/// [NetworksMessagingRepository] 的实现,透传给 [SocketClient]
|
||||
class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
||||
SocketClient? _socketClient;
|
||||
bool _isInitialized = false;
|
||||
@@ -47,6 +49,12 @@ class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
||||
return _socketClient!.connectionState;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateToken(String token) {
|
||||
_checkInitialized();
|
||||
_socketClient!.updateToken(token);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> send(Map<String, dynamic> message) {
|
||||
_checkInitialized();
|
||||
@@ -59,6 +67,12 @@ class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
||||
return _socketClient!.sendString(message);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> sendBytes(List<int> bytes) {
|
||||
_checkInitialized();
|
||||
return _socketClient!.sendBytes(bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Map<String, dynamic>> get messageStream {
|
||||
_checkInitialized();
|
||||
@@ -71,6 +85,12 @@ class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
||||
return _socketClient!.rawMessageStream;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> get binaryMessageStream {
|
||||
_checkInitialized();
|
||||
return _socketClient!.binaryMessageStream;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<SocketConnectionState> get connectionStateStream {
|
||||
_checkInitialized();
|
||||
@@ -104,4 +124,3 @@ class NetworksMessagingRepositoryImpl implements NetworksMessagingRepository {
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
//Repository Impl
|
||||
// Repository Impl
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||
|
||||
import '../../domain/repositories/networks_sdk_repository.dart';
|
||||
import '../datasources/networks_sdk_method_channel_datasource.dart';
|
||||
|
||||
class NetworksSdkRepositoryImpl implements NetworksSdkRepository
|
||||
{
|
||||
/// [NetworksSdkRepository] 的实现,透传给 [NetworksSdkMethodChannelDataSource]
|
||||
class NetworksSdkRepositoryImpl implements NetworksSdkRepository {
|
||||
final NetworksSdkMethodChannelDataSource _datasource;
|
||||
|
||||
const NetworksSdkRepositoryImpl(this._datasource);
|
||||
@@ -18,12 +20,34 @@ class NetworksSdkRepositoryImpl implements NetworksSdkRepository
|
||||
}
|
||||
|
||||
@override
|
||||
void initialize(ApiConfig apiConfig){
|
||||
_datasource.initialize(apiConfig);
|
||||
void initialize(ApiConfig apiConfig) {
|
||||
_datasource.initialize(apiConfig);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<T?> executeRequest<T>(ApiRequestable<T> request) {
|
||||
return _datasource.executeRequest(request);
|
||||
Future<T?> executeRequest<T>(
|
||||
ApiRequestable<T> request, {
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return _datasource.executeRequest(request, cancelToken: cancelToken);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> executeDownload({
|
||||
required String url,
|
||||
required String savePath,
|
||||
OnDownloadProgress? onProgress,
|
||||
CancelToken? cancelToken,
|
||||
bool resume = false,
|
||||
Map<String, String>? headers,
|
||||
}) {
|
||||
return _datasource.executeDownload(
|
||||
url: url,
|
||||
savePath: savePath,
|
||||
onProgress: onProgress,
|
||||
cancelToken: cancelToken,
|
||||
resume: resume,
|
||||
headers: headers,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ class ApiError with _$ApiError implements Exception {
|
||||
required int code,
|
||||
required String message,
|
||||
}) = _ApiError;
|
||||
|
||||
/// 请求被 CancelToken 取消
|
||||
const factory ApiError.cancelled() = _Cancelled;
|
||||
const factory ApiError.unknown(String? message) = _Unknown;
|
||||
}
|
||||
|
||||
@@ -25,7 +28,8 @@ extension ApiErrorExtension on ApiError {
|
||||
networkError: (message) => 'Network error: $message',
|
||||
decodingError: (message) => 'Decoding error: $message',
|
||||
apiError: (code, message) => message,
|
||||
cancelled: () => 'Request cancelled',
|
||||
unknown: (message) => message ?? 'Unknown error',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,13 @@ enum ApiRequestType {
|
||||
|
||||
/// 文件上传(multipart,不序列化 parameters)
|
||||
upload,
|
||||
|
||||
/// 流式请求(SSE / chunked)
|
||||
///
|
||||
/// SDK 内部自动切换 `ResponseType.plain`,Dio 返回原始文本。
|
||||
/// 业务 Request 类 override `decodeResponse` 处理 SSE 解析。
|
||||
stream,
|
||||
|
||||
/// 文件下载(带进度回调,支持断点续传)
|
||||
download,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/// HTTP 请求加密结果
|
||||
///
|
||||
/// 加密回调返回此对象,[EncryptionInterceptor] 根据非 null 字段覆盖原始请求。
|
||||
/// 未设置的字段保持原值不变。
|
||||
///
|
||||
/// 设计依据:HTTP 加密模式下,加密后需要同时修改:
|
||||
/// - 路径(原文 path 加密为 hex 编码)
|
||||
/// - 请求体(JSON body 加密为 base64 字符串,不再是 Map)
|
||||
/// - Headers(添加 X-Token、X-Signature、secret-key 等加密 header)
|
||||
/// - Content-Type(JSON → text/plain)
|
||||
///
|
||||
/// ```dart
|
||||
/// // 加密回调返回示例
|
||||
/// EncryptedRequest(
|
||||
/// path: '/api/${hexEncode(encrypt(originalPath))}',
|
||||
/// body: base64Encode(encrypt(jsonBody)),
|
||||
/// headers: {
|
||||
/// 'X-Token': encryptedToken,
|
||||
/// 'X-Signature': signature,
|
||||
/// 'secret-key': clientPublicKey,
|
||||
/// },
|
||||
/// contentType: 'text/plain',
|
||||
/// )
|
||||
/// ```
|
||||
class EncryptedRequest {
|
||||
/// 加密后的路径
|
||||
///
|
||||
/// null 表示不修改路径。
|
||||
/// 如需加密,拦截器会用此值替换 `RequestOptions.path`。
|
||||
final String? path;
|
||||
|
||||
/// 加密后的请求体
|
||||
///
|
||||
/// null 表示不修改 body。
|
||||
/// 类型不限于 Map — 加密后通常是 base64 字符串或 bytes。
|
||||
final Object? body;
|
||||
|
||||
/// 需要添加或覆盖的 headers
|
||||
///
|
||||
/// null 表示不修改 headers。
|
||||
/// 拦截器会将这些 header 合并到请求中(覆盖同名 header)。
|
||||
final Map<String, String>? headers;
|
||||
|
||||
/// 覆盖 Content-Type
|
||||
///
|
||||
/// null 表示不修改。加密后通常是 `text/plain`(body 已是字符串,非 JSON)。
|
||||
final String? contentType;
|
||||
|
||||
const EncryptedRequest({
|
||||
this.path,
|
||||
this.body,
|
||||
this.headers,
|
||||
this.contentType,
|
||||
});
|
||||
}
|
||||
@@ -1,49 +1,45 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
||||
|
||||
/// Messaging Repository Interface (Domain)
|
||||
/// Messaging Repository 接口
|
||||
abstract class NetworksMessagingRepository {
|
||||
/// Initialize with config
|
||||
void initialize(SocketConfig config);
|
||||
|
||||
/// Connect to messaging server
|
||||
Future<bool> connect(String url, {String? token});
|
||||
|
||||
/// Disconnect from server
|
||||
Future<void> disconnect();
|
||||
|
||||
/// Check if connected
|
||||
bool get isConnected;
|
||||
|
||||
/// Current connection state
|
||||
SocketConnectionState get connectionState;
|
||||
|
||||
/// Send a JSON message
|
||||
/// Token 热更新(不断连)
|
||||
void updateToken(String token);
|
||||
|
||||
Future<bool> send(Map<String, dynamic> message);
|
||||
|
||||
/// Send a raw string message
|
||||
Future<bool> sendString(String message);
|
||||
|
||||
/// Stream of incoming parsed JSON messages
|
||||
/// 发送二进制数据
|
||||
Future<bool> sendBytes(List<int> bytes);
|
||||
|
||||
Stream<Map<String, dynamic>> get messageStream;
|
||||
|
||||
/// Stream of raw string messages
|
||||
Stream<String> get rawMessageStream;
|
||||
|
||||
/// Stream of connection state changes
|
||||
/// 二进制消息流
|
||||
Stream<Uint8List> get binaryMessageStream;
|
||||
|
||||
Stream<SocketConnectionState> get connectionStateStream;
|
||||
|
||||
/// Stream of errors
|
||||
Stream<SocketError> get errorStream;
|
||||
|
||||
/// Called when app enters foreground
|
||||
void onEnterForeground();
|
||||
|
||||
/// Called when app enters background
|
||||
void onEnterBackground();
|
||||
|
||||
/// Dispose all resources
|
||||
Future<void> dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,32 @@
|
||||
// Repository Interface(Domain)
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||
|
||||
/// 网络层 Repository 接口
|
||||
///
|
||||
/// 定义两种请求入口:
|
||||
/// - [executeRequest] — 统一请求入口(标准 / Upload / 流式)
|
||||
/// - [executeDownload] — 带进度的文件下载(支持断点续传)
|
||||
abstract class NetworksSdkRepository {
|
||||
Future<String?> platformVersion();
|
||||
|
||||
void initialize(ApiConfig apiConfig);
|
||||
|
||||
Future<T?> executeRequest<T>(ApiRequestable<T> request);
|
||||
}
|
||||
Future<T?> executeRequest<T>(
|
||||
ApiRequestable<T> request, {
|
||||
CancelToken? cancelToken,
|
||||
});
|
||||
|
||||
/// 文件下载(支持进度回调和断点续传)
|
||||
Future<void> executeDownload({
|
||||
required String url,
|
||||
required String savePath,
|
||||
OnDownloadProgress? onProgress,
|
||||
CancelToken? cancelToken,
|
||||
bool resume = false,
|
||||
Map<String, String>? headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,92 +1,75 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/networks_sdk_wiring.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
||||
|
||||
/// Messaging API for real-time communication
|
||||
/// 实时通信公开接口
|
||||
///
|
||||
/// This abstract class provides a technology-agnostic interface for
|
||||
/// real-time messaging. The actual implementation may use WebSocket
|
||||
/// or other transport mechanisms.
|
||||
/// 底层基于 WebSocket,支持 JSON / 字符串 / 二进制消息、
|
||||
/// 自动重连(含无限重连)、Token 热更新、消息加密/解密钩子。
|
||||
///
|
||||
/// ## Usage
|
||||
/// ## 使用方式
|
||||
///
|
||||
/// ```dart
|
||||
/// final messaging = NetworksMessagingApi();
|
||||
/// await messaging.initialize(SocketConfig(...));
|
||||
///
|
||||
/// // Connect to messaging server
|
||||
/// await messaging.connect('wss://api.example.com/ws', token: 'xxx');
|
||||
///
|
||||
/// // Listen for messages
|
||||
/// messaging.messageStream.listen((msg) => print(msg));
|
||||
///
|
||||
/// // Send messages
|
||||
/// await messaging.send({'type': 'chat', 'data': {...}});
|
||||
///
|
||||
/// // Handle connection state
|
||||
/// messaging.connectionStateStream.listen((state) => ...);
|
||||
/// // Token 热更新(不断连)
|
||||
/// messaging.updateToken('new_token');
|
||||
///
|
||||
/// // Handle errors
|
||||
/// messaging.errorStream.listen((error) => ...);
|
||||
/// // 发送二进制
|
||||
/// await messaging.sendBytes(Uint8List.fromList([0x01, 0x02]));
|
||||
///
|
||||
/// // Lifecycle management
|
||||
/// messaging.onEnterForeground();
|
||||
/// messaging.onEnterBackground();
|
||||
///
|
||||
/// // Cleanup
|
||||
/// await messaging.disconnect();
|
||||
/// await messaging.dispose();
|
||||
/// ```
|
||||
abstract class NetworksMessagingApi
|
||||
{
|
||||
abstract class NetworksMessagingApi {
|
||||
factory NetworksMessagingApi() => NetworksSdkWiring.buildMessagingApi();
|
||||
|
||||
/// Initialize the messaging service with configuration
|
||||
void initialize(SocketConfig config);
|
||||
|
||||
/// Connect to the messaging server
|
||||
///
|
||||
/// [url] - WebSocket URL (e.g., 'wss://api.example.com/ws')
|
||||
/// [token] - Optional authentication token
|
||||
Future<bool> connect(String url, {String? token});
|
||||
|
||||
/// Disconnect from the messaging server
|
||||
///
|
||||
/// Manual disconnect does not trigger auto-reconnect
|
||||
Future<void> disconnect();
|
||||
|
||||
/// Check if currently connected
|
||||
bool get isConnected;
|
||||
|
||||
/// Current connection state
|
||||
SocketConnectionState get connectionState;
|
||||
|
||||
/// Send a JSON message
|
||||
/// Token 热更新(不断开连接)
|
||||
///
|
||||
/// 仅更新内部 token,下次重连自动使用新 token。
|
||||
void updateToken(String token);
|
||||
|
||||
Future<bool> send(Map<String, dynamic> message);
|
||||
|
||||
/// Send a raw string message
|
||||
Future<bool> sendString(String message);
|
||||
|
||||
/// Stream of incoming parsed JSON messages
|
||||
/// 发送二进制数据
|
||||
Future<bool> sendBytes(List<int> bytes);
|
||||
|
||||
Stream<Map<String, dynamic>> get messageStream;
|
||||
|
||||
/// Stream of raw string messages (including failed JSON parses)
|
||||
Stream<String> get rawMessageStream;
|
||||
|
||||
/// Stream of connection state changes
|
||||
/// 二进制消息流
|
||||
Stream<Uint8List> get binaryMessageStream;
|
||||
|
||||
Stream<SocketConnectionState> get connectionStateStream;
|
||||
|
||||
/// Stream of errors
|
||||
Stream<SocketError> get errorStream;
|
||||
|
||||
/// Called when app enters foreground
|
||||
void onEnterForeground();
|
||||
|
||||
/// Called when app enters background
|
||||
void onEnterBackground();
|
||||
|
||||
/// Dispose all resources
|
||||
Future<void> dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,69 @@
|
||||
|
||||
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:networks_sdk/src/data/dto/api_requestable.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/api_config.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/networks_sdk_wiring.dart';
|
||||
|
||||
|
||||
/// SDK API
|
||||
abstract class NetworksSdkApi
|
||||
{
|
||||
/// Networks SDK 公开接口
|
||||
///
|
||||
/// 提供两种请求入口:
|
||||
/// - [executeRequest] — 统一请求入口(标准 / Upload / 流式)
|
||||
/// - [executeDownload] — 带进度的文件下载(支持断点续传)
|
||||
///
|
||||
/// 流式请求(SSE)也走 [executeRequest],由业务 Request 类 override
|
||||
/// `decodeResponse` 处理 SSE 解析。SDK 根据 `requestType == stream`
|
||||
/// 自动切换响应类型。
|
||||
///
|
||||
/// 使用方式:
|
||||
/// ```dart
|
||||
/// final sdk = NetworksSdkApi();
|
||||
/// sdk.initialize(apiConfig);
|
||||
///
|
||||
/// // 标准请求
|
||||
/// final data = await sdk.executeRequest(LoginRequest(...));
|
||||
///
|
||||
/// // 流式请求(SSE)— 同一入口,Request 类 override decodeResponse
|
||||
/// final result = await sdk.executeRequest(VoiceTranscribeRequest(...));
|
||||
///
|
||||
/// // 文件下载
|
||||
/// await sdk.executeDownload(
|
||||
/// url: '/files/report.pdf',
|
||||
/// savePath: '/tmp/report.pdf',
|
||||
/// onProgress: (received, total) => print('$received / $total'),
|
||||
/// );
|
||||
/// ```
|
||||
abstract class NetworksSdkApi {
|
||||
factory NetworksSdkApi() => NetworksSdkWiring.build();
|
||||
|
||||
Future<String?> platformVersion();
|
||||
|
||||
void initialize(ApiConfig aApiConfig);
|
||||
void initialize(ApiConfig apiConfig);
|
||||
|
||||
Future<T?> executeRequest<T>(ApiRequestable<T> request);
|
||||
/// 执行 API 请求 — 统一入口
|
||||
///
|
||||
/// 支持标准请求、登录、上传、流式(SSE),由 `request.requestType` 控制。
|
||||
/// 流式请求由业务 Request 类 override `decodeResponse` 处理 SSE 解析。
|
||||
///
|
||||
/// [cancelToken] — 可选,用于取消正在进行的请求
|
||||
Future<T?> executeRequest<T>(
|
||||
ApiRequestable<T> request, {
|
||||
CancelToken? cancelToken,
|
||||
});
|
||||
|
||||
/// 下载文件到本地路径
|
||||
///
|
||||
/// [url] — 下载 URL(完整路径或相对路径)
|
||||
/// [savePath] — 本地保存路径
|
||||
/// [onProgress] — 下载进度回调
|
||||
/// [cancelToken] — 取消令牌
|
||||
/// [resume] — 是否断点续传
|
||||
/// [headers] — 额外请求头
|
||||
Future<void> executeDownload({
|
||||
required String url,
|
||||
required String savePath,
|
||||
OnDownloadProgress? onProgress,
|
||||
CancelToken? cancelToken,
|
||||
bool resume = false,
|
||||
Map<String, String>? headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'network_callbacks.dart';
|
||||
|
||||
/// API 配置
|
||||
@@ -13,12 +12,22 @@ class ApiConfig {
|
||||
/// 平台相关 headers(App 层注入:version、platform、channel 等)
|
||||
Map<String, String> platformHeaders;
|
||||
|
||||
// ── 认证回调 ──
|
||||
|
||||
/// Token 过期时的刷新回调
|
||||
final OnTokenRefresh? onTokenRefresh;
|
||||
|
||||
/// 需要强制登出时的回调
|
||||
final OnForceLogout? onForceLogout;
|
||||
|
||||
/// Token 更新后的通知回调
|
||||
///
|
||||
/// 在 [updateToken] 被调用且新 token 非空时触发。
|
||||
/// App 层用于同步 token 到 WebSocket 等其他模块。
|
||||
final void Function(String newToken)? onTokenUpdated;
|
||||
|
||||
// ── 基础回调 ──
|
||||
|
||||
/// 日志输出回调(不设置则不输出日志)
|
||||
final OnLog? onLog;
|
||||
|
||||
@@ -29,12 +38,39 @@ class ApiConfig {
|
||||
/// 返回 false 则直接抛 [ApiError.noNetworkConnection],不走网络。
|
||||
final OnCheckNetworkAvailable? onCheckNetworkAvailable;
|
||||
|
||||
// ── 加密回调(预留给 cipher_guard_sdk)──
|
||||
|
||||
/// 请求体加密回调,null 时不加密
|
||||
final OnEncryptRequest? onEncryptRequest;
|
||||
|
||||
/// 响应体解密回调,null 时不解密
|
||||
final OnDecryptResponse? onDecryptResponse;
|
||||
|
||||
// ── 业务错误回调 ──
|
||||
|
||||
/// 业务错误拦截回调
|
||||
///
|
||||
/// 在 token 过期 / 强制登出判断之后执行。
|
||||
/// 返回 true = App 层已处理,SDK 正常传递响应;
|
||||
/// 返回 false = 未处理,SDK 继续正常流程。
|
||||
final OnBusinessError? onBusinessError;
|
||||
|
||||
/// 响应变换回调
|
||||
///
|
||||
/// 在 `executeRequest` 解码前调用,App 层可以统一解包
|
||||
/// `{ code, data, message }` 结构。返回 null 表示不变换。
|
||||
final OnTransformResponse? onTransformResponse;
|
||||
|
||||
// ── 错误码集合 ──
|
||||
|
||||
/// App 层定义的 Token 过期错误码集合
|
||||
final Set<int> tokenExpiredCodes;
|
||||
|
||||
/// App 层定义的强制登出错误码集合
|
||||
final Set<int> forceLogoutCodes;
|
||||
|
||||
// ── 重试配置 ──
|
||||
|
||||
/// 瞬态错误最大重试次数(5xx / 超时 / 连接失败)
|
||||
///
|
||||
/// 0 = 不重试(默认),设为 3 启用重试。
|
||||
@@ -46,18 +82,50 @@ class ApiConfig {
|
||||
/// 实际延迟 = min(baseDelay * 2^attempt, 30s) + jitter
|
||||
final Duration retryBaseDelay;
|
||||
|
||||
// ── Token 刷新配置 ──
|
||||
|
||||
/// Token 刷新超时时间,防止 onTokenRefresh 卡住导致请求永远阻塞
|
||||
final Duration tokenRefreshTimeout;
|
||||
|
||||
/// Token 刷新时间窗口:刷新成功后此时间内再次收到过期码直接返回成功,
|
||||
/// 避免服务端同步延迟导致的误判
|
||||
final Duration tokenReuseWindow;
|
||||
|
||||
// ── 主动刷新配置 ──
|
||||
|
||||
/// Token 过期时间解析回调
|
||||
///
|
||||
/// App 层解析 JWT `exp` claim,用于主动刷新判断。
|
||||
/// 未注入时不启用主动刷新。
|
||||
final OnGetTokenExpiry? onGetTokenExpiry;
|
||||
|
||||
/// 主动刷新阈值:距过期不足此时间时提前刷新
|
||||
///
|
||||
/// 默认 1 小时。WebSocket 重连前、App 回前台时
|
||||
/// 自动检查并刷新即将过期的 token,避免带过期 token 发起请求。
|
||||
final Duration proactiveRefreshThreshold;
|
||||
|
||||
ApiConfig({
|
||||
required this.baseURL,
|
||||
this.token,
|
||||
this.platformHeaders = const {},
|
||||
this.onTokenRefresh,
|
||||
this.onForceLogout,
|
||||
this.onTokenUpdated,
|
||||
this.onLog,
|
||||
this.onCheckNetworkAvailable,
|
||||
this.onEncryptRequest,
|
||||
this.onDecryptResponse,
|
||||
this.onBusinessError,
|
||||
this.onTransformResponse,
|
||||
this.tokenExpiredCodes = const {},
|
||||
this.forceLogoutCodes = const {},
|
||||
this.maxRetries = 0,
|
||||
this.retryBaseDelay = const Duration(seconds: 1),
|
||||
this.tokenRefreshTimeout = const Duration(seconds: 10),
|
||||
this.tokenReuseWindow = const Duration(seconds: 3),
|
||||
this.onGetTokenExpiry,
|
||||
this.proactiveRefreshThreshold = const Duration(hours: 1),
|
||||
});
|
||||
|
||||
/// 构建默认 headers
|
||||
@@ -70,6 +138,8 @@ class ApiConfig {
|
||||
final headers = <String, String>{
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Accept': 'application/json',
|
||||
'Keep-Alive': 'timeout=60',
|
||||
// Unix 时间戳(秒),整数值,非格式化日期字符串
|
||||
'Timestamp': '${DateTime.now().millisecondsSinceEpoch ~/ 1000}',
|
||||
'APP-Request-ID': _generateRequestId(),
|
||||
};
|
||||
@@ -91,8 +161,13 @@ class ApiConfig {
|
||||
}
|
||||
|
||||
/// 更新 token
|
||||
///
|
||||
/// 同时触发 [onTokenUpdated] 通知其他模块(如 WebSocket)同步 token。
|
||||
void updateToken(String? newToken) {
|
||||
token = newToken;
|
||||
if (newToken != null && newToken.isNotEmpty) {
|
||||
onTokenUpdated?.call(newToken);
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新 base URL
|
||||
@@ -105,4 +180,4 @@ class ApiConfig {
|
||||
final now = DateTime.now().microsecondsSinceEpoch;
|
||||
return '$now${Object().hashCode}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,105 @@
|
||||
/// 网络层回调类型定义,由 App 层注入 SDK,避免 SDK 直接依赖外部实现。
|
||||
library;
|
||||
|
||||
import 'package:networks_sdk/src/domain/entities/encrypted_request.dart';
|
||||
|
||||
// ── 认证 ──
|
||||
|
||||
/// Token 刷新回调,返回新 token;返回 null 表示刷新失败
|
||||
typedef OnTokenRefresh = Future<String?> Function();
|
||||
|
||||
/// 強制登出回調
|
||||
/// 强制登出回调
|
||||
typedef OnForceLogout = void Function();
|
||||
|
||||
/// 日誌輸出回調
|
||||
// ── Token 生命周期 ──
|
||||
|
||||
/// 获取 token 过期时间
|
||||
///
|
||||
/// App 层解析 JWT 的 `exp` claim 返回过期时间。
|
||||
/// 返回 null 表示无法解析(非 JWT 或格式错误)。
|
||||
typedef OnGetTokenExpiry = DateTime? Function(String token);
|
||||
|
||||
// ── 基础 ──
|
||||
|
||||
/// 日志输出回调
|
||||
typedef OnLog = void Function(String message, {String? tag});
|
||||
|
||||
/// 網路可用性查詢(App 層注入,SDK 在請求前調用)
|
||||
typedef OnCheckNetworkAvailable = Future<bool> Function();
|
||||
/// 网络可用性查询(App 层注入,SDK 在请求前调用)
|
||||
typedef OnCheckNetworkAvailable = Future<bool> Function();
|
||||
|
||||
// ── 加密(预留给 cipher_guard_sdk)──
|
||||
|
||||
/// HTTP 请求加密回调
|
||||
///
|
||||
/// 接收原始路径、headers、请求体,返回 [EncryptedRequest]。
|
||||
/// 拦截器根据返回值中的非 null 字段覆盖原始请求。
|
||||
///
|
||||
/// 参数说明:
|
||||
/// - [path] — 原始请求路径(如 `/api/v1/auth/login`)
|
||||
/// - [headers] — 当前请求的全部 headers(含 token、platform headers 等)
|
||||
/// - [body] — 原始请求体(可能是 Map、String、null 等)
|
||||
///
|
||||
/// App 层实现示例(X25519 + AES-256-CBC 模式):
|
||||
/// - 加密 path → hex 编码 → 替换路径
|
||||
/// - 加密 body → base64 编码 → 替换请求体
|
||||
/// - 加密 token → 放入 X-Token header
|
||||
/// - Ed25519 签名 → 放入 X-Signature header
|
||||
/// - Content-Type → text/plain
|
||||
typedef OnEncryptRequest =
|
||||
Future<EncryptedRequest> Function(
|
||||
String path,
|
||||
Map<String, String> headers,
|
||||
Object? body,
|
||||
);
|
||||
|
||||
/// HTTP 响应解密回调
|
||||
///
|
||||
/// 输入是原始响应数据(加密后可能是 String、`List<int>`、或 Map),
|
||||
/// 返回解密后的 Map 供业务层使用。
|
||||
///
|
||||
/// [responseData] 的实际类型取决于服务端响应格式:
|
||||
/// - 加密模式下通常是 base64 字符串
|
||||
/// - 非加密模式下是 `Map<String, dynamic>`
|
||||
typedef OnDecryptResponse =
|
||||
Future<Map<String, dynamic>> Function(Object responseData);
|
||||
|
||||
// ── 业务错误 ──
|
||||
|
||||
/// 业务错误拦截回调
|
||||
///
|
||||
/// App 层统一处理特定错误码,返回 true = 已处理(SDK 不再抛错),
|
||||
/// 返回 false = 未处理(SDK 继续正常流程)。
|
||||
typedef OnBusinessError = bool Function(int code, String message, String path);
|
||||
|
||||
/// 响应变换回调
|
||||
///
|
||||
/// App 层自定义响应解包逻辑(如统一解包 `{ code, data, message }` 结构)。
|
||||
/// 返回 null 表示不变换,使用原始响应。
|
||||
typedef OnTransformResponse =
|
||||
Map<String, dynamic>? Function(Map<String, dynamic> raw);
|
||||
|
||||
// ── 下载 ──
|
||||
|
||||
/// 下载进度回调
|
||||
typedef OnDownloadProgress = void Function(int received, int total);
|
||||
|
||||
// ── WebSocket 加密(预留给 cipher_guard_sdk)──
|
||||
|
||||
/// WebSocket 连接 URL 构建回调
|
||||
///
|
||||
/// 建立连接前调用,接收原始 URL 和 token,返回最终的连接 URL 字符串。
|
||||
/// WS 握手本质是 HTTP GET 升级请求,只需控制 URL(路径 + 查询参数)。
|
||||
///
|
||||
/// App 层可在此(通过调用 cipher_guard_sdk):
|
||||
/// - 加密 URL 路径(如 `/ws` → `/hex(encrypt(ws))`)
|
||||
/// - 加密 token 参数(明文 token 不出现在 URL 中)
|
||||
/// - 添加加密模式协商参数(如 `cipher=true&type=mode3`)
|
||||
///
|
||||
/// null 时使用默认行为:在 URL 后追加 `?token=xxx`。
|
||||
typedef OnBuildConnectUrl = String Function(String url, String? token);
|
||||
|
||||
/// WebSocket 发送前加密回调
|
||||
typedef OnEncryptMessage = Future<String> Function(String plainText);
|
||||
|
||||
/// WebSocket 收到后解密回调
|
||||
typedef OnDecryptMessage = Future<String> Function(String cipherText);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:networks_sdk/src/data/repositories/networks_messaging_repository_impl.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/socket_connection_state.dart';
|
||||
import 'package:networks_sdk/src/domain/entities/socket_error.dart';
|
||||
@@ -5,7 +7,7 @@ import 'package:networks_sdk/src/domain/repositories/networks_messaging_reposito
|
||||
import 'package:networks_sdk/src/presentation/facade/networks_messaging_api.dart';
|
||||
import 'package:networks_sdk/src/presentation/wiring/socket_config.dart';
|
||||
|
||||
/// Implementation of [NetworksMessagingApi] using [NetworksMessagingRepository]
|
||||
/// [NetworksMessagingApi] 的实现,透传给 [NetworksMessagingRepository]
|
||||
class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
||||
NetworksMessagingRepository? _repository;
|
||||
|
||||
@@ -47,6 +49,12 @@ class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
||||
return _repository!.connectionState;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateToken(String token) {
|
||||
_checkInitialized();
|
||||
_repository!.updateToken(token);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> send(Map<String, dynamic> message) {
|
||||
_checkInitialized();
|
||||
@@ -59,6 +67,12 @@ class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
||||
return _repository!.sendString(message);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> sendBytes(List<int> bytes) {
|
||||
_checkInitialized();
|
||||
return _repository!.sendBytes(bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Map<String, dynamic>> get messageStream {
|
||||
_checkInitialized();
|
||||
@@ -71,6 +85,12 @@ class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
||||
return _repository!.rawMessageStream;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Uint8List> get binaryMessageStream {
|
||||
_checkInitialized();
|
||||
return _repository!.binaryMessageStream;
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<SocketConnectionState> get connectionStateStream {
|
||||
_checkInitialized();
|
||||
@@ -103,4 +123,3 @@ class NetworksMessagingApiImpl implements NetworksMessagingApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import '../../../networks_sdk.dart';
|
||||
import 'networks_sdk_core.dart';
|
||||
|
||||
/// SDK API Implementation
|
||||
/// [NetworksSdkApi] 的实现,透传给 Repository
|
||||
class NetworksSdkApiImpl implements NetworksSdkApi {
|
||||
final NetworksSdkCore _core;
|
||||
|
||||
@@ -14,6 +14,29 @@ class NetworksSdkApiImpl implements NetworksSdkApi {
|
||||
void initialize(ApiConfig apiConfig) => _core.repo.initialize(apiConfig);
|
||||
|
||||
@override
|
||||
Future<T?> executeRequest<T>(ApiRequestable<T> request) => _core.repo.executeRequest(request);
|
||||
Future<T?> executeRequest<T>(
|
||||
ApiRequestable<T> request, {
|
||||
CancelToken? cancelToken,
|
||||
}) {
|
||||
return _core.repo.executeRequest(request, cancelToken: cancelToken);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> executeDownload({
|
||||
required String url,
|
||||
required String savePath,
|
||||
OnDownloadProgress? onProgress,
|
||||
CancelToken? cancelToken,
|
||||
bool resume = false,
|
||||
Map<String, String>? headers,
|
||||
}) {
|
||||
return _core.repo.executeDownload(
|
||||
url: url,
|
||||
savePath: savePath,
|
||||
onProgress: onProgress,
|
||||
cancelToken: cancelToken,
|
||||
resume: resume,
|
||||
headers: headers,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'package:networks_sdk/src/presentation/wiring/network_callbacks.dart';
|
||||
|
||||
/// WebSocket 配置
|
||||
/// 非单例,由 App 层构造并注入到 SocketClient
|
||||
///
|
||||
/// 与 [ApiConfig] 设计一致:SDK 不依赖 Flutter,
|
||||
/// 网络检测、生命周期等业务逻辑通过回调注入。
|
||||
class SocketConfig {
|
||||
// ── 心跳 ──
|
||||
|
||||
/// 应用层心跳间隔(定时发送 "ping" 字符串)
|
||||
final Duration heartbeatInterval;
|
||||
|
||||
@@ -13,10 +17,19 @@ class SocketConfig {
|
||||
/// Pong 超时(超过此时间未收到 pong 则判定连接断开)
|
||||
final Duration pongTimeout;
|
||||
|
||||
// ── 连接 ──
|
||||
|
||||
/// 连接超时
|
||||
final Duration connectTimeout;
|
||||
|
||||
/// 是否启用 WebSocket 压缩(permessage-deflate)
|
||||
final bool enableCompression;
|
||||
|
||||
// ── 重连 ──
|
||||
|
||||
/// 最大重连次数(0 = 不重连)
|
||||
///
|
||||
/// 当 [unlimitedReconnect] 为 true 时此字段无效。
|
||||
final int maxReconnectAttempts;
|
||||
|
||||
/// 最大重连延迟(指数退避上限)
|
||||
@@ -25,22 +38,65 @@ class SocketConfig {
|
||||
/// 是否自动重连
|
||||
final bool autoReconnect;
|
||||
|
||||
/// 无限重连模式
|
||||
///
|
||||
/// IM 场景建议开启:连接断开后始终尝试重连,不受
|
||||
/// [maxReconnectAttempts] 限制。退避延迟仍受
|
||||
/// [maxReconnectDelay] 约束。
|
||||
final bool unlimitedReconnect;
|
||||
|
||||
// ── 回调 ──
|
||||
|
||||
/// 日志输出回调(与 ApiConfig.onLog 同签名)
|
||||
final void Function(String message, {String? tag})? onLog;
|
||||
final OnLog? onLog;
|
||||
|
||||
/// 网络可用性查询(App 层注入,SDK 在重连前调用)
|
||||
/// 返回 true 表示网络可用,可以尝试重连
|
||||
final Future<bool> Function()? onCheckNetworkAvailable;
|
||||
final OnCheckNetworkAvailable? onCheckNetworkAvailable;
|
||||
|
||||
/// 重连前回调
|
||||
///
|
||||
/// 每次自动重连前调用(心跳超时、连接断开等触发的内部重连)。
|
||||
/// App 层用于:
|
||||
/// - 检查并刷新即将过期的 token(通过 [SocketClient.updateToken])
|
||||
/// - 其他重连前准备工作
|
||||
///
|
||||
/// 回调完成后才发起实际连接。如果回调抛出异常,本次重连跳过,
|
||||
/// 等下一轮退避定时器触发。
|
||||
final Future<void> Function()? onBeforeReconnect;
|
||||
|
||||
// ── 加密回调(预留给 cipher_guard_sdk)──
|
||||
|
||||
/// 连接 URL 构建回调
|
||||
///
|
||||
/// 建立连接前调用,接收原始 URL 和 token,返回最终连接 URL 字符串。
|
||||
/// null 时使用默认行为(URL 后追加 `?token=xxx`)。
|
||||
///
|
||||
/// App 层注入 cipher_guard_sdk 的加密逻辑:路径/token 加密、
|
||||
/// 添加 `cipher=true` 参数等。
|
||||
final OnBuildConnectUrl? onBuildConnectUrl;
|
||||
|
||||
/// 发送前加密回调,null 时不加密
|
||||
final OnEncryptMessage? onEncryptMessage;
|
||||
|
||||
/// 收到后解密回调,null 时不解密
|
||||
final OnDecryptMessage? onDecryptMessage;
|
||||
|
||||
SocketConfig({
|
||||
this.heartbeatInterval = const Duration(seconds: 10),
|
||||
this.pingInterval = const Duration(seconds: 5),
|
||||
this.pongTimeout = const Duration(seconds: 10),
|
||||
this.connectTimeout = const Duration(seconds: 15),
|
||||
this.enableCompression = false,
|
||||
this.maxReconnectAttempts = 5,
|
||||
this.maxReconnectDelay = const Duration(seconds: 30),
|
||||
this.autoReconnect = true,
|
||||
this.unlimitedReconnect = false,
|
||||
this.onLog,
|
||||
this.onCheckNetworkAvailable,
|
||||
this.onBeforeReconnect,
|
||||
this.onBuildConnectUrl,
|
||||
this.onEncryptMessage,
|
||||
this.onDecryptMessage,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
group = "com.example.notification_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "kotlin-android"
|
||||
|
||||
android {
|
||||
namespace = "com.example.notification_sdk"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += "src/main/kotlin"
|
||||
test.java.srcDirs += "src/test/kotlin"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
outputs.upToDateWhen {false}
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/notification_sdk/android/build.gradle.kts
Normal file
71
packages/notification_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
||||
group = "com.example.notification_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.notification_sdk"
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
all {
|
||||
it.useJUnitPlatform()
|
||||
it.outputs.upToDateWhen { false }
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Flutter
|
||||
@preconcurrency import Flutter
|
||||
import UIKit
|
||||
|
||||
public class NotificationSdkPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
||||
|
||||
# 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'
|
||||
s.swift_version = '6.2'
|
||||
|
||||
# If your plugin requires a privacy manifest, for example if it uses any
|
||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
group = "com.example.protocol_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "kotlin-android"
|
||||
|
||||
android {
|
||||
namespace = "com.example.protocol_sdk"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += "src/main/kotlin"
|
||||
test.java.srcDirs += "src/test/kotlin"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
outputs.upToDateWhen {false}
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/protocol_sdk/android/build.gradle.kts
Normal file
71
packages/protocol_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
||||
group = "com.example.protocol_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.protocol_sdk"
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
all {
|
||||
it.useJUnitPlatform()
|
||||
it.outputs.upToDateWhen { false }
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Flutter
|
||||
@preconcurrency import Flutter
|
||||
import UIKit
|
||||
|
||||
public class ProtocolSdkPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
||||
|
||||
# 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'
|
||||
s.swift_version = '6.2'
|
||||
|
||||
# If your plugin requires a privacy manifest, for example if it uses any
|
||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
group = "com.example.rtc_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "kotlin-android"
|
||||
|
||||
android {
|
||||
namespace = "com.example.rtc_sdk"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += "src/main/kotlin"
|
||||
test.java.srcDirs += "src/test/kotlin"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
outputs.upToDateWhen {false}
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/rtc_sdk/android/build.gradle.kts
Normal file
71
packages/rtc_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
||||
group = "com.example.rtc_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.rtc_sdk"
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
all {
|
||||
it.useJUnitPlatform()
|
||||
it.outputs.upToDateWhen { false }
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Flutter
|
||||
@preconcurrency import Flutter
|
||||
import UIKit
|
||||
|
||||
public class RtcSdkPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
||||
|
||||
# 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'
|
||||
s.swift_version = '6.2'
|
||||
|
||||
# If your plugin requires a privacy manifest, for example if it uses any
|
||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
group = "com.example.storage_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: "com.android.library"
|
||||
apply plugin: "kotlin-android"
|
||||
|
||||
android {
|
||||
namespace = "com.example.storage_sdk"
|
||||
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs += "src/main/kotlin"
|
||||
test.java.srcDirs += "src/test/kotlin"
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.all {
|
||||
useJUnitPlatform()
|
||||
|
||||
testLogging {
|
||||
events "passed", "skipped", "failed", "standardOut", "standardError"
|
||||
outputs.upToDateWhen {false}
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/storage_sdk/android/build.gradle.kts
Normal file
71
packages/storage_sdk/android/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
||||
group = "com.example.storage_sdk"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
buildscript {
|
||||
val kotlinVersion = "2.2.20"
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:8.11.1")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.storage_sdk"
|
||||
compileSdk = 36
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||||
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
isIncludeAndroidResources = true
|
||||
all {
|
||||
it.useJUnitPlatform()
|
||||
it.outputs.upToDateWhen { false }
|
||||
it.testLogging {
|
||||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||||
showStandardStreams = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Compose BOM 统一管理子库版本,按需在各 SDK 中引入具体组件
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import Flutter
|
||||
@preconcurrency import Flutter
|
||||
import UIKit
|
||||
|
||||
public class StorageSdkPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
@@ -19,7 +19,7 @@ A new Flutter plugin project.
|
||||
|
||||
# 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'
|
||||
s.swift_version = '6.2'
|
||||
|
||||
# If your plugin requires a privacy manifest, for example if it uses any
|
||||
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
|
||||
|
||||
Reference in New Issue
Block a user