Files
BlueArchiveToolkit/internal/api/launcher.go
T
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

209 lines
7.0 KiB
Go

package api
import (
"fmt"
"net/http"
"net/url"
"strings"
)
// LauncherAPIHost is the official JP launcher API host observed from the PC launcher.
const LauncherAPIHost = "api-launcher-jp.yo-star.com"
func (s *Server) handleLauncherBootstrap(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
return
}
sum := s.releaseSummary()
status := http.StatusOK
if !sum.Ready || sum.Snapshot == nil {
status = http.StatusServiceUnavailable
}
writeNoStoreJSON(w, status, s.launcherBootstrapBody(sum))
}
func (s *Server) handleLauncherGameConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
return
}
sum := s.releaseSummary()
if !sum.Ready || sum.Snapshot == nil {
writeLauncherEnvelope(w, http.StatusServiceUnavailable, 503, "release not ready", LauncherGameConfigJSONData{
ResourceBootstrapURL: s.launcherBootstrapURL(),
Scope: "resource_bootstrap_only",
})
return
}
snap := sum.Snapshot
data := LauncherGameConfigData{
GameLatestVersion: defaultString(s.launcherLatestVersion(snap), snap.AppVersion),
GameLatestFilePath: s.launcherLatestFilePath(snap),
GameStartExeName: s.launcherStartExeName(snap),
GameStartParams: s.launcherStartParams(snap),
ResourceBootstrapURL: s.launcherBootstrapURL(),
ServerInfoURL: s.serverInfoURL(),
ClientPatchBaseURL: s.clientPatchBaseURL(),
Scope: "resource_bootstrap_only",
}
if snap.LauncherMetadata != nil && snap.LauncherMetadata.GameLowestVersion != "" {
data.GameLowestVersion = snap.LauncherMetadata.GameLowestVersion
}
writeLauncherEnvelope(w, http.StatusOK, 200, "ok", data)
}
func (s *Server) handleLauncherGameConfigJSON(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
return
}
sum := s.releaseSummary()
if !sum.Ready || sum.Snapshot == nil {
writeLauncherEnvelope(w, http.StatusServiceUnavailable, 503, "release not ready", LauncherGameConfigJSONData{
URL: s.launcherBootstrapURL(),
ResourceBootstrapURL: s.launcherBootstrapURL(),
Scope: "resource_bootstrap_only",
})
return
}
writeLauncherEnvelope(w, http.StatusOK, 200, "ok", LauncherGameConfigJSONData{
URL: s.launcherBootstrapJSONURL(r.URL.Query()),
ResourceBootstrapURL: s.launcherBootstrapURL(),
PackageUpdateManifest: false,
Scope: "resource_bootstrap_only",
})
}
func (s *Server) handleLauncherCdnConfig(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
return
}
writeLauncherEnvelope(w, http.StatusOK, 200, "ok", LauncherCdnConfigData{
PrimaryCDN: strings.TrimRight(s.cfg.PublicBaseURL, "/"),
BackupCDN: strings.TrimRight(s.cfg.PublicBaseURL, "/"),
ResourceBootstrapURL: s.launcherBootstrapURL(),
PackageUpdateManifest: false,
Scope: "resource_bootstrap_only",
})
}
func (s *Server) launcherBootstrapBody(sum ReleaseSummary) LauncherBootstrapResponse {
publicBase := strings.TrimRight(s.cfg.PublicBaseURL, "/")
body := LauncherBootstrapResponse{
Service: "bat-api",
Ready: sum.Ready,
LauncherAPI: LauncherAPIInfo{
Host: LauncherAPIHost,
ObservedResourceRelevantEndpoints: []string{
"/api/launcher/game/config",
"/api/launcher/game/config/json",
"/api/launcher/advanced/game/download/cdn",
},
CompatibilityScope: "resource_bootstrap_only",
},
Policy: LauncherPolicy{
Source: "rust_bat_snapshot",
DownloadsLauncherPackage: false,
EmulatesLoginOrGateway: false,
PackageUpdateManifest: false,
},
Resource: LauncherResource{
Release: sum.Snapshot,
ServerInfoURL: s.serverInfoURL(),
ClientPatchBaseURL: s.clientPatchBaseURL(),
},
Endpoints: LauncherEndpointSet{
Bootstrap: publicBase + "/v1/bootstrap",
LauncherBootstrap: publicBase + "/v1/launcher/bootstrap",
ServerInfo: s.serverInfoURL(),
ClientPatchBase: s.clientPatchBaseURL(),
LauncherGameConfig: publicBase + "/" + LauncherAPIHost + "/api/launcher/game/config",
LauncherGameConfigJSON: publicBase + "/" + LauncherAPIHost + "/api/launcher/game/config/json",
LauncherCdnConfig: publicBase + "/" + LauncherAPIHost + "/api/launcher/advanced/game/download/cdn",
},
}
if sum.Snapshot != nil {
body.GameMainConfig = sum.Snapshot.GameMainConfig
body.LauncherMetadata = sum.Snapshot.LauncherMetadata
if rewritten, ok := rewriteAddressablesRoot(sum.Snapshot.AddressablesRoot, publicBase); ok {
body.AddressablesCatalogURLRoot = rewritten
}
}
return body
}
func (s *Server) releaseSummary() ReleaseSummary {
if idx := s.index(); idx != nil {
return idx.Summary()
}
return ReleaseSummary{}
}
func (s *Server) serverInfoURL() string {
return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + ServerInfoHost + "/server-info.json"
}
func (s *Server) clientPatchBaseURL() string {
return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + ClientPatchHost
}
func (s *Server) launcherBootstrapURL() string {
return strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/v1/launcher/bootstrap"
}
func (s *Server) launcherBootstrapJSONURL(query url.Values) string {
base := strings.TrimRight(s.cfg.PublicBaseURL, "/") + "/" + LauncherAPIHost + "/api/launcher/resource/bootstrap.json"
values := url.Values{}
for _, key := range []string{"version", "file_path"} {
if value := query.Get(key); value != "" {
values.Set(key, value)
}
}
if encoded := values.Encode(); encoded != "" {
return base + "?" + encoded
}
return base
}
func (s *Server) launcherLatestVersion(snap *SnapshotSummary) string {
if snap.LauncherMetadata != nil && snap.LauncherMetadata.GameLatestVersion != "" {
return snap.LauncherMetadata.GameLatestVersion
}
return snap.AppVersion
}
func (s *Server) launcherLatestFilePath(snap *SnapshotSummary) string {
if snap.LauncherMetadata != nil {
return snap.LauncherMetadata.GameLatestFilePath
}
return ""
}
func (s *Server) launcherStartExeName(snap *SnapshotSummary) string {
if snap.LauncherMetadata != nil {
return snap.LauncherMetadata.GameStartExeName
}
return ""
}
func (s *Server) launcherStartParams(snap *SnapshotSummary) []string {
if snap.LauncherMetadata != nil && len(snap.LauncherMetadata.GameStartParams) > 0 {
return append([]string(nil), snap.LauncherMetadata.GameStartParams...)
}
return []string{}
}
func writeLauncherEnvelope[T any](w http.ResponseWriter, status int, code int, message string, data T) {
writeNoStoreJSON(w, status, LauncherEnvelope[T]{
Code: code,
Message: message,
Data: data,
})
}
func launcherHostPath(path string) string {
return fmt.Sprintf("/%s%s", LauncherAPIHost, path)
}