From 5f67274e3fc7b18bb7b651c99481937de65c2cb8 Mon Sep 17 00:00:00 2001 From: Odboy Date: Wed, 19 Aug 2026 21:11:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20Bash=20=E5=92=8C?= =?UTF-8?q?=20Dart=20=E5=AE=A2=E6=88=B7=E7=AB=AF=20SDK=20=E5=8F=8A=20demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 --- Dockerfile.bash | 17 ++ Dockerfile.dart | 11 + README.md | 103 ++++++- README_DOCKERFILE.md | 32 +++ README_EN.md | 103 ++++++- kenaito-conf-bash-client-demo/app.sh | 82 ++++++ kenaito-conf-bash-client/README.md | 0 kenaito-conf-bash-client/README_EN.md | 0 kenaito-conf-bash-client/lib/kenaito_conf.sh | 264 ++++++++++++++++++ kenaito-conf-dart-client-demo/app.dart | 79 ++++++ kenaito-conf-dart-client/README.md | 83 ++++++ kenaito-conf-dart-client/README_EN.md | 83 ++++++ .../lib/kenaito_conf.dart | 8 + kenaito-conf-dart-client/lib/src/cache.dart | 34 +++ kenaito-conf-dart-client/lib/src/client.dart | 197 +++++++++++++ kenaito-conf-dart-client/lib/src/config.dart | 22 ++ .../lib/src/listener.dart | 22 ++ kenaito-conf-dart-client/pubspec.yaml | 7 + 18 files changed, 1143 insertions(+), 4 deletions(-) create mode 100644 Dockerfile.bash create mode 100644 Dockerfile.dart create mode 100644 kenaito-conf-bash-client-demo/app.sh create mode 100644 kenaito-conf-bash-client/README.md create mode 100644 kenaito-conf-bash-client/README_EN.md create mode 100644 kenaito-conf-bash-client/lib/kenaito_conf.sh create mode 100644 kenaito-conf-dart-client-demo/app.dart create mode 100644 kenaito-conf-dart-client/README.md create mode 100644 kenaito-conf-dart-client/README_EN.md create mode 100644 kenaito-conf-dart-client/lib/kenaito_conf.dart create mode 100644 kenaito-conf-dart-client/lib/src/cache.dart create mode 100644 kenaito-conf-dart-client/lib/src/client.dart create mode 100644 kenaito-conf-dart-client/lib/src/config.dart create mode 100644 kenaito-conf-dart-client/lib/src/listener.dart create mode 100644 kenaito-conf-dart-client/pubspec.yaml diff --git a/Dockerfile.bash b/Dockerfile.bash new file mode 100644 index 0000000..92b1ae5 --- /dev/null +++ b/Dockerfile.bash @@ -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"] diff --git a/Dockerfile.dart b/Dockerfile.dart new file mode 100644 index 0000000..da88c14 --- /dev/null +++ b/Dockerfile.dart @@ -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"] diff --git a/README.md b/README.md index 7998049..764bd02 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ - **后端**: Go + Gin + xorm + MySQL - **前端**: 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-demo/ # Rust 客户端示例 ├── 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 ``` +### 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 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(需 JWT 认证) diff --git a/README_DOCKERFILE.md b/README_DOCKERFILE.md index 2d90fe6..4b4cc33 100644 --- a/README_DOCKERFILE.md +++ b/README_DOCKERFILE.md @@ -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 ``` +## 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 +``` + ## 环境变量说明 | 变量名 | 说明 | 默认值 | diff --git a/README_EN.md b/README_EN.md index ba5cb32..623d701 100644 --- a/README_EN.md +++ b/README_EN.md @@ -6,7 +6,7 @@ A centralized configuration management platform rebuilt with Go, supporting mult - **Backend**: Go + Gin + xorm + MySQL - **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 @@ -35,7 +35,11 @@ kenaito-conf/ ├── kenaito-conf-rust-client/ # Rust client SDK (Rust 2021+) ├── kenaito-conf-rust-client-demo/ # Rust client demo ├── 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 @@ -472,6 +476,101 @@ client.sync_once 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 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 ### Admin API (JWT Authentication Required) diff --git a/kenaito-conf-bash-client-demo/app.sh b/kenaito-conf-bash-client-demo/app.sh new file mode 100644 index 0000000..dbf701c --- /dev/null +++ b/kenaito-conf-bash-client-demo/app.sh @@ -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 diff --git a/kenaito-conf-bash-client/README.md b/kenaito-conf-bash-client/README.md new file mode 100644 index 0000000..e69de29 diff --git a/kenaito-conf-bash-client/README_EN.md b/kenaito-conf-bash-client/README_EN.md new file mode 100644 index 0000000..e69de29 diff --git a/kenaito-conf-bash-client/lib/kenaito_conf.sh b/kenaito-conf-bash-client/lib/kenaito_conf.sh new file mode 100644 index 0000000..8a3dd6e --- /dev/null +++ b/kenaito-conf-bash-client/lib/kenaito_conf.sh @@ -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 +} diff --git a/kenaito-conf-dart-client-demo/app.dart b/kenaito-conf-dart-client-demo/app.dart new file mode 100644 index 0000000..536c41f --- /dev/null +++ b/kenaito-conf-dart-client-demo/app.dart @@ -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 = { + '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 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)); + } +} diff --git a/kenaito-conf-dart-client/README.md b/kenaito-conf-dart-client/README.md new file mode 100644 index 0000000..a2147c3 --- /dev/null +++ b/kenaito-conf-dart-client/README.md @@ -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 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) +- ✅ 空安全支持 diff --git a/kenaito-conf-dart-client/README_EN.md b/kenaito-conf-dart-client/README_EN.md new file mode 100644 index 0000000..a76f126 --- /dev/null +++ b/kenaito-conf-dart-client/README_EN.md @@ -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 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 diff --git a/kenaito-conf-dart-client/lib/kenaito_conf.dart b/kenaito-conf-dart-client/lib/kenaito_conf.dart new file mode 100644 index 0000000..c521a97 --- /dev/null +++ b/kenaito-conf-dart-client/lib/kenaito_conf.dart @@ -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'; diff --git a/kenaito-conf-dart-client/lib/src/cache.dart b/kenaito-conf-dart-client/lib/src/cache.dart new file mode 100644 index 0000000..0b0d014 --- /dev/null +++ b/kenaito-conf-dart-client/lib/src/cache.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? read() { + try { + final file = File(_cacheFile); + if (!file.existsSync()) { + return null; + } + final content = file.readAsStringSync(); + return json.decode(content) as Map; + } catch (e) { + stderr.writeln('[ConfigClient] cache read error: $e'); + return null; + } + } + + /// 写入缓存 + void write(Map data) { + try { + final file = File(_cacheFile); + file.writeAsStringSync(json.encode(data)); + } catch (e) { + stderr.writeln('[ConfigClient] cache write error: $e'); + } + } +} diff --git a/kenaito-conf-dart-client/lib/src/client.dart b/kenaito-conf-dart-client/lib/src/client.dart new file mode 100644 index 0000000..ac2ba06 --- /dev/null +++ b/kenaito-conf-dart-client/lib/src/client.dart @@ -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 _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 getAll() { + return Map.from(_configs); + } + + /// 启动客户端 + Future 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 syncOnce() async { + try { + await _fetchChanged(); + } catch (e) { + stderr.writeln('[ConfigClient] sync error: $e'); + } + } + + /// HTTP POST 请求 + Future> _httpPost(String url, Map 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; + } catch (e) { + throw Exception('JSON decode error: $e'); + } + } finally { + httpClient.close(); + } + } + + /// 全量拉取配置 + Future _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 _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; + + 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 = {}; + 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 data) { + data.forEach((key, value) { + if (key.startsWith('kenaito.') || key == 'latest.fetch.timestamp') { + return; + } + if (value is String) { + _configs[key] = value; + } + }); + } +} diff --git a/kenaito-conf-dart-client/lib/src/config.dart b/kenaito-conf-dart-client/lib/src/config.dart new file mode 100644 index 0000000..e58e0bd --- /dev/null +++ b/kenaito-conf-dart-client/lib/src/config.dart @@ -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 = '.', + }); +} diff --git a/kenaito-conf-dart-client/lib/src/listener.dart b/kenaito-conf-dart-client/lib/src/listener.dart new file mode 100644 index 0000000..5531c81 --- /dev/null +++ b/kenaito-conf-dart-client/lib/src/listener.dart @@ -0,0 +1,22 @@ +import 'dart:io'; + +/// 变更监听器 +class Listener { + final List _handlers = []; + + /// 注册变更回调 + void onChange(Function handler) { + _handlers.add(handler); + } + + /// 通知所有监听器 + void notify(Map changed) { + for (final handler in _handlers) { + try { + handler(changed); + } catch (e) { + stderr.writeln('[ConfigClient] change handler error: $e'); + } + } + } +} diff --git a/kenaito-conf-dart-client/pubspec.yaml b/kenaito-conf-dart-client/pubspec.yaml new file mode 100644 index 0000000..93033b5 --- /dev/null +++ b/kenaito-conf-dart-client/pubspec.yaml @@ -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'