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,17 @@
|
|||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
bash curl jq ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /kenaito-conf-bash-client-demo
|
||||||
|
|
||||||
|
# Copy client library
|
||||||
|
COPY kenaito-conf-bash-client/ /kenaito-conf-bash-client/
|
||||||
|
|
||||||
|
# Copy demo
|
||||||
|
COPY kenaito-conf-bash-client-demo/app.sh /kenaito-conf-bash-client-demo/app.sh
|
||||||
|
|
||||||
|
RUN chmod +x app.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["bash", "app.sh"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
FROM dart:3.5
|
||||||
|
|
||||||
|
WORKDIR /kenaito-conf-dart-client-demo
|
||||||
|
|
||||||
|
# Copy client library
|
||||||
|
COPY kenaito-conf-dart-client/ /kenaito-conf-dart-client/
|
||||||
|
|
||||||
|
# Copy demo
|
||||||
|
COPY kenaito-conf-dart-client-demo/app.dart /kenaito-conf-dart-client-demo/app.dart
|
||||||
|
|
||||||
|
ENTRYPOINT ["dart", "run", "app.dart"]
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
- **后端**: Go + Gin + xorm + MySQL
|
- **后端**: Go + Gin + xorm + MySQL
|
||||||
- **前端**: Vue 2 + Vite 5 + Element UI 2
|
- **前端**: Vue 2 + Vite 5 + Element UI 2
|
||||||
- **客户端**: Go / Java(Spring Boot 2.x)/ Python / Node.js / Perl / Lua / PHP / .NET / C++ / Rust / Ruby
|
- **客户端**: Go / Java(Spring Boot 2.x)/ Python / Node.js / Perl / Lua / PHP / .NET / C++ / Rust / Ruby / Bash / Dart
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@@ -37,7 +37,11 @@ kenaito-conf/
|
|||||||
├── kenaito-conf-rust-client/ # Rust 客户端 SDK(Rust 2021+)
|
├── kenaito-conf-rust-client/ # Rust 客户端 SDK(Rust 2021+)
|
||||||
├── kenaito-conf-rust-client-demo/ # Rust 客户端示例
|
├── kenaito-conf-rust-client-demo/ # Rust 客户端示例
|
||||||
├── kenaito-conf-ruby-client/ # Ruby 客户端 SDK(Ruby 2.5+)
|
├── kenaito-conf-ruby-client/ # Ruby 客户端 SDK(Ruby 2.5+)
|
||||||
└── kenaito-conf-ruby-client-demo/ # Ruby 客户端示例
|
├── kenaito-conf-ruby-client-demo/ # Ruby 客户端示例
|
||||||
|
├── kenaito-conf-bash-client/ # Bash 客户端 SDK(Bash 4.0+)
|
||||||
|
├── kenaito-conf-bash-client-demo/ # Bash 客户端示例
|
||||||
|
├── kenaito-conf-dart-client/ # Dart 客户端 SDK(Dart 2.12+)
|
||||||
|
└── kenaito-conf-dart-client-demo/ # Dart 客户端示例
|
||||||
```
|
```
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
@@ -478,6 +482,101 @@ client.sync_once
|
|||||||
client.stop_client
|
client.stop_client
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Bash 客户端
|
||||||
|
|
||||||
|
适用于 Bash 4.0+ 脚本、CI/CD 流水线、容器启动脚本等场景。依赖 `curl` 和 `jq`。
|
||||||
|
|
||||||
|
**1. 引入**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source /path/to/kenaito-conf-bash-client/lib/kenaito_conf.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 使用**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
source /path/to/kenaito-conf-bash-client/lib/kenaito_conf.sh
|
||||||
|
|
||||||
|
export KENAITO_BASE_URL="http://localhost:8080"
|
||||||
|
export KENAITO_APP_NAME="my-app"
|
||||||
|
export KENAITO_ENV="daily"
|
||||||
|
export KENAITO_SYNC_INTERVAL=10
|
||||||
|
|
||||||
|
# 变更回调函数(可选)
|
||||||
|
kenaito_on_change() {
|
||||||
|
local changed_keys="$1"
|
||||||
|
echo "Config changed: $changed_keys"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 启动客户端
|
||||||
|
kenaito_start
|
||||||
|
|
||||||
|
# 获取配置
|
||||||
|
value=$(kenaito_get "database.host")
|
||||||
|
|
||||||
|
# 获取所有配置
|
||||||
|
kenaito_get_all
|
||||||
|
|
||||||
|
# 同步循环
|
||||||
|
while true; do
|
||||||
|
kenaito_sync_once
|
||||||
|
sleep "$KENAITO_SYNC_INTERVAL"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dart 客户端
|
||||||
|
|
||||||
|
适用于 Dart 2.12+ 项目(支持空安全)。
|
||||||
|
|
||||||
|
**1. 安装**
|
||||||
|
|
||||||
|
在项目 `pubspec.yaml` 中添加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
kenaito_conf:
|
||||||
|
path: /path/to/kenaito-conf-dart-client
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. 使用**
|
||||||
|
|
||||||
|
```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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## API 概览
|
## API 概览
|
||||||
|
|
||||||
### 管理端 API(需 JWT 认证)
|
### 管理端 API(需 JWT 认证)
|
||||||
|
|||||||
@@ -213,6 +213,38 @@ docker run -it --rm \
|
|||||||
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-ruby-demo
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-ruby-demo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Bash 客户端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f Dockerfile.bash -t kenaito-conf-bash-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
|
docker run -it --rm \
|
||||||
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
|
-e KENAITO_ENV=daily \
|
||||||
|
kenaito-conf-bash-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-bash-demo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dart 客户端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -f Dockerfile.dart -t kenaito-conf-dart-demo .
|
||||||
|
|
||||||
|
# linux or mac
|
||||||
|
docker run -it --rm \
|
||||||
|
-e KENAITO_BASE_URL=http://host.docker.internal:8080 \
|
||||||
|
-e KENAITO_APP_NAME=kenaito-springboot-demo \
|
||||||
|
-e KENAITO_ENV=daily \
|
||||||
|
kenaito-conf-dart-demo
|
||||||
|
|
||||||
|
# windows
|
||||||
|
docker run -it --rm -e KENAITO_BASE_URL=http://host.docker.internal:8080 -e KENAITO_APP_NAME=kenaito-springboot-demo -e KENAITO_ENV=daily kenaito-conf-dart-demo
|
||||||
|
```
|
||||||
|
|
||||||
## 环境变量说明
|
## 环境变量说明
|
||||||
|
|
||||||
| 变量名 | 说明 | 默认值 |
|
| 变量名 | 说明 | 默认值 |
|
||||||
|
|||||||
+101
-2
@@ -6,7 +6,7 @@ A centralized configuration management platform rebuilt with Go, supporting mult
|
|||||||
|
|
||||||
- **Backend**: Go + Gin + xorm + MySQL
|
- **Backend**: Go + Gin + xorm + MySQL
|
||||||
- **Frontend**: Vue 2 + Vite 5 + Element UI 2
|
- **Frontend**: Vue 2 + Vite 5 + Element UI 2
|
||||||
- **Clients**: Go / Java (Spring Boot 2.x) / Python / Node.js / Perl / Lua / PHP / .NET / C++ / Rust / Ruby
|
- **Clients**: Go / Java (Spring Boot 2.x) / Python / Node.js / Perl / Lua / PHP / .NET / C++ / Rust / Ruby / Bash / Dart
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -35,7 +35,11 @@ kenaito-conf/
|
|||||||
├── kenaito-conf-rust-client/ # Rust client SDK (Rust 2021+)
|
├── kenaito-conf-rust-client/ # Rust client SDK (Rust 2021+)
|
||||||
├── kenaito-conf-rust-client-demo/ # Rust client demo
|
├── kenaito-conf-rust-client-demo/ # Rust client demo
|
||||||
├── kenaito-conf-ruby-client/ # Ruby client SDK (Ruby 2.5+)
|
├── kenaito-conf-ruby-client/ # Ruby client SDK (Ruby 2.5+)
|
||||||
└── kenaito-conf-ruby-client-demo/ # Ruby client demo
|
├── kenaito-conf-ruby-client-demo/ # Ruby client demo
|
||||||
|
├── kenaito-conf-bash-client/ # Bash client SDK (Bash 4.0+)
|
||||||
|
├── kenaito-conf-bash-client-demo/ # Bash client demo
|
||||||
|
├── kenaito-conf-dart-client/ # Dart client SDK (Dart 2.12+)
|
||||||
|
└── kenaito-conf-dart-client-demo/ # Dart client demo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -472,6 +476,101 @@ client.sync_once
|
|||||||
client.stop_client
|
client.stop_client
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Bash Client
|
||||||
|
|
||||||
|
For Bash 4.0+ scripts, CI/CD pipelines, container startup scripts, etc. Requires `curl` and `jq`.
|
||||||
|
|
||||||
|
**1. Source**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source /path/to/kenaito-conf-bash-client/lib/kenaito_conf.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Usage**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
source /path/to/kenaito-conf-bash-client/lib/kenaito_conf.sh
|
||||||
|
|
||||||
|
export KENAITO_BASE_URL="http://localhost:8080"
|
||||||
|
export KENAITO_APP_NAME="my-app"
|
||||||
|
export KENAITO_ENV="daily"
|
||||||
|
export KENAITO_SYNC_INTERVAL=10
|
||||||
|
|
||||||
|
# Change callback function (optional)
|
||||||
|
kenaito_on_change() {
|
||||||
|
local changed_keys="$1"
|
||||||
|
echo "Config changed: $changed_keys"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start client
|
||||||
|
kenaito_start
|
||||||
|
|
||||||
|
# Get config
|
||||||
|
value=$(kenaito_get "database.host")
|
||||||
|
|
||||||
|
# Get all configs
|
||||||
|
kenaito_get_all
|
||||||
|
|
||||||
|
# Sync loop
|
||||||
|
while true; do
|
||||||
|
kenaito_sync_once
|
||||||
|
sleep "$KENAITO_SYNC_INTERVAL"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dart Client
|
||||||
|
|
||||||
|
For Dart 2.12+ projects (null-safe).
|
||||||
|
|
||||||
|
**1. Install**
|
||||||
|
|
||||||
|
Add to your project `pubspec.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
kenaito_conf:
|
||||||
|
path: /path/to/kenaito-conf-dart-client
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Usage**
|
||||||
|
|
||||||
|
```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));
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## API Overview
|
## API Overview
|
||||||
|
|
||||||
### Admin API (JWT Authentication Required)
|
### Admin API (JWT Authentication Required)
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Kenaito Config Center Bash Client Demo
|
||||||
|
|
||||||
|
# 引入客户端库
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "${SCRIPT_DIR}/../kenaito-conf-bash-client/lib/kenaito_conf.sh"
|
||||||
|
|
||||||
|
# 配置(可通过环境变量覆盖)
|
||||||
|
export KENAITO_BASE_URL="${KENAITO_BASE_URL:-http://localhost:8080}"
|
||||||
|
export KENAITO_APP_NAME="${KENAITO_APP_NAME:-kenaito-bash-demo}"
|
||||||
|
export KENAITO_ENV="${KENAITO_ENV:-daily}"
|
||||||
|
export KENAITO_VERSION="${KENAITO_VERSION:-latest}"
|
||||||
|
export KENAITO_TOKEN="${KENAITO_TOKEN:-}"
|
||||||
|
export KENAITO_SYNC_INTERVAL="${KENAITO_SYNC_INTERVAL:-10}"
|
||||||
|
export KENAITO_CACHE_DIR="${KENAITO_CACHE_DIR:-.}"
|
||||||
|
|
||||||
|
# 本地默认配置
|
||||||
|
declare -A config_values
|
||||||
|
config_values=(
|
||||||
|
["database.host"]="${DATABASE_HOST:-192.168.1.1}"
|
||||||
|
["database.port"]="${DATABASE_PORT:-8888}"
|
||||||
|
["database.name"]="${DATABASE_NAME:-xxxx}"
|
||||||
|
["database.user"]="${DATABASE_USER:-xxxx}"
|
||||||
|
["database.password"]="${DATABASE_PASSWORD:-xxxxxxxx}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 变更回调函数
|
||||||
|
kenaito_on_change() {
|
||||||
|
local changed_keys="$1"
|
||||||
|
echo "Config changed:"
|
||||||
|
IFS=', ' read -ra keys <<< "$changed_keys"
|
||||||
|
for key in "${keys[@]}"; do
|
||||||
|
local value
|
||||||
|
value=$(kenaito_get "$key")
|
||||||
|
echo " $key = $value"
|
||||||
|
if [ -n "${config_values[$key]+x}" ]; then
|
||||||
|
config_values["$key"]="$value"
|
||||||
|
echo " [updated] $key = $value"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# 启动客户端
|
||||||
|
kenaito_start
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
# 用远程配置覆盖本地默认值
|
||||||
|
for key in "${!config_values[@]}"; do
|
||||||
|
remote_value=$(kenaito_get "$key")
|
||||||
|
if [ -n "$remote_value" ]; then
|
||||||
|
config_values["$key"]="$remote_value"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 统计配置数量
|
||||||
|
count=0
|
||||||
|
for key in "${!_KENAITO_CONFIGS[@]}"; do
|
||||||
|
count=$((count + 1))
|
||||||
|
done
|
||||||
|
echo "loaded $count configs from remote"
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo "Failed to init config client, using local defaults"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 显示当前配置
|
||||||
|
echo "Current configs:"
|
||||||
|
for key in $(echo "${!config_values[@]}" | tr ' ' '\n' | sort); do
|
||||||
|
echo " $key = ${config_values[$key]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Listening for config changes... (press Ctrl+C to stop)"
|
||||||
|
|
||||||
|
# 信号处理
|
||||||
|
trap 'echo ""; echo "Shutting down..."; exit 0' INT TERM
|
||||||
|
|
||||||
|
# 同步循环
|
||||||
|
while true; do
|
||||||
|
kenaito_sync_once
|
||||||
|
sleep "$KENAITO_SYNC_INTERVAL"
|
||||||
|
done
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Kenaito Config Center Bash Client
|
||||||
|
# 依赖: curl, jq
|
||||||
|
# 用法: source kenaito_conf.sh 后调用函数
|
||||||
|
|
||||||
|
KENAITO_CONF_VERSION="1.0.3"
|
||||||
|
|
||||||
|
# 配置变量(使用前需设置)
|
||||||
|
KENAITO_BASE_URL="${KENAITO_BASE_URL:-http://localhost:8080}"
|
||||||
|
KENAITO_APP_NAME="${KENAITO_APP_NAME:-}"
|
||||||
|
KENAITO_ENV="${KENAITO_ENV:-dev}"
|
||||||
|
KENAITO_VERSION="${KENAITO_VERSION:-latest}"
|
||||||
|
KENAITO_TOKEN="${KENAITO_TOKEN:-}"
|
||||||
|
KENAITO_SYNC_INTERVAL="${KENAITO_SYNC_INTERVAL:-10}"
|
||||||
|
KENAITO_IGNORE_GLOBAL="${KENAITO_IGNORE_GLOBAL:-false}"
|
||||||
|
KENAITO_CACHE_DIR="${KENAITO_CACHE_DIR:-.}"
|
||||||
|
|
||||||
|
# 内部状态
|
||||||
|
_KENAITO_LAST_TIME=0
|
||||||
|
_KENAITO_STARTED=false
|
||||||
|
declare -A _KENAITO_CONFIGS
|
||||||
|
|
||||||
|
# 获取缓存文件路径
|
||||||
|
_kenaito_cache_file() {
|
||||||
|
echo "${KENAITO_CACHE_DIR}/config-cache.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 从缓存读取
|
||||||
|
_kenaito_cache_read() {
|
||||||
|
local cache_file
|
||||||
|
cache_file=$(_kenaito_cache_file)
|
||||||
|
if [ -f "$cache_file" ]; then
|
||||||
|
cat "$cache_file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 写入缓存
|
||||||
|
_kenaito_cache_write() {
|
||||||
|
local data="$1"
|
||||||
|
local cache_file
|
||||||
|
cache_file=$(_kenaito_cache_file)
|
||||||
|
mkdir -p "$(dirname "$cache_file")"
|
||||||
|
echo "$data" > "$cache_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP POST 请求
|
||||||
|
_kenaito_http_post() {
|
||||||
|
local url="$1"
|
||||||
|
local payload="$2"
|
||||||
|
local response_file
|
||||||
|
local http_code_file
|
||||||
|
local http_code
|
||||||
|
|
||||||
|
response_file=$(mktemp)
|
||||||
|
http_code_file=$(mktemp)
|
||||||
|
|
||||||
|
curl -s -o "$response_file" -w "%{http_code}" -X POST \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$payload" \
|
||||||
|
--connect-timeout 10 \
|
||||||
|
--max-time 10 \
|
||||||
|
"$url" > "$http_code_file" 2>/dev/null
|
||||||
|
|
||||||
|
local curl_exit=$?
|
||||||
|
http_code=$(cat "$http_code_file")
|
||||||
|
|
||||||
|
if [ $curl_exit -ne 0 ]; then
|
||||||
|
echo "ERROR: curl failed with exit code $curl_exit" >&2
|
||||||
|
rm -f "$response_file" "$http_code_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$http_code" != "200" ]; then
|
||||||
|
echo "ERROR: HTTP $http_code" >&2
|
||||||
|
rm -f "$response_file" "$http_code_file"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$response_file"
|
||||||
|
rm -f "$response_file" "$http_code_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 应用配置到本地存储
|
||||||
|
_kenaito_apply_configs() {
|
||||||
|
local data="$1"
|
||||||
|
_KENAITO_CONFIGS=()
|
||||||
|
|
||||||
|
local keys
|
||||||
|
keys=$(echo "$data" | jq -r 'keys[]' 2>/dev/null)
|
||||||
|
|
||||||
|
for key in $keys; do
|
||||||
|
if [[ "$key" == kenaito.* ]] || [[ "$key" == "latest.fetch.timestamp" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
local value
|
||||||
|
value=$(echo "$data" | jq -r --arg k "$key" '.[$k] // empty' 2>/dev/null)
|
||||||
|
if [ -n "$value" ]; then
|
||||||
|
_KENAITO_CONFIGS["$key"]="$value"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取配置值
|
||||||
|
kenaito_get() {
|
||||||
|
local key="$1"
|
||||||
|
echo "${_KENAITO_CONFIGS[$key]:-}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取所有配置(输出为 key=value 格式)
|
||||||
|
kenaito_get_all() {
|
||||||
|
for key in "${!_KENAITO_CONFIGS[@]}"; do
|
||||||
|
echo "$key=${_KENAITO_CONFIGS[$key]}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# 全量拉取配置
|
||||||
|
kenaito_fetch_all() {
|
||||||
|
local url="${KENAITO_BASE_URL}/api/fetchAllConfigs"
|
||||||
|
local payload
|
||||||
|
payload=$(jq -c -n \
|
||||||
|
--arg appName "$KENAITO_APP_NAME" \
|
||||||
|
--arg env "$KENAITO_ENV" \
|
||||||
|
--arg version "$KENAITO_VERSION" \
|
||||||
|
--argjson ignoreGlobal "$KENAITO_IGNORE_GLOBAL" \
|
||||||
|
--arg token "$KENAITO_TOKEN" \
|
||||||
|
'{appName: $appName, env: $env, version: $version, ignoreGlobal: $ignoreGlobal, token: $token}')
|
||||||
|
|
||||||
|
local data
|
||||||
|
data=$(_kenaito_http_post "$url" "$payload")
|
||||||
|
local exit_code=$?
|
||||||
|
if [ $exit_code -ne 0 ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local code
|
||||||
|
code=$(echo "$data" | jq -r '.code // empty' 2>/dev/null)
|
||||||
|
if [ -n "$code" ] && [ "$code" != "0" ]; then
|
||||||
|
local msg
|
||||||
|
msg=$(echo "$data" | jq -r '.msg // "unknown"' 2>/dev/null)
|
||||||
|
echo "ERROR: server error: $msg" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local timestamp
|
||||||
|
timestamp=$(echo "$data" | jq -r '."latest.fetch.timestamp" // empty' 2>/dev/null)
|
||||||
|
if [ -n "$timestamp" ]; then
|
||||||
|
_KENAITO_LAST_TIME=$timestamp
|
||||||
|
fi
|
||||||
|
|
||||||
|
_kenaito_cache_write "$data"
|
||||||
|
_kenaito_apply_configs "$data"
|
||||||
|
|
||||||
|
local count=${#_KENAITO_CONFIGS[@]}
|
||||||
|
echo "[ConfigClient] fetched $count configs" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# 增量拉取变更
|
||||||
|
kenaito_fetch_changed() {
|
||||||
|
local url="${KENAITO_BASE_URL}/api/fetchChangedConfigs"
|
||||||
|
local payload
|
||||||
|
payload=$(jq -c -n \
|
||||||
|
--arg appName "$KENAITO_APP_NAME" \
|
||||||
|
--arg env "$KENAITO_ENV" \
|
||||||
|
--arg version "$KENAITO_VERSION" \
|
||||||
|
--argjson lastTime "$_KENAITO_LAST_TIME" \
|
||||||
|
--argjson ignoreGlobal "$KENAITO_IGNORE_GLOBAL" \
|
||||||
|
--arg token "$KENAITO_TOKEN" \
|
||||||
|
'{appName: $appName, env: $env, version: $version, lastTime: $lastTime, ignoreGlobal: $ignoreGlobal, token: $token}')
|
||||||
|
|
||||||
|
local result
|
||||||
|
result=$(_kenaito_http_post "$url" "$payload")
|
||||||
|
local exit_code=$?
|
||||||
|
if [ $exit_code -ne 0 ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local code
|
||||||
|
code=$(echo "$result" | jq -r '.code // empty' 2>/dev/null)
|
||||||
|
if [ -n "$code" ] && [ "$code" != "0" ]; then
|
||||||
|
echo "ERROR: server error code: $code" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local changed_data
|
||||||
|
changed_data=$(echo "$result" | jq -c '.data // {}' 2>/dev/null)
|
||||||
|
|
||||||
|
local timestamp
|
||||||
|
timestamp=$(echo "$changed_data" | jq -r '."latest.fetch.timestamp" // empty' 2>/dev/null)
|
||||||
|
if [ -n "$timestamp" ]; then
|
||||||
|
_KENAITO_LAST_TIME=$timestamp
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 处理删除的配置
|
||||||
|
local deleted_keys
|
||||||
|
deleted_keys=$(echo "$changed_data" | jq -r '.deletedKeys // empty' 2>/dev/null)
|
||||||
|
if [ -n "$deleted_keys" ]; then
|
||||||
|
IFS=',' read -ra keys <<< "$deleted_keys"
|
||||||
|
for key in "${keys[@]}"; do
|
||||||
|
unset "_KENAITO_CONFIGS[$key]"
|
||||||
|
done
|
||||||
|
local count=${#keys[@]}
|
||||||
|
echo "[ConfigClient] $count configs deleted: $deleted_keys" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 处理变更的配置
|
||||||
|
local changed_count=0
|
||||||
|
local changed_keys_list=""
|
||||||
|
local keys
|
||||||
|
keys=$(echo "$changed_data" | jq -r 'keys[]' 2>/dev/null)
|
||||||
|
|
||||||
|
for key in $keys; do
|
||||||
|
if [[ "$key" == "latest.fetch.timestamp" ]] || [[ "$key" == "deletedKeys" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
local value
|
||||||
|
value=$(echo "$changed_data" | jq -r --arg k "$key" '.[$k] // empty' 2>/dev/null)
|
||||||
|
if [ -n "$value" ]; then
|
||||||
|
_KENAITO_CONFIGS["$key"]="$value"
|
||||||
|
if [ -n "$changed_keys_list" ]; then
|
||||||
|
changed_keys_list="$changed_keys_list, $key"
|
||||||
|
else
|
||||||
|
changed_keys_list="$key"
|
||||||
|
fi
|
||||||
|
changed_count=$((changed_count + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $changed_count -gt 0 ]; then
|
||||||
|
if type kenaito_on_change &>/dev/null; then
|
||||||
|
kenaito_on_change "$changed_keys_list"
|
||||||
|
fi
|
||||||
|
echo "[ConfigClient] $changed_count configs changed: $changed_keys_list" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 启动客户端
|
||||||
|
kenaito_start() {
|
||||||
|
if [ "$_KENAITO_STARTED" = true ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
_KENAITO_STARTED=true
|
||||||
|
|
||||||
|
if ! kenaito_fetch_all; then
|
||||||
|
echo "[ConfigClient] fetch all configs error, trying cache" >&2
|
||||||
|
local cached
|
||||||
|
if cached=$(_kenaito_cache_read); then
|
||||||
|
_kenaito_apply_configs "$cached"
|
||||||
|
local count=${#_KENAITO_CONFIGS[@]}
|
||||||
|
echo "[ConfigClient] loaded $count configs from cache" >&2
|
||||||
|
else
|
||||||
|
echo "[ConfigClient] fetch configs failed and no cache" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 同步一次
|
||||||
|
kenaito_sync_once() {
|
||||||
|
if ! kenaito_fetch_changed; then
|
||||||
|
echo "[ConfigClient] sync error" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import '../kenaito-conf-dart-client/lib/kenaito_conf.dart';
|
||||||
|
|
||||||
|
String getEnvVar(String key, String defaultValue) {
|
||||||
|
final value = Platform.environment[key];
|
||||||
|
return (value != null && value.isNotEmpty) ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
// 配置值(可被远程配置覆盖)
|
||||||
|
final configValues = <String, String>{
|
||||||
|
'database.host': getEnvVar('DATABASE_HOST', '192.168.1.1'),
|
||||||
|
'database.port': getEnvVar('DATABASE_PORT', '8888'),
|
||||||
|
'database.name': getEnvVar('DATABASE_NAME', 'xxxx'),
|
||||||
|
'database.user': getEnvVar('DATABASE_USER', 'xxxx'),
|
||||||
|
'database.password': getEnvVar('DATABASE_PASSWORD', 'xxxxxxxx'),
|
||||||
|
};
|
||||||
|
|
||||||
|
final config = Config(
|
||||||
|
baseUrl: getEnvVar('KENAITO_BASE_URL', 'http://localhost:8080'),
|
||||||
|
appName: getEnvVar('KENAITO_APP_NAME', 'kenaito-dart-demo'),
|
||||||
|
env: getEnvVar('KENAITO_ENV', 'daily'),
|
||||||
|
version: getEnvVar('KENAITO_VERSION', 'latest'),
|
||||||
|
token: getEnvVar('KENAITO_TOKEN', ''),
|
||||||
|
syncInterval: int.parse(getEnvVar('KENAITO_SYNC_INTERVAL', '10')),
|
||||||
|
cacheDir: getEnvVar('KENAITO_CACHE_DIR', '.'),
|
||||||
|
);
|
||||||
|
|
||||||
|
final client = Client(config);
|
||||||
|
|
||||||
|
// 监听配置变更
|
||||||
|
client.onChange((Map<String, String> changed) {
|
||||||
|
print('Config changed:');
|
||||||
|
changed.forEach((key, value) {
|
||||||
|
print(' $key = $value');
|
||||||
|
if (configValues.containsKey(key)) {
|
||||||
|
configValues[key] = value;
|
||||||
|
print(' [updated] $key = $value');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动客户端
|
||||||
|
try {
|
||||||
|
await client.start();
|
||||||
|
|
||||||
|
final remote = client.getAll();
|
||||||
|
configValues.forEach((key, _) {
|
||||||
|
if (remote.containsKey(key)) {
|
||||||
|
configValues[key] = remote[key]!;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
print('loaded ${remote.length} configs from remote\n');
|
||||||
|
} catch (e) {
|
||||||
|
print('Failed to init config client, using local defaults: $e\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示当前配置
|
||||||
|
print('Current configs:');
|
||||||
|
final sortedKeys = configValues.keys.toList()..sort();
|
||||||
|
for (final key in sortedKeys) {
|
||||||
|
print(' $key = ${configValues[key]}');
|
||||||
|
}
|
||||||
|
|
||||||
|
print('\nListening for config changes... (press Ctrl+C to stop)\n');
|
||||||
|
|
||||||
|
// 信号处理
|
||||||
|
ProcessSignal.sigint.watch().listen((_) {
|
||||||
|
print('\nShutting down...');
|
||||||
|
client.stop = true;
|
||||||
|
exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 同步循环
|
||||||
|
while (!client.stop) {
|
||||||
|
await client.syncOnce();
|
||||||
|
await Future.delayed(Duration(seconds: config.syncInterval));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
- ✅ 空安全支持
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
Reference in New Issue
Block a user