feat: 新增 Bash 和 Dart 客户端 SDK 及 demo

- 新增 kenaito-conf-bash-client(Bash 4.0+,依赖 curl、jq)及对应 demo
- 新增 kenaito-conf-dart-client(Dart 2.12+,空安全)及对应 demo
- 新增 Dockerfile.bash(debian:bookworm-slim)和 Dockerfile.dart(dart:3.5)
- README 技术栈和项目结构补充 Bash、Dart 客户端
- README_EN.md 同步更新
- README_DOCKERFILE.md 补充 Bash、Dart 构建命令
- 修复 Bash 客户端 curl 2>&1 混入 stderr 导致响应解析失败
- 修复 Dart 客户端 latest.fetch.timestamp 类型转换兼容 string/num
This commit is contained in:
2026-08-19 21:11:28 +08:00
parent 97a5843483
commit 5f67274e3f
18 changed files with 1143 additions and 4 deletions
+83
View File
@@ -0,0 +1,83 @@
# Kenaito Config Center Dart Client
[English](README_EN.md) | 中文
Kenaito 配置中心 Dart 客户端 SDK,适用于 Dart 2.12+ 项目(支持空安全)。
## 安装
### 方式一:本地路径引入
在你的项目 `pubspec.yaml` 中添加:
```yaml
dependencies:
kenaito_conf:
path: /path/to/kenaito-conf-dart-client
```
### 方式二:直接引入
```dart
import '/path/to/kenaito-conf-dart-client/lib/kenaito_conf.dart';
```
## 使用示例
```dart
import 'package:kenaito_conf/kenaito_conf.dart';
void main() async {
final config = Config(
baseUrl: 'http://localhost:8080',
appName: 'my-app',
env: 'daily',
syncInterval: 10,
);
final client = Client(config);
// 监听配置变更
client.onChange((Map<String, String> changed) {
changed.forEach((key, value) {
print('config changed: $key = $value');
});
});
// 启动客户端(拉取配置)
client.start();
await Future.delayed(Duration(seconds: 2)); // 等待异步完成
// 获取配置
final value = client.get('database.host');
final allConfigs = client.getAll();
// 同步循环
while (true) {
client.syncOnce();
await Future.delayed(Duration(seconds: config.syncInterval));
}
}
```
## 配置项
| 配置项 | 类型 | 说明 | 默认值 |
|--------|------|------|--------|
| baseUrl | String | 配置中心后端地址 | http://localhost:8080 |
| appName | String | 应用名称 | 空 |
| env | String | 环境(dev/daily/prod) | dev |
| version | String | 配置版本 | latest |
| token | String | 应用 Token | 空 |
| syncInterval | int | 同步间隔(秒) | 10 |
| ignoreGlobal | bool | 是否忽略全局配置 | false |
| cacheDir | String | 缓存目录 | . |
## 功能特性
- ✅ 全量拉取配置
- ✅ 增量同步变更
- ✅ 本地文件缓存容灾
- ✅ 配置变更监听回调
- ✅ 删除配置感知(deletedKeys)
- ✅ 空安全支持
+83
View File
@@ -0,0 +1,83 @@
# Kenaito Config Center Dart Client
[English](README_EN.md) | 中文
Kenaito Config Center Dart client SDK, compatible with Dart 2.12+ (null-safe).
## Installation
### Option 1: Local Path
Add to your project `pubspec.yaml`:
```yaml
dependencies:
kenaito_conf:
path: /path/to/kenaito-conf-dart-client
```
### Option 2: Direct Import
```dart
import '/path/to/kenaito-conf-dart-client/lib/kenaito_conf.dart';
```
## Usage Example
```dart
import 'package:kenaito_conf/kenaito_conf.dart';
void main() async {
final config = Config(
baseUrl: 'http://localhost:8080',
appName: 'my-app',
env: 'daily',
syncInterval: 10,
);
final client = Client(config);
// Listen for config changes
client.onChange((Map<String, String> changed) {
changed.forEach((key, value) {
print('config changed: $key = $value');
});
});
// Start client (fetch configs)
client.start();
await Future.delayed(Duration(seconds: 2)); // Wait for async completion
// Get config
final value = client.get('database.host');
final allConfigs = client.getAll();
// Sync loop
while (true) {
client.syncOnce();
await Future.delayed(Duration(seconds: config.syncInterval));
}
}
```
## Configuration Options
| Option | Type | Description | Default |
|--------|------|-------------|---------|
| baseUrl | String | Config center backend URL | http://localhost:8080 |
| appName | String | Application name | empty |
| env | String | Environment (dev/daily/prod) | dev |
| version | String | Config version | latest |
| token | String | Application token | empty |
| syncInterval | int | Sync interval (seconds) | 10 |
| ignoreGlobal | bool | Ignore global configs | false |
| cacheDir | String | Cache directory | . |
## Features
- ✅ Full config fetch
- ✅ Incremental sync
- ✅ Local file cache for disaster recovery
- ✅ Config change listener callback
- ✅ Deleted config awareness (deletedKeys)
- ✅ Null safety support
@@ -0,0 +1,8 @@
/// Kenaito Config Center Dart Client
/// 版本: 1.0.3
library kenaito_conf;
export 'src/config.dart';
export 'src/cache.dart';
export 'src/listener.dart';
export 'src/client.dart';
@@ -0,0 +1,34 @@
import 'dart:io';
import 'dart:convert';
/// 文件缓存
class Cache {
final String _cacheFile;
Cache(String cacheDir) : _cacheFile = '$cacheDir/config-cache.json';
/// 读取缓存
Map<String, dynamic>? read() {
try {
final file = File(_cacheFile);
if (!file.existsSync()) {
return null;
}
final content = file.readAsStringSync();
return json.decode(content) as Map<String, dynamic>;
} catch (e) {
stderr.writeln('[ConfigClient] cache read error: $e');
return null;
}
}
/// 写入缓存
void write(Map<String, dynamic> data) {
try {
final file = File(_cacheFile);
file.writeAsStringSync(json.encode(data));
} catch (e) {
stderr.writeln('[ConfigClient] cache write error: $e');
}
}
}
@@ -0,0 +1,197 @@
import 'dart:io';
import 'dart:convert';
import 'config.dart';
import 'cache.dart';
import 'listener.dart';
/// 配置中心客户端
class Client {
final Config _config;
final Cache _cache;
final Listener _listener;
final Map<String, String> _configs = {};
int _lastTime = 0;
bool _started = false;
bool stop = false;
Client(Config config)
: _config = config,
_cache = Cache(config.cacheDir),
_listener = Listener();
/// 注册变更回调
void onChange(Function handler) {
_listener.onChange(handler);
}
/// 获取配置值
String? get(String key) {
return _configs[key];
}
/// 获取所有配置
Map<String, String> getAll() {
return Map.from(_configs);
}
/// 启动客户端
Future<void> start() async {
if (_started) {
return;
}
_started = true;
try {
await _fetchAll();
} catch (e) {
stderr.writeln('[ConfigClient] fetch all configs error: $e, trying cache');
final cached = _cache.read();
if (cached != null) {
_applyConfigs(cached);
stderr.writeln('[ConfigClient] loaded ${_configs.length} configs from cache');
} else {
throw Exception('fetch configs failed and no cache: $e');
}
}
}
/// 同步一次
Future<void> syncOnce() async {
try {
await _fetchChanged();
} catch (e) {
stderr.writeln('[ConfigClient] sync error: $e');
}
}
/// HTTP POST 请求
Future<Map<String, dynamic>> _httpPost(String url, Map<String, dynamic> payload) async {
final uri = Uri.parse(url);
final httpClient = HttpClient();
try {
final request = await httpClient.postUrl(uri).timeout(Duration(seconds: 10));
request.headers.set('Content-Type', 'application/json');
request.write(json.encode(payload));
final response = await request.close().timeout(Duration(seconds: 10));
if (response.statusCode != 200) {
throw Exception('HTTP ${response.statusCode}');
}
final body = await response.transform(utf8.decoder).join();
try {
return json.decode(body) as Map<String, dynamic>;
} catch (e) {
throw Exception('JSON decode error: $e');
}
} finally {
httpClient.close();
}
}
/// 全量拉取配置
Future<void> _fetchAll() async {
final url = '${_config.baseUrl}/api/fetchAllConfigs';
final payload = {
'appName': _config.appName,
'env': _config.env,
'version': _config.version,
'ignoreGlobal': _config.ignoreGlobal,
'token': _config.token,
};
final result = await _httpPost(url, payload);
if (result.containsKey('code') && result['code'] != 0) {
throw Exception('server error: ${result['msg'] ?? 'unknown'}');
}
if (result.containsKey('latest.fetch.timestamp')) {
final ts = result['latest.fetch.timestamp'];
if (ts is num) {
_lastTime = ts.toInt();
} else if (ts is String) {
_lastTime = int.tryParse(ts) ?? 0;
}
}
_cache.write(result);
_applyConfigs(result);
stderr.writeln('[ConfigClient] fetched ${_configs.length} configs');
}
/// 增量拉取变更
Future<void> _fetchChanged() async {
final url = '${_config.baseUrl}/api/fetchChangedConfigs';
final payload = {
'appName': _config.appName,
'env': _config.env,
'version': _config.version,
'lastTime': _lastTime,
'ignoreGlobal': _config.ignoreGlobal,
'token': _config.token,
};
final data = await _httpPost(url, payload);
if (data.containsKey('code') && data['code'] != 0) {
throw Exception('server error code: ${data['code']}');
}
final changedData = (data['data'] ?? {}) as Map<String, dynamic>;
if (changedData.containsKey('latest.fetch.timestamp')) {
final ts = changedData['latest.fetch.timestamp'];
if (ts is num) {
_lastTime = ts.toInt();
} else if (ts is String) {
_lastTime = int.tryParse(ts) ?? 0;
}
changedData.remove('latest.fetch.timestamp');
}
// 处理删除的配置
if (changedData.containsKey('deletedKeys') && changedData['deletedKeys'] != null) {
final deletedKeysStr = changedData['deletedKeys'] as String;
if (deletedKeysStr.isNotEmpty) {
final deletedKeys = deletedKeysStr.split(',');
for (final key in deletedKeys) {
_configs.remove(key);
}
stderr.writeln('[ConfigClient] ${deletedKeys.length} configs deleted: ${deletedKeys.join(', ')}');
}
changedData.remove('deletedKeys');
}
// 处理变更的配置
final changed = <String, String>{};
changedData.forEach((key, value) {
if (value is String) {
_configs[key] = value;
changed[key] = value;
}
});
if (changed.isNotEmpty) {
_listener.notify(changed);
stderr.writeln('[ConfigClient] ${changed.length} configs changed: ${changed.keys.join(', ')}');
}
}
/// 应用配置到本地存储
void _applyConfigs(Map<String, dynamic> data) {
data.forEach((key, value) {
if (key.startsWith('kenaito.') || key == 'latest.fetch.timestamp') {
return;
}
if (value is String) {
_configs[key] = value;
}
});
}
}
@@ -0,0 +1,22 @@
/// 配置类
class Config {
final String baseUrl;
final String appName;
final String env;
final String version;
final String token;
final int syncInterval;
final bool ignoreGlobal;
final String cacheDir;
Config({
this.baseUrl = 'http://localhost:8080',
this.appName = '',
this.env = 'dev',
this.version = 'latest',
this.token = '',
this.syncInterval = 10,
this.ignoreGlobal = false,
this.cacheDir = '.',
});
}
@@ -0,0 +1,22 @@
import 'dart:io';
/// 变更监听器
class Listener {
final List<Function> _handlers = [];
/// 注册变更回调
void onChange(Function handler) {
_handlers.add(handler);
}
/// 通知所有监听器
void notify(Map<String, String> changed) {
for (final handler in _handlers) {
try {
handler(changed);
} catch (e) {
stderr.writeln('[ConfigClient] change handler error: $e');
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
name: kenaito_conf
description: Kenaito Config Center Dart Client
version: 1.0.3
homepage: https://github.com/odboy/kenaito-conf
environment:
sdk: '>=2.12.0 <4.0.0'