diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go index 273c1e8..11e4248 100644 --- a/apps/api/internal/config/config.go +++ b/apps/api/internal/config/config.go @@ -22,6 +22,7 @@ type Config struct { ServerMainBaseURL string ServerMainInternalToken string PublicBaseURL string + WebBaseURL string LocalGeneratedStorageDir string LocalUploadedStorageDir string LocalTempAssetTTLHours int @@ -49,6 +50,7 @@ func Load() Config { ), ServerMainInternalToken: env("SERVER_MAIN_INTERNAL_TOKEN", ""), PublicBaseURL: strings.TrimRight(env("AI_GATEWAY_PUBLIC_BASE_URL", env("PUBLIC_BASE_URL", "")), "/"), + WebBaseURL: strings.TrimRight(env("AI_GATEWAY_WEB_BASE_URL", env("GATEWAY_WEB_BASE_URL", env("PUBLIC_WEB_BASE_URL", ""))), "/"), LocalGeneratedStorageDir: env("AI_GATEWAY_GENERATED_STORAGE_DIR", env("LOCAL_GENERATED_STORAGE_DIR", env("AI_GATEWAY_STATIC_STORAGE_DIR", DefaultLocalGeneratedStorageDir))), LocalUploadedStorageDir: env("AI_GATEWAY_UPLOADED_STORAGE_DIR", env("LOCAL_UPLOADED_STORAGE_DIR", DefaultLocalUploadedStorageDir)), LocalTempAssetTTLHours: envInt("AI_GATEWAY_LOCAL_TEMP_ASSET_TTL_HOURS", 24), diff --git a/apps/api/internal/httpapi/core_flow_integration_test.go b/apps/api/internal/httpapi/core_flow_integration_test.go index 083c325..c9e9fce 100644 --- a/apps/api/internal/httpapi/core_flow_integration_test.go +++ b/apps/api/internal/httpapi/core_flow_integration_test.go @@ -773,10 +773,20 @@ WHERE reference_type = 'gateway_task' Balance float64 `json:"balance"` } `json:"primaryAccount"` } - doJSON(t, server.URL, http.MethodGet, "/api/workspace/wallet", loginResponse.AccessToken, nil, http.StatusOK, &walletSummary) + doJSON(t, server.URL, http.MethodGet, "/api/workspace/wallet?currency=resource", loginResponse.AccessToken, nil, http.StatusOK, &walletSummary) if walletSummary.PrimaryAccount.Currency != "resource" || !floatNear(walletSummary.PrimaryAccount.Balance, walletBalanceAfter) || len(walletSummary.Accounts) == 0 { t.Fatalf("workspace wallet should expose current resource balance, got %+v want balance=%f", walletSummary, walletBalanceAfter) } + var desktopConfig struct { + Billing struct { + GatewayBaseURL string `json:"gatewayBaseUrl"` + GatewayBillingPath string `json:"gatewayBillingPath"` + } `json:"billing"` + } + doJSON(t, server.URL, http.MethodGet, "/api/workspace/desktop-config", loginResponse.AccessToken, nil, http.StatusOK, &desktopConfig) + if desktopConfig.Billing.GatewayBillingPath != "/workspace/billing" || desktopConfig.Billing.GatewayBaseURL == "" { + t.Fatalf("desktop config should expose gateway billing route, got %+v", desktopConfig) + } var walletTransactions struct { Items []struct { TransactionType string `json:"transactionType"` diff --git a/apps/api/internal/httpapi/desktop_config_handlers.go b/apps/api/internal/httpapi/desktop_config_handlers.go new file mode 100644 index 0000000..f806995 --- /dev/null +++ b/apps/api/internal/httpapi/desktop_config_handlers.go @@ -0,0 +1,55 @@ +package httpapi + +import ( + "net/http" + "strings" +) + +type desktopBillingConfigResponse struct { + GatewayBaseURL string `json:"gatewayBaseUrl,omitempty"` + GatewayBillingPath string `json:"gatewayBillingPath"` +} + +type desktopConfigResponse struct { + Billing desktopBillingConfigResponse `json:"billing"` +} + +// getDesktopConfig godoc +// @Summary 获取桌面端配置 +// @Description 返回桌面端需要的 Gateway Web 账单入口配置。 +// @Tags workspace +// @Produce json +// @Security BearerAuth +// @Success 200 {object} desktopConfigResponse +// @Failure 401 {object} ErrorEnvelope +// @Router /api/workspace/desktop-config [get] +func (s *Server) getDesktopConfig(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, desktopConfigResponse{ + Billing: desktopBillingConfigResponse{ + GatewayBaseURL: firstNonEmpty( + strings.TrimRight(strings.TrimSpace(s.cfg.WebBaseURL), "/"), + strings.TrimRight(strings.TrimSpace(s.cfg.PublicBaseURL), "/"), + requestOrigin(r), + ), + GatewayBillingPath: "/workspace/billing", + }, + }) +} + +func requestOrigin(r *http.Request) string { + host := strings.TrimSpace(r.Host) + if host == "" { + return "" + } + proto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")) + if proto == "" { + proto = "http" + } + if comma := strings.Index(proto, ","); comma >= 0 { + proto = strings.TrimSpace(proto[:comma]) + } + if proto == "" { + proto = "http" + } + return proto + "://" + host +} diff --git a/apps/api/internal/httpapi/server.go b/apps/api/internal/httpapi/server.go index 7cb11fe..335b6f8 100644 --- a/apps/api/internal/httpapi/server.go +++ b/apps/api/internal/httpapi/server.go @@ -88,6 +88,7 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor mux.Handle("PATCH /api/v1/api-keys/{apiKeyID}/disable", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.disableAPIKey))) mux.Handle("DELETE /api/v1/api-keys/{apiKeyID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.deleteAPIKey))) mux.Handle("GET /api/playground/api-keys", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayableAPIKeys))) + mux.Handle("GET /api/workspace/desktop-config", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.getDesktopConfig))) mux.Handle("GET /api/workspace/user-groups", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listCurrentUserGroups))) mux.Handle("GET /api/workspace/wallet", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.getWallet))) mux.Handle("GET /api/workspace/wallet/transactions", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listWalletTransactions))) diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index a367c63..d85714b 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -592,6 +592,13 @@ export interface WalletSummaryResponse { primaryAccount: GatewayWalletAccount; } +export interface GatewayDesktopConfigResponse { + billing: { + gatewayBaseUrl?: string; + gatewayBillingPath: string; + }; +} + export interface WalletAdjustmentResponse { account: GatewayWalletAccount; before: GatewayWalletAccount;