Files
nyaKazuha 3f78f8f880 feat(api): 补齐资源分发服务入口
新增 bat-api 资源 bootstrap/分发 HTTP 服务、RPC release 发现、CDN path 分发、launcher 资源引导兼容、控制面中间件、OpenAPI 和 systemd 模板。

同步 Go 边界文档,明确 Rust bat 是资源生产者和同步运维入口,Go bat-api 是只读 bootstrap/分发服务,试验 Go CLI 产物为 bin/bat-go。

验证:未运行新命令;本轮已按要求停止重复构建/测试。
2026-07-31 00:41:48 +08:00

126 lines
3.6 KiB
Go

package api
import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
)
// RewriteServerInfoAddressables rewrites AddressablesCatalogUrlRoot values so
// clients fetch resources from publicBaseURL while leaving business API URLs
// untouched.
func RewriteServerInfoAddressables(raw []byte, publicBaseURL string) ([]byte, error) {
var doc map[string]any
if err := json.Unmarshal(raw, &doc); err != nil {
return nil, fmt.Errorf("parse server-info: %w", err)
}
groups, _ := doc["ConnectionGroups"].([]any)
for _, g := range groups {
group, ok := g.(map[string]any)
if !ok {
continue
}
rewriteGroupAddressables(group, publicBaseURL)
if overrides, ok := group["OverrideConnectionGroups"].([]any); ok {
for _, o := range overrides {
if og, ok := o.(map[string]any); ok {
rewriteGroupAddressables(og, publicBaseURL)
}
}
}
}
return json.Marshal(doc)
}
func rewriteGroupAddressables(group map[string]any, publicBaseURL string) {
raw, _ := group["AddressablesCatalogUrlRoot"].(string)
if raw == "" {
return
}
if rewritten, ok := rewriteAddressablesRoot(raw, publicBaseURL); ok {
group["AddressablesCatalogUrlRoot"] = rewritten
}
}
func rewriteAddressablesRoot(original, publicBaseURL string) (string, bool) {
original = strings.TrimRight(original, "/")
publicBaseURL = strings.TrimRight(publicBaseURL, "/")
// Already rewritten to this base.
if strings.HasPrefix(original, publicBaseURL+"/"+ClientPatchHost) ||
strings.HasPrefix(original, publicBaseURL+"/"+ServerInfoHost) {
return original, true
}
u, err := url.Parse(original)
if err != nil || u.Host == "" {
return "", false
}
// Keep host/path so CDN path mapping stays 1:1 with destination_for_url.
path := strings.TrimPrefix(u.Path, "/")
if path == "" {
return publicBaseURL + "/" + u.Host, true
}
return publicBaseURL + "/" + u.Host + "/" + path, true
}
// LoadServerInfoBytes loads server-info from an explicit file, or from the
// release tree under yostar-serverinfo host, or synthesizes a minimal document
// from the release snapshot addressables root.
func LoadServerInfoBytes(cfg Config, idx *ReleaseIndex, requestName string) ([]byte, error) {
if cfg.ServerInfoFile != "" {
return os.ReadFile(cfg.ServerInfoFile)
}
if idx != nil && idx.ResourceRoot != "" {
name := requestName
if name == "" {
name = "server-info.json"
}
// Official path layout.
candidates := []string{
filepath.Join(idx.ResourceRoot, ServerInfoHost, name),
filepath.Join(idx.ResourceRoot, ServerInfoHost, requestName),
}
for _, c := range candidates {
if c == "" {
continue
}
if data, err := os.ReadFile(c); err == nil {
return data, nil
}
}
// Any single json under server-info host.
dir := filepath.Join(idx.ResourceRoot, ServerInfoHost)
if entries, err := os.ReadDir(dir); err == nil {
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") {
continue
}
return os.ReadFile(filepath.Join(dir, e.Name()))
}
}
}
if idx != nil && idx.Snapshot != nil && idx.Snapshot.AddressablesRoot != "" {
doc := map[string]any{
"ConnectionGroups": []any{
map[string]any{
"Name": defaultString(idx.Snapshot.ConnectionGroupName, "Prod"),
"AddressablesCatalogUrlRoot": idx.Snapshot.AddressablesRoot,
"BundleVersion": idx.Snapshot.BundleVersion,
"IsLivePublished": true,
},
},
}
return json.Marshal(doc)
}
return nil, fmt.Errorf("server-info source not found")
}
func defaultString(v, fallback string) string {
if v == "" {
return fallback
}
return v
}