mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 10:54:55 +08:00
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。 验证:未运行新命令;本轮已按要求停止重复构建/测试。
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user