Files
NianAIGC/backend/internal/application/runtime.go
2026-08-21 13:44:56 +08:00

575 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package application
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/billing"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/httpapi"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/jobs"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/logging"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/postgres"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/providers"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/settings"
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/webhook"
)
func unavailableHandler(status int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(status) })
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
return ""
}
func positiveInt64Env(getenv postgres.Getenv, name string, fallback int64) int64 {
return positiveInt64Value(getenv(name), fallback)
}
func positiveInt64Value(raw string, fallback int64) int64 {
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
if err != nil || value <= 0 {
return fallback
}
return value
}
func durationEnv(getenv postgres.Getenv, name string, fallback time.Duration) time.Duration {
milliseconds := positiveInt64Env(getenv, name, fallback.Milliseconds())
return time.Duration(milliseconds) * time.Millisecond
}
func parseBool(value string) bool {
switch strings.ToLower(strings.TrimSpace(value)) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
func applicationJobID() string {
return applicationID("job")
}
func applicationID(prefix string) string {
raw := make([]byte, 12)
if _, err := rand.Read(raw); err == nil {
return prefix + "-" + hex.EncodeToString(raw)
}
return prefix + "-" + strconv.FormatInt(time.Now().UnixNano(), 36)
}
func imageEngine(getenv postgres.Getenv) string {
engine := strings.ToLower(firstNonEmpty(getenv("IMAGE_GENERATE_ENGINE"), getenv("IMAGE_CREATION_ENGINE"), getenv("IMAGE_PROVIDER"), "jimeng"))
if engine != "evolink" && engine != "bailian" && engine != "seedream" {
return "jimeng"
}
return engine
}
func videoEngine(getenv postgres.Getenv) string {
if strings.EqualFold(strings.TrimSpace(getenv("VIDEO_GENERATE_ENGINE")), "seedance") {
return "seedance"
}
return "bailian"
}
func imageProvider(getenv postgres.Getenv) string {
switch imageEngine(getenv) {
case "evolink":
return "evolink"
case "bailian":
return "bailian"
case "seedream":
return "seedream"
default:
return "volcengine-visual"
}
}
func videoProvider(getenv postgres.Getenv) string {
if videoEngine(getenv) == "seedance" {
return "seedance"
}
return "bailian"
}
func imageModel(getenv postgres.Getenv) string {
switch imageEngine(getenv) {
case "evolink":
return firstNonEmpty(getenv("EVOLINK_IMAGE_MODEL"), "gpt-image-2")
case "bailian":
return firstNonEmpty(getenv("BAILIAN_IMAGE_MODEL"), "wan2.7-image-pro")
case "seedream":
return providers.Seedream50ProModel
default:
return firstNonEmpty(getenv("JIMENG_IMAGE_GENERATE_46_REQ_KEY"), "jimeng_seedream46_cvtob")
}
}
func videoModel(getenv postgres.Getenv) string {
if videoEngine(getenv) == "seedance" {
return firstNonEmpty(getenv("SEEDANCE_MODEL"), "doubao-seedance-2-0-260128")
}
return firstNonEmpty(getenv("BAILIAN_VIDEO_MODEL"), "wan2.7-i2v-2026-04-25")
}
func bailianAPIKey(getenv postgres.Getenv) string {
return firstNonEmpty(getenv("BAILIAN_API_KEY"), getenv("DASHSCOPE_API_KEY"))
}
func bailianNativeBaseURL(getenv postgres.Getenv) string {
base := strings.TrimRight(firstNonEmpty(getenv("BAILIAN_BASE_URL"), "https://llm-126wneubbdo6dbr5.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"), "/")
lower := strings.ToLower(base)
const suffix = "/compatible-mode/v1"
if strings.HasSuffix(lower, suffix) {
return base[:len(base)-len(suffix)]
}
return base
}
func buildProviderRegistry(getenv postgres.Getenv) jobs.ProviderRegistry {
client := &http.Client{Timeout: durationEnv(getenv, "ZHINIAN_PROVIDER_TIMEOUT_MS", 30*time.Second)}
maximum := positiveInt64Env(getenv, "ZHINIAN_PROVIDER_MAX_RESPONSE_BYTES", 2<<20)
return buildProviderRegistryWithClient(getenv, client, maximum)
}
func buildProviderRegistryWithClient(getenv postgres.Getenv, client *http.Client, maximum int64) jobs.ProviderRegistry {
return jobs.ProviderRegistry{
"volcengine-visual": providers.NewVolcengine(providers.Config{
BaseURL: firstNonEmpty(getenv("VOLCENGINE_VISUAL_ENDPOINT"), "https://visual.volcengineapi.com"),
Model: imageModel(getenv), AccessKeyID: getenv("VOLCENGINE_ACCESS_KEY_ID"), SecretAccessKey: getenv("VOLCENGINE_SECRET_ACCESS_KEY"),
Region: firstNonEmpty(getenv("VOLCENGINE_REGION"), "cn-north-1"), Service: firstNonEmpty(getenv("VOLCENGINE_SERVICE"), "cv"), MaxResponseBytes: maximum,
}, client, nil),
"evolink": providers.NewEvoLink(providers.Config{BaseURL: firstNonEmpty(getenv("EVOLINK_BASE_URL"), "https://api.evolink.ai"), APIKey: getenv("EVOLINK_API_KEY"), Model: firstNonEmpty(getenv("EVOLINK_IMAGE_MODEL"), "gpt-image-2"), MaxResponseBytes: maximum}, client),
"bailian": providers.NewBailian(providers.Config{BaseURL: bailianNativeBaseURL(getenv), APIKey: bailianAPIKey(getenv), Model: firstNonEmpty(getenv("BAILIAN_IMAGE_MODEL"), "wan2.7-image-pro"), MaxResponseBytes: maximum}, client),
"seedance": providers.NewSeedance(providers.Config{BaseURL: firstNonEmpty(getenv("SEEDANCE_BASE_URL"), "https://ark.cn-beijing.volces.com/api/v3"), APIKey: getenv("SEEDANCE_API_KEY"), Model: firstNonEmpty(getenv("SEEDANCE_MODEL"), "doubao-seedance-2-0-260128"), MaxResponseBytes: maximum}, client),
"seedream": providers.NewSeedream(providers.Config{BaseURL: firstNonEmpty(getenv("SEEDANCE_BASE_URL"), "https://ark.cn-beijing.volces.com/api/v3"), APIKey: getenv("SEEDANCE_API_KEY"), Model: providers.Seedream50ProModel, MaxResponseBytes: maximum}, client),
}
}
func providerImageTargets(getenv postgres.Getenv) map[string]jobs.ProviderTarget {
return map[string]jobs.ProviderTarget{
"jimeng": {
Provider: "volcengine-visual",
Model: firstNonEmpty(getenv("JIMENG_IMAGE_GENERATE_46_REQ_KEY"), "jimeng_seedream46_cvtob"),
},
"evolink": {
Provider: "evolink",
Model: firstNonEmpty(getenv("EVOLINK_IMAGE_MODEL"), "gpt-image-2"),
Settings: map[string]any{"quality": firstNonEmpty(getenv("EVOLINK_IMAGE_QUALITY"), "medium")},
},
"bailian": {
Provider: "bailian",
Model: firstNonEmpty(getenv("BAILIAN_IMAGE_MODEL"), "wan2.7-image-pro"),
},
"seedream": {
Provider: "seedream",
Model: providers.Seedream50ProModel,
Settings: map[string]any{"size": "1.5K", "outputFormat": "png", "optimizeMode": "standard", "imageCount": float64(1)},
},
}
}
func providerVideoTargets(getenv postgres.Getenv) map[string]jobs.ProviderTarget {
seedanceModel := firstNonEmpty(getenv("SEEDANCE_MODEL"), "doubao-seedance-2-0-260128")
return map[string]jobs.ProviderTarget{
"seedance": {
Provider: "seedance",
Model: seedanceModel,
Settings: seedanceDefaultSettings(getenv, seedanceModel),
},
"bailian": {
Provider: "bailian",
Model: firstNonEmpty(getenv("BAILIAN_VIDEO_MODEL"), "wan2.7-i2v-2026-04-25"),
},
}
}
func providerVideoModels(getenv postgres.Getenv) map[string]jobs.ProviderTarget {
models := []string{
"doubao-seedance-2-0-260128",
"doubao-seedance-2-5-260628",
firstNonEmpty(getenv("SEEDANCE_MODEL"), "doubao-seedance-2-0-260128"),
}
targets := make(map[string]jobs.ProviderTarget, len(models))
for _, model := range models {
targets[model] = jobs.ProviderTarget{Provider: "seedance", Model: model, Settings: seedanceDefaultSettings(getenv, model)}
}
return targets
}
func seedanceDefaultSettings(getenv postgres.Getenv, model string) map[string]any {
duration := positiveInt64Value(firstNonEmpty(getenv("SEEDANCE_DURATION"), getenv("SEEDANCE_DEFAULT_DURATION")), 5)
maximum := int64(15)
if model == "doubao-seedance-2-5-260628" {
maximum = 30
}
if duration < 4 {
duration = 4
} else if duration > maximum {
duration = maximum
}
return map[string]any{
"ratio": firstNonEmpty(getenv("SEEDANCE_RATIO"), getenv("SEEDANCE_DEFAULT_RATIO"), "9:16"),
"duration": float64(duration),
"resolution": firstNonEmpty(getenv("SEEDANCE_RESOLUTION"), getenv("SEEDANCE_DEFAULT_RESOLUTION"), "720p"),
}
}
func providerUnavailableMessages(getenv postgres.Getenv) map[string]string {
missing := map[string]string{}
if strings.TrimSpace(getenv("VOLCENGINE_ACCESS_KEY_ID")) == "" || strings.TrimSpace(getenv("VOLCENGINE_SECRET_ACCESS_KEY")) == "" {
missing["volcengine-visual"] = "即梦服务商未配置,请先配置 VOLCENGINE_ACCESS_KEY_ID 和 VOLCENGINE_SECRET_ACCESS_KEY。"
}
if strings.TrimSpace(getenv("EVOLINK_API_KEY")) == "" {
missing["evolink"] = "EvoLink 服务商未配置,请先配置 EVOLINK_API_KEY。"
}
if bailianAPIKey(getenv) == "" {
missing["bailian"] = "百炼服务商未配置,请先配置 BAILIAN_API_KEY 或 DASHSCOPE_API_KEY。"
}
if strings.TrimSpace(getenv("SEEDANCE_API_KEY")) == "" {
missing["seedance"] = "Seedance 服务商未配置,请先配置 SEEDANCE_API_KEY。"
missing["seedream"] = "Seedream 服务商未配置,请先配置火山方舟 API KeySEEDANCE_API_KEY。"
}
return missing
}
func defaultWebhookSender(getenv postgres.Getenv) (*webhook.HTTPSender, error) {
return webhook.NewPublicHTTPSender(
durationEnv(getenv, "ZHINIAN_WEBHOOK_TIMEOUT_MS", 10*time.Second),
webhook.NewPublicDestinationPolicy(nil, nil),
)
}
func capabilitySummary(getenv postgres.Getenv) func(context.Context) (any, error) {
return func(context.Context) (any, error) {
engine := imageEngine(getenv)
provider := map[string]string{"jimeng": "volcengine-visual", "evolink": "evolink", "bailian": "bailian", "seedream": "seedream"}[engine]
primaryVideoEngine := videoEngine(getenv)
primaryVideo := map[string]any{
"id": "video.generate", "kind": "video", "engine": primaryVideoEngine,
"provider": primaryVideoEngine, "reqKey": videoModel(getenv),
}
if primaryVideoEngine == "seedance" {
primaryVideo["label"] = "Seedance 视频生成"
primaryVideo["limits"] = seedanceCapabilityLimits(videoModel(getenv))
primaryVideo["models"] = seedanceModelOptions()
} else {
primaryVideo["label"] = "百炼图生视频"
primaryVideo["limits"] = bailianVideoCapabilityLimits()
}
return []any{
map[string]any{"id": "image.generate", "label": imageCapabilityLabel(engine), "kind": "image", "engine": engine, "provider": provider, "reqKey": imageModel(getenv)},
primaryVideo,
map[string]any{"id": "video.generate.bailian", "label": "百炼图生视频", "kind": "video", "engine": "bailian", "provider": "bailian", "reqKey": firstNonEmpty(getenv("BAILIAN_VIDEO_MODEL"), "wan2.7-i2v-2026-04-25"), "limits": bailianVideoCapabilityLimits()},
}, nil
}
}
func seedanceCapabilityLimits(model string) map[string]any {
maximum := 15
if model == "doubao-seedance-2-5-260628" {
maximum = 30
}
return map[string]any{"durationSeconds": map[string]int{"min": 4, "max": maximum}, "ratios": []string{"16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"}, "resolutions": []string{"480p", "720p", "1080p"}}
}
func seedanceModelOptions() []any {
return []any{
map[string]any{"id": "doubao-seedance-2-0-260128", "label": "Seedance 2.0", "limits": seedanceCapabilityLimits("doubao-seedance-2-0-260128")},
map[string]any{"id": "doubao-seedance-2-5-260628", "label": "Seedance 2.5", "limits": seedanceCapabilityLimits("doubao-seedance-2-5-260628")},
}
}
func bailianVideoCapabilityLimits() map[string]any {
return map[string]any{"inputImages": map[string]int{"min": 1, "max": 2}, "durationSeconds": map[string]int{"min": 2, "max": 15}, "resolutions": []string{"720P", "1080P"}}
}
func runtimeHealthDetails(getenv postgres.Getenv) httpapi.HealthDetails {
image := imageEngine(getenv)
imageLabel := map[string]string{"jimeng": "即梦", "evolink": "EvoLink", "bailian": "阿里云百炼", "seedream": "Seedream 5.0 Pro"}[image]
video := videoEngine(getenv)
videoLabel := map[string]string{"seedance": "Seedance", "bailian": "阿里云百炼"}[video]
visualConfigured := strings.TrimSpace(getenv("VOLCENGINE_ACCESS_KEY_ID")) != "" && strings.TrimSpace(getenv("VOLCENGINE_SECRET_ACCESS_KEY")) != ""
auth, _ := ParseAuthConfig(getenv)
bailianKey := bailianAPIKey(getenv)
authMode := "disabled"
if auth.Required {
authMode = "missing"
if auth.Configured {
authMode = "configured"
}
}
return httpapi.HealthDetails{
VisualAPIMode: providerMode(visualConfigured, "volcengine"),
EvolinkMode: providerMode(strings.TrimSpace(getenv("EVOLINK_API_KEY")) != "", "evolink"),
SeedanceMode: providerMode(strings.TrimSpace(getenv("SEEDANCE_API_KEY")) != "", "seedance"),
BailianMode: providerMode(bailianKey != "", "bailian"),
AuthMode: authMode,
Capabilities: []any{
map[string]any{"id": "image.generate", "label": imageCapabilityLabel(image), "engine": image, "engineLabel": imageLabel, "reqKey": imageModel(getenv)},
map[string]any{"id": "video.generate", "label": "视频生成", "engine": video, "engineLabel": videoLabel, "reqKey": videoModel(getenv), "models": seedanceModelOptions()},
},
}
}
func imageCapabilityLabel(engine string) string {
if engine == "seedream" {
return "Seedream 5.0 Pro 图片生成"
}
return "图片生成 4.6"
}
func providerMode(configured bool, live string) string {
if !configured {
return "missing"
}
return live
}
func remoteAssetMaxBytes(getenv postgres.Getenv) int64 {
return positiveInt64Env(getenv, "ZHINIAN_REMOTE_ASSET_MAX_BYTES", 20<<20)
}
var runtimeSettingKeys = []string{
"ALI_OSS_ACCESS_KEY_ID", "ALI_OSS_ACCESS_KEY_SECRET", "ALI_OSS_BUCKET", "ALI_OSS_ENDPOINT", "ALI_OSS_PUBLIC_BASE_URL",
"BAILIAN_API_KEY", "BAILIAN_BASE_URL", "BAILIAN_IMAGE_MODEL", "BAILIAN_VIDEO_MODEL", "DATABASE_URL", "DASHSCOPE_API_KEY",
"EVOLINK_API_KEY", "EVOLINK_BASE_URL", "EVOLINK_IMAGE_MODEL", "EVOLINK_IMAGE_QUALITY", "IMAGE_GENERATE_ENGINE",
"SEEDANCE_API_KEY", "SEEDANCE_MODEL", "VIDEO_GENERATE_ENGINE", "VOLCENGINE_ACCESS_KEY_ID", "VOLCENGINE_SECRET_ACCESS_KEY",
"ZHINIAN_AUTH_REQUIRED", "ZHINIAN_AUTH_SESSION_SECRET", "ZHINIAN_BILLING_ACCOUNT_BANK", "ZHINIAN_BILLING_ACCOUNT_NAME",
"ZHINIAN_BILLING_ACCOUNT_NUMBER", "ZHINIAN_BILLING_CONTACT", "ZHINIAN_BILLING_REQUIRED",
}
func runtimeEnvironment(getenv postgres.Getenv) map[string]string {
values := make(map[string]string, len(runtimeSettingKeys))
for _, key := range runtimeSettingKeys {
if value := getenv(key); value != "" {
values[key] = value
}
}
return values
}
func defaultSettingsService(getenv postgres.Getenv) *settings.Service {
path := firstNonEmpty(getenv("ZHINIAN_SETTINGS_FILE"), ".env.local")
return settings.New(path, runtimeEnvironment(getenv), nil)
}
func databaseSettingsService(getenv postgres.Getenv, repository settings.RuntimeSettingsRepository) *settings.Service {
path := firstNonEmpty(getenv("ZHINIAN_SETTINGS_FILE"), ".env.local")
return settings.NewWithRuntimeRepository(path, runtimeEnvironment(getenv), repository, nil)
}
func runtimeSettingsGetenv(ctx context.Context, fallback postgres.Getenv, service *settings.Service) (postgres.Getenv, error) {
values, err := service.RuntimeValues(ctx)
if err != nil {
return nil, err
}
return mergedSettingsGetenv(values, fallback), nil
}
func cachedRuntimeSettingsGetenv(fallback postgres.Getenv, service *settings.Service) postgres.Getenv {
return mergedSettingsGetenv(service.CachedRuntimeValues(), fallback)
}
func mergedSettingsGetenv(values map[string]string, fallback postgres.Getenv) postgres.Getenv {
return func(name string) string {
if value, ok := values[name]; ok {
return value
}
return fallback(name)
}
}
type runtimeProviderResolver struct {
fallback postgres.Getenv
settings *settings.Service
client *http.Client
maximum int64
}
func newRuntimeProviderResolver(fallback postgres.Getenv, service *settings.Service) *runtimeProviderResolver {
return &runtimeProviderResolver{
fallback: fallback, settings: service,
client: &http.Client{Timeout: durationEnv(fallback, "ZHINIAN_PROVIDER_TIMEOUT_MS", 30*time.Second)},
maximum: positiveInt64Env(fallback, "ZHINIAN_PROVIDER_MAX_RESPONSE_BYTES", 2<<20),
}
}
func (resolver *runtimeProviderResolver) Resolve(ctx context.Context, name string) (providers.Adapter, error) {
getenv, err := runtimeSettingsGetenv(ctx, resolver.fallback, resolver.settings)
if err != nil {
return nil, err
}
return buildProviderRegistryWithClient(getenv, resolver.client, resolver.maximum)[name], nil
}
type runtimeProviderJobBuilder struct {
fallback postgres.Getenv
settings *settings.Service
enforceAvailability bool
}
func (builder runtimeProviderJobBuilder) Build(ctx context.Context, owner, client, capability, idempotency string, body map[string]any) (jobs.CreateCommand, error) {
getenv, err := runtimeSettingsGetenv(ctx, builder.fallback, builder.settings)
if err != nil {
return jobs.CreateCommand{}, fmt.Errorf("load runtime provider settings: %w", err)
}
return configuredProviderJobBuilder(getenv, builder.enforceAvailability).Build(ctx, owner, client, capability, idempotency, body)
}
func configuredProviderJobBuilder(getenv postgres.Getenv, enforceAvailability bool) jobs.ProviderJobBuilder {
var unavailable map[string]string
if enforceAvailability {
unavailable = providerUnavailableMessages(getenv)
}
return jobs.ProviderJobBuilder{
ImageProvider: imageProvider(getenv), VideoProvider: videoProvider(getenv),
ImageModel: imageModel(getenv), VideoModel: videoModel(getenv), ImageEngine: imageEngine(getenv), VideoEngine: videoEngine(getenv),
ImageEngines: providerImageTargets(getenv), VideoEngines: providerVideoTargets(getenv), VideoModels: providerVideoModels(getenv), UnavailableProviders: unavailable, NewID: applicationJobID,
}
}
func runtimeCapabilitySummary(fallback postgres.Getenv, service *settings.Service) func(context.Context) (any, error) {
return func(ctx context.Context) (any, error) {
getenv, err := runtimeSettingsGetenv(ctx, fallback, service)
if err != nil {
return nil, fmt.Errorf("load runtime provider settings: %w", err)
}
return capabilitySummary(getenv)(ctx)
}
}
func runtimeHealthDetailsProvider(fallback postgres.Getenv, service *settings.Service) func(context.Context) httpapi.HealthDetails {
return func(ctx context.Context) httpapi.HealthDetails {
refreshContext, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
defer cancel()
getenv, err := runtimeSettingsGetenv(refreshContext, fallback, service)
if err != nil {
getenv = cachedRuntimeSettingsGetenv(fallback, service)
}
return runtimeHealthDetails(getenv)
}
}
type settingsBillingAccountStore struct{ service *settings.Service }
func (store settingsBillingAccountStore) Load(ctx context.Context) (billing.AccountConfig, error) {
value, err := store.service.Get(ctx)
if err != nil {
return billing.AccountConfig{}, err
}
payload, ok := value.(settings.Payload)
if !ok {
return billing.AccountConfig{}, fmt.Errorf("load billing account settings: unexpected settings payload")
}
fields := map[string]string{}
for _, group := range payload.Groups {
for _, field := range group.Fields {
fields[field.Key] = field.Value
}
}
return billing.AccountConfig{
AccountName: fields["ZHINIAN_BILLING_ACCOUNT_NAME"], BankName: fields["ZHINIAN_BILLING_ACCOUNT_BANK"],
AccountNumber: fields["ZHINIAN_BILLING_ACCOUNT_NUMBER"], Contact: fields["ZHINIAN_BILLING_CONTACT"],
}, nil
}
func (store settingsBillingAccountStore) Save(ctx context.Context, value billing.AccountConfig) error {
_, err := store.service.Save(ctx, map[string]any{
"ZHINIAN_BILLING_ACCOUNT_NAME": value.AccountName, "ZHINIAN_BILLING_ACCOUNT_BANK": value.BankName,
"ZHINIAN_BILLING_ACCOUNT_NUMBER": value.AccountNumber, "ZHINIAN_BILLING_CONTACT": value.Contact,
})
return err
}
type logServiceAdapter struct{ service *logging.Service }
func (adapter logServiceAdapter) List(ctx context.Context, filters httpapi.LogFilters) (any, error) {
return adapter.service.List(ctx, logging.Filters{Level: filters.Level, Q: filters.Q, Source: filters.Source, Limit: filters.Limit})
}
func (adapter logServiceAdapter) Clear(ctx context.Context) error { return adapter.service.Clear(ctx) }
func (adapter logServiceAdapter) Append(ctx context.Context, input logging.Input) (logging.Entry, error) {
return adapter.service.Append(ctx, input)
}
func defaultLogService(getenv postgres.Getenv) httpapi.LogService {
runtimeDirectory := firstNonEmpty(getenv("ZHINIAN_RUNTIME_DIR"), ".runtime")
logDirectory := firstNonEmpty(getenv("ZHINIAN_LOG_DIR"), filepath.Join(runtimeDirectory, "logs"))
return logServiceAdapter{service: logging.New(filepath.Join(logDirectory, "server-events.jsonl"), positiveInt64Env(getenv, "ZHINIAN_LOG_MAX_BYTES", 5<<20), nil, nil)}
}
type prefixedBlobStore struct {
prefix string
store assets.BlobStore
}
func (store prefixedBlobStore) Put(ctx context.Context, key string, body io.Reader, size int64, contentType string) (assets.StoredObject, error) {
stored, err := store.store.Put(ctx, path.Join(store.prefix, key), body, size, contentType)
if err != nil {
return assets.StoredObject{}, err
}
// StoragePath is an application key used by /uploads and
// /generated-results. The OSS namespace prefix is private to this adapter.
stored.Key = key
return stored, nil
}
func (store prefixedBlobStore) Read(ctx context.Context, key string) (assets.Blob, error) {
return store.store.Read(ctx, path.Join(store.prefix, key))
}
func (store prefixedBlobStore) Delete(ctx context.Context, key string) error {
return store.store.Delete(ctx, path.Join(store.prefix, key))
}
func (store prefixedBlobStore) SignReadURL(key string, ttl time.Duration) (string, error) {
signer, ok := store.store.(assets.BlobURLSigner)
if !ok {
return "", errors.New("blob store does not support signed URLs")
}
return signer.SignReadURL(path.Join(store.prefix, key), ttl)
}
func configuredOSSBlobStore(getenv postgres.Getenv) (assets.BlobStore, bool, error) {
endpoint, bucket := strings.TrimSpace(getenv("ALI_OSS_ENDPOINT")), strings.TrimSpace(getenv("ALI_OSS_BUCKET"))
accessKeyID, secret := strings.TrimSpace(getenv("ALI_OSS_ACCESS_KEY_ID")), strings.TrimSpace(getenv("ALI_OSS_ACCESS_KEY_SECRET"))
publicURL := strings.TrimSpace(getenv("ALI_OSS_PUBLIC_BASE_URL"))
if endpoint == "" || bucket == "" || accessKeyID == "" || secret == "" || publicURL == "" {
return nil, false, nil
}
client, err := assets.NewOSSHTTPClient(accessKeyID, secret, &http.Client{Timeout: durationEnv(getenv, "ZHINIAN_OSS_TIMEOUT_MS", 30*time.Second)}, nil)
if err != nil {
return nil, false, err
}
store, err := assets.NewOSS(assets.OSSConfig{Endpoint: endpoint, Bucket: bucket, PublicBaseURL: publicURL, PublicRead: false}, client)
if err != nil {
return nil, false, err
}
prefix := strings.Trim(strings.TrimSpace(getenv("ALI_OSS_PREFIX")), "/")
if prefix == "" {
prefix = "zhinian"
}
return prefixedBlobStore{prefix: prefix, store: store}, true, nil
}