Files
root 618399f86d feat(server): hot-reload config store and reloadable infra clients
- Replace single Load() with a watching Store that re-reads config files
  (and config.local.yaml) and fans out a ReloadEvent with a per-field diff
  so consumers can decide whether the change is hot-applicable or requires
  a process restart.
- Wrap llm, retrieval, vector store, and object storage clients in
  Reloadable* shells so the bootstrap can swap their underlying impls when
  config changes without re-instantiating handlers.
- Make jwt.Manager and ops TokenIssuer mutable under a lock so secrets and
  TTLs can be rotated live; thread default plan code through a setter on
  the ops AdminUserService.
- Wire ConfigStore through bootstrap and every cmd/main.go, scheduler /
  worker / tenant-api / ops-api start the watcher; services and handlers
  take a config.Provider so they always read current values for things
  like generation.stream_enabled, scheduler dispatch, retrieval, etc.
- Switch shared/config decoding off viper to a Kratos-derived runtime
  package so env placeholders (\${VAR:default}) resolve consistently and
  the same source machinery powers both the loader and the watcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:01:23 +08:00

168 lines
3.5 KiB
Go

package runtime
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"gopkg.in/yaml.v3"
)
type Decoder func(*KeyValue, map[string]any) error
type Resolver func(map[string]any) error
type Merge func(dst, src any) error
type Option func(*options)
type options struct {
sources []Source
decoder Decoder
resolver Resolver
merge Merge
}
func WithSource(s ...Source) Option {
return func(o *options) {
o.sources = s
}
}
func WithDecoder(d Decoder) Option {
return func(o *options) {
o.decoder = d
}
}
func WithResolveActualTypes(enableConvertToType bool) Option {
return func(o *options) {
o.resolver = newActualTypesResolver(enableConvertToType)
}
}
func WithResolver(r Resolver) Option {
return func(o *options) {
o.resolver = r
}
}
func WithMergeFunc(m Merge) Option {
return func(o *options) {
o.merge = m
}
}
func defaultDecoder(src *KeyValue, target map[string]any) error {
if src.Format == "" {
keys := strings.Split(src.Key, ".")
for i, key := range keys {
if i == len(keys)-1 {
target[key] = src.Value
return nil
}
sub := make(map[string]any)
target[key] = sub
target = sub
}
return nil
}
switch strings.ToLower(src.Format) {
case "json":
return json.Unmarshal(src.Value, &target)
case "yaml", "yml":
return yaml.Unmarshal(src.Value, &target)
}
return fmt.Errorf("unsupported key: %s format: %s", src.Key, src.Format)
}
func newActualTypesResolver(enableConvertToType bool) func(map[string]any) error {
return func(input map[string]any) error {
mapper := mapper(input)
return resolver(input, mapper, enableConvertToType)
}
}
func defaultResolver(input map[string]any) error {
mapper := mapper(input)
return resolver(input, mapper, false)
}
func resolver(input map[string]any, mapper func(name string) string, toType bool) error {
var resolve func(map[string]any) error
resolve = func(sub map[string]any) error {
for key, value := range sub {
switch typed := value.(type) {
case string:
sub[key] = expand(typed, mapper, toType)
case map[string]any:
if err := resolve(typed); err != nil {
return err
}
case []any:
for i, item := range typed {
switch child := item.(type) {
case string:
typed[i] = expand(child, mapper, toType)
case map[string]any:
if err := resolve(child); err != nil {
return err
}
}
}
sub[key] = typed
}
}
return nil
}
return resolve(input)
}
func mapper(input map[string]any) func(name string) string {
return func(name string) string {
args := strings.SplitN(strings.TrimSpace(name), ":", 2)
if value, ok := readValue(input, args[0]); ok {
s, _ := value.String()
return s
}
if len(args) > 1 {
return args[1]
}
return ""
}
}
func convertToType(input string) any {
if strings.HasPrefix(input, "\"") && strings.HasSuffix(input, "\"") {
return strings.Trim(input, "\"")
}
if input == "true" || input == "false" {
b, _ := strconv.ParseBool(input)
return b
}
if strings.Contains(input, ".") {
if f, err := strconv.ParseFloat(input, 64); err == nil {
return f
}
}
if i, err := strconv.ParseInt(input, 10, 64); err == nil {
return i
}
return input
}
var placeholderRegexp = regexp.MustCompile(`\${(.*?)}`)
func expand(s string, mapping func(string) string, toType bool) any {
for _, match := range placeholderRegexp.FindAllStringSubmatch(s, -1) {
if len(match) != 2 {
continue
}
mapped := mapping(match[1])
if toType {
return convertToType(mapped)
}
s = strings.ReplaceAll(s, match[0], mapped)
}
return s
}