Skip to content

derek44554/block_flutter

Repository files navigation

block_flutter SDK

Block 网络的核心 Flutter SDK,提供网络通信、加密传输、数据模型和缓存能力。

新项目只需依赖此包即可接入 Block 网络,无需重复实现加密、路由拼装等底层逻辑。


目录


核心概念

Block 网络

Block 网络是一个去中心化的数据网络,所有数据以"Block"为单位存储和传输。

  • 每个 Block 有唯一的 BID(21字符标识符)
  • Block 通过 model 字段区分类型(document、article、set、file、service、user、record、gps 等)
  • 所有网络请求走同一个 HTTP 端点 /bridge/ins,通过 routing 字段区分操作
  • 请求和响应均使用 AES-CBC + Base64 对称加密

连接模型(ConnectionModel)

连接到 Block 网络节点需要两个核心信息:

  • address:节点的 HTTP 地址,例如 http://192.168.1.100:8080
  • keyBase64:与该节点共享的 AES 对称密钥(Base64 编码)

所有 API 类都接受 ConnectionModel 作为构造参数。

协议模式

protocol 字段有两个值:

  • cert(默认):需要节点证书验证的请求,用于大多数数据操作
  • open:无需证书的开放请求,用于节点信息查询(如 getSignature

快速接入

1. 添加依赖

# pubspec.yaml
dependencies:
  block_flutter:
    path: ./block_flutter  # 本地路径,或发布后使用版本号

2. 导入

import 'package:block_flutter/block_flutter.dart';

3. 创建连接并调用 API

final connection = ConnectionModel(
  name: '我的节点',
  address: 'http://192.168.1.100:8080',
  keyBase64: 'your-base64-encoded-aes-key==',
  status: ConnectionStatus.connected,
);

final blockApi = BlockApi(connection: connection);
final response = await blockApi.getBlock(bid: 'abc123def456ghi789012');

模块总览

block_flutter/lib/
├── block_flutter.dart          # 统一导出入口
└── src/
    ├── models/
    │   ├── connection_model.dart   # 节点连接信息
    │   ├── block_model.dart        # Block 数据模型
    │   ├── result.dart             # Result<T> 封装
    │   └── api_error.dart          # API 错误类型
    ├── network/
    │   ├── bridge_transport.dart   # 底层加密 HTTP 传输
    │   ├── api_client.dart         # 通用请求客户端
    │   ├── block_api.dart          # Block CRUD 操作
    │   └── node_api.dart           # 节点信息查询
    ├── cache/
    │   ├── block_cache.dart        # Block 元数据缓存(内存+磁盘)
    │   ├── image_cache.dart        # 图片缓存(多尺寸变体)
    │   └── audio_cache.dart        # 音频文件缓存
    └── crypto/
        └── crypto_util.dart        # AES-CBC / AES-GCM 加解密工具

所有公开类均通过 package:block_flutter/block_flutter.dart 统一导出,无需引用内部路径。


数据模型

ConnectionModel

描述一个 Block 网络节点的连接信息。

const connection = ConnectionModel(
  name: '主节点',
  address: 'http://192.168.1.100:8080',
  keyBase64: 'base64encodedkey==',
  status: ConnectionStatus.connected,
  isActive: true,
  enableIpfsStorage: false,  // 是否用于 IPFS 存储
);
字段 类型 说明
name String 节点显示名称
address String 节点 HTTP 地址
keyBase64 String AES 对称密钥(Base64)
status ConnectionStatus connected / connecting / offline
isActive bool 是否为当前活跃连接
nodeData Map? 节点元数据(由 /node/node 返回)
signatureData Map? 节点签名数据
enableIpfsStorage bool 是否启用 IPFS 存储

序列化:

// 持久化
final json = connection.toJson();

// 恢复
final connection = ConnectionModel.fromJson(json);

// 更新字段(不可变模式)
final updated = connection.copyWith(status: ConnectionStatus.offline);

BlockModel

所有 Block 数据的统一容器,内部是一个 Map<String, dynamic>,提供类型安全的读取方法。

final block = BlockModel(data: responseMap);

// 类型安全读取
final bid = block.getString('bid');           // String,不存在返回 ''
final name = block.maybeString('name');       // String?,空字符串返回 null
final count = block.getInt('count');          // int,不存在返回 0
final flag = block.getBool('is_public');      // bool
final tags = block.getList<String>('tags');   // List<String>
final meta = block.getMap('metadata');        // Map<String, dynamic>
final created = block.getDateTime('created_at'); // DateTime?

// 便捷属性
block.bid        // maybeString('bid')
block.typeId     // maybeString('model')
block.title      // maybeString('name')
block.intro      // maybeString('intro')
block.createdAt  // getDateTime('created_at')
block.updatedAt  // getDateTime('updated_at')

// 判断字段是否存在且非空
if (block.has('cover_cid')) { ... }

// 函数调用语法(等同于 getString)
final title = block('name');

// 更新字段
final updated = block.copyWith({'name': '新标题'});

Result<T>

操作结果的密封类封装,用于需要显式处理成功/失败的场景。

// 创建
final ok = Success<String>('data');
final err = Failure<String>(Exception('something went wrong'));

// 消费
result.when(
  success: (data) => print(data),
  failure: (error) => print(error),
);

// 映射
final mapped = result.map((s) => s.length);

// 快速判断
if (result.isSuccess) { ... }
final data = result.dataOrNull;

ApiError

API 层抛出的结构化错误。

try {
  await blockApi.getBlock(bid: bid);
} on ApiError catch (e) {
  print(e.message);    // 错误描述
  print(e.code);       // 业务错误码(可为 null)
  print(e.statusCode); // HTTP 状态码(可为 null)
}

网络层

架构概览

调用方
  │
  ▼
BlockApi / NodeApi          ← 高层 API,封装路由和参数
  │
  ▼
ApiClient                   ← 通用请求客户端,组装 payload
  │
  ▼
BridgeTransport             ← 加密 + HTTP POST /bridge/ins
  │
  ▼
Block 网络节点

BridgeTransport

底层传输层,负责加密、发送、解密。通常不需要直接使用,由 ApiClient 调用。

final response = await BridgeTransport.post(
  connection: connection,
  payload: {
    'protocol': 'cert',
    'routing': '/block/block/get',
    'data': {'bid': 'abc123...'},
    'receiver': '',
    'wait': true,
    'timeout': 60,
  },
);

传输流程:

  1. 将 payload JSON 序列化
  2. CryptoUtil.encryptBase64 加密(AES-CBC,随机 IV)
  3. POST {"text": "<密文>"}{address}/bridge/ins
  4. 解析响应中的 text 字段并解密
  5. 返回解密后的 Map<String, dynamic>

ApiClient

通用请求客户端,接受 ConnectionModel,封装 payload 组装逻辑。

final client = ApiClient(connection: connection);

final response = await client.postToBridge(
  routing: '/block/block/get',
  data: {'bid': 'abc123...'},
  // 以下均有默认值
  protocol: 'cert',      // 'cert' 或 'open'
  receiver: '',          // 接收节点 BID,'' 表示默认
  wait: true,            // 是否等待响应
  timeout: 60,           // 超时秒数
  receiverBid: null,     // 指定接收节点前缀(优先于 receiver)
);

payload 字段说明:

字段 说明
routing Block 网络内部路由,类似 HTTP path
data 业务数据,随路由不同而变化
protocol cert(需认证)或 open(公开)
receiver 接收节点标识,"" 默认,"*" 广播
wait true 等待响应数据,false 仅发送
timeout 等待超时秒数

BlockApi

Block 数据的 CRUD 操作,自动处理路由拼装和 BID 规范化。

final api = BlockApi(connection: connection);

getBlock

// 支持传入 String BID 或 BlockModel 实例
final response = await api.getBlock(bid: 'abc123def456ghi789012');
final response = await api.getBlock(bid: someBlockModel);

saveBlock

final response = await api.saveBlock(
  data: {
    'bid': 'abc123def456ghi789012',
    'model': 'document',
    'name': '标题',
    'content': '正文内容',
    'created_at': '2025-01-01T00:00:00+08:00',
  },
  receiverBid: null,  // 可选,不传则从 data['bid'] 前10位推断
);

data 中若包含 node_bid 字段,会自动作为 receiverBid 使用。

deleteBlock

final response = await api.deleteBlock(bid: 'abc123def456ghi789012');
// 删除 Block 本身及其所有关联 Link 和 Tag

getBlocksByTag

final response = await api.getBlocksByTag(
  name: 'flutter',
  page: 1,
  limit: 20,
);

getLinksByTarget

// 获取以指定 BID 为 target 的所有 Link
final response = await api.getLinksByTarget(
  bid: 'abc123...',
  page: 1,
  limit: 10,
  order: 'desc',  // 可选
);

getLinksByMain

// 获取以指定 BID 为 main 的所有 Link,支持按 model/tag 过滤
final response = await api.getLinksByMain(
  bid: 'abc123...',
  page: 1,
  limit: 10,
  model: 'document',  // 可选
  tag: 'flutter',     // 可选
  order: 'asc',       // 可选
);

getLinksByTargets

// 批量查询多个 target BID 的 Link
final response = await api.getLinksByTargets(
  bids: ['bid1', 'bid2', 'bid3'],
  page: 1,
  limit: 10,
);

getRecentBlocks

final response = await api.getRecentBlocks(limit: 20, page: 1);

getMultipleBlocks

final response = await api.getMultipleBlocks(
  bids: ['bid1', 'bid2', 'bid3'],
);

getAllBlocks

final response = await api.getAllBlocks(
  page: 1,
  limit: 20,
  order: 'asc',           // 'asc' 或 'desc'
  model: 'document',      // 可选,按类型过滤
  tag: 'flutter',         // 可选,按标签过滤
  excludeModels: ['service', 'user'],  // 可选,排除类型
  receiverBid: null,      // 可选
);

NodeApi

节点信息查询。

final nodeApi = NodeApi(connection: connection);

// 获取节点签名信息(使用 open 协议,无需认证)
final response = await nodeApi.getSignature();

缓存层

BlockCache

Block 元数据的两级缓存(内存 LRU + 磁盘 SharedPreferences),TTL 3天,内存最多100条。

final cache = BlockCache.instance;  // 单例

// 读取(内存 → 磁盘 → null)
final block = await cache.get('abc123...');
if (block == null) {
  // 缓存未命中,从 API 获取
  final response = await blockApi.getBlock(bid: 'abc123...');
  final fetched = BlockModel(data: response);
  await cache.put('abc123...', fetched);
}

// 写入
await cache.put('abc123...', blockModel);

// 删除
await cache.remove('abc123...');

// 清空全部
await cache.clear();

// 统计
final stats = cache.getStats();
// {'memorySize': 42, 'maxMemorySize': 100, 'ttlHours': 72}

ImageCacheHelper

图片的多级缓存,支持 small(240px)、medium(480px)、original 三种尺寸变体,磁盘上限 2GiB,内存上限 120MiB。

// 检查磁盘缓存
final file = await ImageCacheHelper.getCachedImage(
  cid,
  variant: ImageVariant.medium,
);

// 保存到缓存
await ImageCacheHelper.saveImageToCache(cid, bytes, variant: ImageVariant.original);

// 内存缓存读写
final bytes = ImageCacheHelper.getMemoryImage(cid, variant: ImageVariant.small);
ImageCacheHelper.cacheMemoryImage(cid, bytes, variant: ImageVariant.medium);

// 一体化加载(内存 → 磁盘 → fetcher)
final bytes = await ImageCacheHelper.getOrLoadImage(
  cid: cid,
  endpoint: 'http://ipfs-gateway.example.com',
  variant: ImageVariant.medium,
  fetcher: ({required cid, required endpoint, variant = ImageVariant.original}) async {
    // 实现具体的网络获取逻辑
    return await myIpfsFetch(cid, endpoint);
  },
);

// 生成缩略图变体(异步,去重并发)
final thumbnail = await ImageCacheHelper.ensureVariant(
  cid,
  originalBytes,
  variant: ImageVariant.small,
);

// 删除所有变体
await ImageCacheHelper.removeFromCache(cid);

ImageVariant 说明:

变体 目标宽度 用途
small 240px 列表缩略图
medium 480px 预览图
original 原始 详情页全图

AudioCacheHelper

音频文件的磁盘缓存,按 CID + 扩展名存储。

// 检查缓存
final file = await AudioCacheHelper.getCachedAudio(cid, '.mp3');
if (file != null) {
  // 使用缓存文件
}

// 保存
final cachedFile = await AudioCacheHelper.saveAudioToCache(cid, bytes, '.mp3');

// 删除
await AudioCacheHelper.deleteCachedAudio(cid, '.mp3');

// 清空全部
await AudioCacheHelper.clearAllCache();

// 统计
final sizeBytes = await AudioCacheHelper.getCacheSize();
final count = await AudioCacheHelper.getCacheCount();
final readable = AudioCacheHelper.formatCacheSize(sizeBytes); // '12.34 MB'

加密工具

CryptoUtil

AES-CBC(主要用于网络传输)和 AES-GCM(用于文件加密)的工具类。

// AES-CBC 加密(用于 BridgeTransport)
// 输出格式:Base64(随机16字节IV + 密文)
final encrypted = CryptoUtil.encryptBase64(plainText, base64Key);
final decrypted = CryptoUtil.decryptBase64(encrypted, base64Key);

// Base64 规范化(去除空白,补齐填充)
final normalized = CryptoUtil.normalizeBase64(rawBase64String);

// AES-GCM 加密(用于文件内容加密)
// 输出格式:Hex(16字节nonce + 密文 + 16字节认证tag)
final keyBytes = CryptoUtil.generateRandomKey(32); // 256-bit key
final hexEncrypted = CryptoUtil.encryptAesGcm(dataBytes, keyBytes);
final decryptedBytes = CryptoUtil.decryptAesGcm(hexEncrypted, keyBytes);

// Hex 工具
final bytes = CryptoUtil.hexToBytes('deadbeef');

密钥长度: 支持 16(AES-128)、24(AES-192)、32(AES-256)字节。


适配层模式(推荐)

在 Flutter 应用中,连接信息通常由状态管理层(如 Provider)持有,而 SDK 的 API 类接受 ConnectionModel。推荐使用适配层模式桥接两者:

// 适配层示例:从 Provider 获取连接,委托给 SDK
class BlockApi {
  BlockApi({required ConnectionProvider connectionProvider})
      : _connectionProvider = connectionProvider;

  final ConnectionProvider _connectionProvider;

  Future<Map<String, dynamic>> getBlock({required dynamic bid}) {
    final connection = _connectionProvider.activeConnection;
    if (connection == null) {
      throw StateError('No active connection available. Cannot perform request.');
    }
    // 委托给 SDK,每次调用动态获取最新连接
    return sdk.BlockApi(connection: connection).getBlock(bid: bid);
  }
}

为什么每次调用都新建 SDK 实例?

连接可能在运行时切换(用户切换节点),每次调用时动态获取 activeConnection 可确保始终使用最新的连接信息,而不是缓存了旧连接的实例。

无活跃连接时的约定:

所有适配层方法在 activeConnection == null 时统一抛出:

throw StateError('No active connection available. Cannot perform request.');

完整使用示例

场景一:直接使用 SDK(无 Flutter 状态管理)

import 'package:block_flutter/block_flutter.dart';

Future<void> example() async {
  final connection = ConnectionModel(
    name: '节点',
    address: 'http://192.168.1.100:8080',
    keyBase64: 'your-key==',
    status: ConnectionStatus.connected,
  );

  final blockApi = BlockApi(connection: connection);

  // 创建 Block
  final saveResponse = await blockApi.saveBlock(data: {
    'bid': 'abc123def456ghi789012',
    'model': 'document',
    'name': '我的文档',
    'content': '正文',
    'created_at': DateTime.now().toIso8601String(),
  });

  // 读取 Block
  final getResponse = await blockApi.getBlock(bid: 'abc123def456ghi789012');
  final block = BlockModel(data: getResponse);
  print(block.title);  // '我的文档'

  // 查询节点签名
  final nodeApi = NodeApi(connection: connection);
  final signature = await nodeApi.getSignature();
}

场景二:配合 BlockCache 减少重复请求

Future<BlockModel> fetchWithCache(BlockApi api, String bid) async {
  final cache = BlockCache.instance;

  // 先查缓存
  final cached = await cache.get(bid);
  if (cached != null) return cached;

  // 缓存未命中,从网络获取
  final response = await api.getBlock(bid: bid);
  final block = BlockModel(data: response);

  // 写入缓存
  await cache.put(bid, block);
  return block;
}

场景三:自定义路由请求

如果需要调用 SDK 未封装的路由,直接使用 ApiClient

final client = ApiClient(connection: connection);

final response = await client.postToBridge(
  routing: '/custom/my/route',
  data: {'key': 'value'},
  protocol: 'cert',
  timeout: 30,
);

错误处理

SDK 不捕获或包装异常,所有错误直接向上传播:

异常类型 触发场景
HttpException HTTP 状态码非 200
FormatException 响应 JSON 解析失败
ArgumentError AES 解密失败(密钥错误、数据损坏)
StateError 适配层中无活跃连接(由应用层抛出)

推荐的错误处理模式:

try {
  final response = await blockApi.getBlock(bid: bid);
  // 处理成功响应
} on StateError catch (e) {
  // 无活跃连接,引导用户配置节点
  showConnectionError(e.message);
} on HttpException catch (e) {
  // 网络或服务端错误
  showNetworkError(e.message);
} catch (e) {
  // 其他错误(解密失败等)
  showGenericError(e.toString());
}

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages