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:
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user