refactor(*): 平台架构 - 拆分后端领域模块并同步插件协议与模型能力
This commit is contained in:
@@ -42,14 +42,15 @@
|
||||
### 后端
|
||||
|
||||
- `backend/internal/handler/`:HTTP 入参、鉴权上下文、调用 service、返回统一响应;不放业务判断和数据库查询。
|
||||
- `backend/internal/service/`:校验、权限、默认值、ID、时间、配额、幂等、任务编排和外部调用。画布生成调度在 `provider.go`;文本 / 图片 / 视频遗留 / 音频 / HTTP / 声明式协议分别在 `provider_text.go`、`provider_image.go`、`provider_video.go`、`provider_audio.go`、`provider_http_client.go`、`provider_protocol.go`。
|
||||
- `backend/internal/service/`:稳定导入面。只再导出 `internal/app` 的类型、常量和包级函数(`aliases_*.go`)。handler/cmd 继续 import 本包。
|
||||
- `backend/internal/app/`:HTTP 组合根和尚未拆出的业务实现。校验、权限、跨域编排从这里进入。画布生成调度仍主要在 `provider.go`;文本 / 图片 / 视频遗留 / 音频 / HTTP / 声明式协议分别在 `provider_text.go`、`provider_image.go`、`provider_video.go`、`provider_audio.go`、`provider_http_client.go`、`provider_protocol.go`。技能库在 `internal/skills`,提示词与风格在 `internal/prompts`,登录注册在 `internal/auth`,画布分享在 `internal/canvas`,资源引用解析在 `internal/assets`,运行时策略/限流/worker 在 `internal/platform`,错误码与通用工具在 `internal/kernel`,出站 SSRF 在 `internal/outbound`。域包不得 import `internal/service` 或 `internal/app`。
|
||||
- `backend/internal/repository/`:GORM 查询和持久化;不承载业务策略。
|
||||
- `backend/internal/model/`:结构、枚举和简单模型方法;不调用外部服务。
|
||||
- `backend/internal/provider/`:模型供应商能力和协议实现。
|
||||
- `backend/internal/database/`:数据库连接、迁移和连接池。
|
||||
- `backend/cmd/`:可执行入口、迁移和启动配置;启动参数不得绕过数据目录约束。
|
||||
|
||||
调用链应保持为:`HTTP -> handler -> service -> repository/model -> database/resource`;需要模型上游时由 service 进入 `provider/outbound`。跨层调用必须有明确理由并补测试。
|
||||
调用链应保持为:`HTTP -> handler -> service(别名)-> app(组合根)-> 业务域包 -> repository/model -> database/resource`;需要模型上游时由域包或 app 进入 `protocol` / `generation` / `outbound`。跨层调用必须有明确理由并补测试。
|
||||
|
||||
### Agent、插件和文档
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
|
||||
| 头像 | 昵称 | 邮箱 | 个性签名 |
|
||||
| --- | --- | --- | --- |
|
||||
| <img src="assets/user-ddcat.jpg" alt="ddCat" width="80"> | ddCat<br><sub>项目发起者 · 微信:ddcat0829</sub> | [ddcat666@126.com](mailto:ddcat666@126.com) | 在计算机里头没有任何黑魔法,所有的东西只不过是我现在不知道而已,总有一天我会把所有的细节、所有的内部的东西全搞明白的。 |
|
||||
| <img src="assets/user-sikongyue.png" alt="爱笑的毛毛虫" width="80"> | 爱笑的毛毛虫<br><sub>用户名:sikongyue</sub> | [315515767@qq.com](mailto:315515767@qq.com) | 正在啃 main 分支,争取下次 merge 的时候变成蝴蝶 |
|
||||
| <img src="assets/user-delve.jpg" alt="delve-s" width="80"> | delve-s | [3013141136@qq.com](mailto:3013141136@qq.com) | 我亦无他,惟手熟尔 |
|
||||
| <img src="assets/user-CyrusAuyeung.jpg" alt="CyrusAuyeung" width="80"> | CyrusAuyeung | [cyrusauyeungho@gmail.com](mailto:cyrusauyeungho@gmail.com) | HKUST(GZ) UG |
|
||||
|
||||
BIN
assets/user-ddcat.jpg
Normal file
BIN
assets/user-ddcat.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
@@ -1,9 +1,10 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"infinite-canvas/backend/internal/kernel"
|
||||
stdlog "log"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -918,15 +919,5 @@ func publicChannel(channel model.ModelChannel, admin bool, channelModels []model
|
||||
}
|
||||
|
||||
func uniqueNonEmpty(values []string) []string {
|
||||
seen := map[string]bool{}
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || seen[value] {
|
||||
continue
|
||||
}
|
||||
seen[value] = true
|
||||
result = append(result, value)
|
||||
}
|
||||
return result
|
||||
return kernel.UniqueNonEmpty(values)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"io"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -186,26 +186,24 @@ func (s *Service) collectRedisPerformance(ctx context.Context) SystemPerformance
|
||||
result.CacheGroups = append(result.CacheGroups, SystemPerformanceCacheGroup{ID: "rateLimits", Label: "本地请求频控", Clearable: true})
|
||||
return result
|
||||
}
|
||||
if s.coordinator.redis == nil {
|
||||
s.coordinator.localMu.Lock()
|
||||
localRateKeys := int64(len(s.coordinator.localRate))
|
||||
s.coordinator.localMu.Unlock()
|
||||
if !s.coordinator.HasRedis() {
|
||||
localRateKeys := int64(s.coordinator.LocalRateCount())
|
||||
result.CacheGroups = append(result.CacheGroups, SystemPerformanceCacheGroup{ID: "rateLimits", Label: "本地请求频控", Keys: localRateKeys, Clearable: true})
|
||||
return result
|
||||
}
|
||||
result.Configured, result.Mode = true, "redis"
|
||||
started := time.Now()
|
||||
if err := s.coordinator.redis.Ping(ctx).Err(); err != nil {
|
||||
if err := s.coordinator.Redis().Ping(ctx).Err(); err != nil {
|
||||
result.StatusMessage = "Redis 暂时不可用"
|
||||
return result
|
||||
}
|
||||
result.Connected = true
|
||||
result.LatencyMs = max(1, time.Since(started).Milliseconds())
|
||||
result.StatusMessage = "Redis 连接正常"
|
||||
if size, err := s.coordinator.redis.DBSize(ctx).Result(); err == nil {
|
||||
if size, err := s.coordinator.Redis().DBSize(ctx).Result(); err == nil {
|
||||
result.Keys = size
|
||||
}
|
||||
if info, err := s.coordinator.redis.Info(ctx, "server", "clients", "memory", "stats").Result(); err == nil {
|
||||
if info, err := s.coordinator.Redis().Info(ctx, "server", "clients", "memory", "stats").Result(); err == nil {
|
||||
values := parseRedisInfo(info)
|
||||
result.Version = values["redis_version"]
|
||||
result.UptimeSeconds = redisInfoInt(values, "uptime_in_seconds")
|
||||
@@ -223,10 +221,10 @@ func (s *Service) collectRedisPerformance(ctx context.Context) SystemPerformance
|
||||
result.HitRate = float64(hits) / float64(hits+misses) * 100
|
||||
}
|
||||
}
|
||||
pool := s.coordinator.redis.PoolStats()
|
||||
pool := s.coordinator.Redis().PoolStats()
|
||||
result.Pool = SystemPerformanceRedisPool{Hits: pool.Hits, Misses: pool.Misses, Timeouts: pool.Timeouts, TotalConnections: pool.TotalConns, IdleConnections: pool.IdleConns, StaleConnections: pool.StaleConns, PendingRequests: pool.PendingRequests}
|
||||
for _, definition := range runtimeCacheDefinitions {
|
||||
count, _ := scanRedisKeyCount(ctx, s.coordinator.redis, definition.pattern)
|
||||
count, _ := scanRedisKeyCount(ctx, s.coordinator.Redis(), definition.pattern)
|
||||
result.CacheGroups = append(result.CacheGroups, SystemPerformanceCacheGroup{ID: definition.id, Label: definition.label, Keys: count, Clearable: true})
|
||||
}
|
||||
return result
|
||||
@@ -243,9 +241,9 @@ func (s *Service) ClearAdminRuntimeCache(ctx context.Context, actor *model.User,
|
||||
defer cancel()
|
||||
result := &AdminCacheClearResult{Scope: "runtime", ClearedAt: time.Now(), Groups: make([]AdminCacheClearGroupResult, 0, len(runtimeCacheDefinitions))}
|
||||
if s.coordinator != nil {
|
||||
if s.coordinator.redis != nil {
|
||||
if s.coordinator.HasRedis() {
|
||||
for _, definition := range runtimeCacheDefinitions {
|
||||
deleted, err := deleteRedisKeysByPattern(ctx, s.coordinator.redis, definition.pattern)
|
||||
deleted, err := deleteRedisKeysByPattern(ctx, s.coordinator.Redis(), definition.pattern)
|
||||
if err != nil {
|
||||
return nil, errors.New("清理 Redis 运行时缓存失败")
|
||||
}
|
||||
@@ -253,18 +251,15 @@ func (s *Service) ClearAdminRuntimeCache(ctx context.Context, actor *model.User,
|
||||
result.DeletedKeys += deleted
|
||||
}
|
||||
} else {
|
||||
s.coordinator.localMu.Lock()
|
||||
count := len(s.coordinator.localRate)
|
||||
s.coordinator.localRate = map[string]localRateEntry{}
|
||||
s.coordinator.localMu.Unlock()
|
||||
count := s.coordinator.ClearLocalRate()
|
||||
result.DeletedKeys += int64(count)
|
||||
result.Groups = append(result.Groups, AdminCacheClearGroupResult{ID: "rateLimits", DeletedKeys: int64(count)})
|
||||
}
|
||||
}
|
||||
s.initReadCaches()
|
||||
result.MemoryEntries += s.concurrencyReadCache.clearAndCount()
|
||||
result.MemoryEntries += s.textReplayReadCache.clearAndCount()
|
||||
result.MemoryEntries += s.routeVersionReadCache.clearAndCount()
|
||||
result.MemoryEntries += s.concurrencyReadCache.ClearAndCount()
|
||||
result.MemoryEntries += s.textReplayReadCache.ClearAndCount()
|
||||
result.MemoryEntries += s.routeVersionReadCache.ClearAndCount()
|
||||
s.routeHealthMu.Lock()
|
||||
result.MemoryEntries += len(s.routeHealthBlocked)
|
||||
s.routeHealthBlocked = make(map[string]time.Time)
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"infinite-canvas/backend/internal/database"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/platform"
|
||||
"infinite-canvas/backend/internal/repository"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -78,17 +79,19 @@ func TestAdminSystemPerformanceReportsSQLiteWithoutRedis(t *testing.T) {
|
||||
func TestClearAdminRuntimeCacheRejectsUnknownScopeAndAuditsLocalClear(t *testing.T) {
|
||||
svc, db := newSystemPerformanceTestService(t)
|
||||
admin := &model.User{ID: "admin-1", Role: model.UserRoleAdmin}
|
||||
svc.coordinator.localRate["user-1"] = localRateEntry{started: time.Now(), count: 2}
|
||||
if ok, err := svc.coordinator.Allow(context.Background(), "user-1", 10, time.Hour); err != nil || !ok {
|
||||
t.Fatalf("seed local rate cache: ok=%v err=%v", ok, err)
|
||||
}
|
||||
|
||||
if _, err := svc.ClearAdminRuntimeCache(context.Background(), admin, AdminCacheClearRequest{Scope: "all"}); authStatus(err) != 400 {
|
||||
t.Fatalf("unknown scope error = %#v, want 400", err)
|
||||
}
|
||||
if len(svc.coordinator.localRate) != 1 {
|
||||
if svc.coordinator.LocalRateCount() != 1 {
|
||||
t.Fatal("invalid scope changed local runtime cache")
|
||||
}
|
||||
|
||||
svc.initReadCaches()
|
||||
if _, err := svc.routeVersionReadCache.get(context.Background(), "route-version", func(context.Context) (int64, int, error) {
|
||||
if _, err := svc.routeVersionReadCache.Get(context.Background(), "route-version", func(context.Context) (int64, int, error) {
|
||||
return 7, 256, nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -102,8 +105,8 @@ func TestClearAdminRuntimeCacheRejectsUnknownScopeAndAuditsLocalClear(t *testing
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.DeletedKeys != 1 || len(svc.coordinator.localRate) != 0 {
|
||||
t.Fatalf("local clear result = %#v, remaining = %d", result, len(svc.coordinator.localRate))
|
||||
if result.DeletedKeys != 1 || svc.coordinator.LocalRateCount() != 0 {
|
||||
t.Fatalf("local clear result = %#v, remaining = %d", result, svc.coordinator.LocalRateCount())
|
||||
}
|
||||
if result.MemoryEntries != 3 {
|
||||
t.Fatalf("memory entries = %d, want 3", result.MemoryEntries)
|
||||
@@ -123,7 +126,7 @@ func TestClearAdminRuntimeCacheRejectsUnknownScopeAndAuditsLocalClear(t *testing
|
||||
func TestClearAdminRuntimeCacheUsesRedisWhitelist(t *testing.T) {
|
||||
client := startSystemPerformanceRedis(t)
|
||||
svc, db := newSystemPerformanceTestService(t)
|
||||
svc.coordinator = &runtimeCoordinator{redis: client, instanceID: "system-performance-test", localRate: map[string]localRateEntry{}, localSlots: map[string]map[string]time.Time{}}
|
||||
svc.coordinator = platform.NewCoordinatorWithRedis(client, "system-performance-test")
|
||||
admin := &model.User{ID: "admin-redis", Role: model.UserRoleAdmin}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -189,7 +192,7 @@ func newSystemPerformanceTestService(t *testing.T) (*Service, *gorm.DB) {
|
||||
return &Service{
|
||||
repo: repository.New(db),
|
||||
dataDir: dataDir,
|
||||
coordinator: &runtimeCoordinator{instanceID: "local-test", localRate: map[string]localRateEntry{}, localSlots: map[string]map[string]time.Time{}},
|
||||
coordinator: platform.NewCoordinatorWithRedis(nil, "local-test"),
|
||||
routeCatalogTTL: 30 * time.Second,
|
||||
routeCatalogMaxStale: 5 * time.Minute,
|
||||
routeHealthBlocked: map[string]time.Time{},
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
261
backend/internal/app/auth_bridge.go
Normal file
261
backend/internal/app/auth_bridge.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/auth"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
const SessionCookieName = auth.SessionCookieName
|
||||
|
||||
type (
|
||||
EmailCodeCooldownError = auth.EmailCodeCooldownError
|
||||
RegisterRequest = auth.RegisterRequest
|
||||
LoginRequest = auth.LoginRequest
|
||||
PublicAuthSettings = auth.PublicAuthSettings
|
||||
AuthSessionResult = auth.AuthSessionResult
|
||||
AuthUser = auth.AuthUser
|
||||
RegistrationSettingRequest = auth.RegistrationSettingRequest
|
||||
PublicRegistrationSetting = auth.PublicRegistrationSetting
|
||||
PasswordResetRequest = auth.PasswordResetRequest
|
||||
EmailSettingRequest = auth.EmailSettingRequest
|
||||
PublicEmailSetting = auth.PublicEmailSetting
|
||||
LinuxDOSettingRequest = auth.LinuxDOSettingRequest
|
||||
PublicLinuxDOSetting = auth.PublicLinuxDOSetting
|
||||
LinuxDOCallbackResult = auth.LinuxDOCallbackResult
|
||||
LibTVSettingRequest = auth.LibTVSettingRequest
|
||||
PublicLibTVSetting = auth.PublicLibTVSetting
|
||||
LibTVImportRequest = auth.LibTVImportRequest
|
||||
LibTVImportResult = auth.LibTVImportResult
|
||||
LibTVCanvasConnection = auth.LibTVCanvasConnection
|
||||
LibTVCanvasNode = auth.LibTVCanvasNode
|
||||
LibTVImportIssue = auth.LibTVImportIssue
|
||||
LibTVImportMetadata = auth.LibTVImportMetadata
|
||||
LibTVImportWarning = auth.LibTVImportWarning
|
||||
TapNowImportRequest = auth.TapNowImportRequest
|
||||
TapNowImportResult = auth.TapNowImportResult
|
||||
TapNowCanvasConnection = auth.TapNowCanvasConnection
|
||||
TapNowCanvasNode = auth.TapNowCanvasNode
|
||||
TapNowImportIssue = auth.TapNowImportIssue
|
||||
TapNowImportMetadata = auth.TapNowImportMetadata
|
||||
TapNowImportWarning = auth.TapNowImportWarning
|
||||
)
|
||||
|
||||
type authHost struct {
|
||||
svc *Service
|
||||
}
|
||||
|
||||
func (h authHost) RequireAdmin(user *model.User) error {
|
||||
if h.svc == nil {
|
||||
return nil
|
||||
}
|
||||
return h.svc.RequireAdmin(user)
|
||||
}
|
||||
|
||||
func (h authHost) EncryptSecret(value string) (string, error) {
|
||||
if h.svc == nil {
|
||||
return value, nil
|
||||
}
|
||||
return h.svc.encryptSettingSecret(value)
|
||||
}
|
||||
|
||||
func (h authHost) DecryptSecret(value string) (string, error) {
|
||||
if h.svc == nil {
|
||||
return value, nil
|
||||
}
|
||||
return h.svc.decryptSettingSecret(value)
|
||||
}
|
||||
|
||||
func (h authHost) SettingsEncryptionKey() ([]byte, error) {
|
||||
if h.svc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return h.svc.settingsEncryptionKey()
|
||||
}
|
||||
|
||||
func (h authHost) BrandName() string {
|
||||
if h.svc == nil {
|
||||
return auth.DefaultBrandName
|
||||
}
|
||||
return h.svc.appearanceBrandName()
|
||||
}
|
||||
|
||||
func (h authHost) EnsureSignupBonus(userID string) error {
|
||||
if h.svc == nil {
|
||||
return nil
|
||||
}
|
||||
return h.svc.ensureSignupBonus(userID)
|
||||
}
|
||||
|
||||
func (h authHost) RecordActivity(userID string, event string, count int) {
|
||||
if h.svc == nil {
|
||||
return
|
||||
}
|
||||
h.svc.recordActivity(userID, event, count)
|
||||
}
|
||||
|
||||
func (h authHost) AllowRequest(ctx context.Context, key string, limit int, window time.Duration) (bool, error) {
|
||||
if h.svc == nil {
|
||||
return true, nil
|
||||
}
|
||||
return h.svc.AllowRequest(ctx, key, limit, window)
|
||||
}
|
||||
|
||||
func (h authHost) RequestRetryAfter(ctx context.Context, key string, window time.Duration) time.Duration {
|
||||
if h.svc == nil {
|
||||
return 0
|
||||
}
|
||||
return h.svc.RequestRetryAfter(ctx, key, window)
|
||||
}
|
||||
|
||||
func (s *Service) authDomain() *auth.Service {
|
||||
if s == nil {
|
||||
return auth.New(nil, nil, nil)
|
||||
}
|
||||
if s.auth != nil {
|
||||
return s.auth
|
||||
}
|
||||
return auth.New(s.repo, authHost{svc: s}, nil)
|
||||
}
|
||||
|
||||
func (s *Service) PublicAuthSettings() (*PublicAuthSettings, error) {
|
||||
return s.authDomain().PublicAuthSettings()
|
||||
}
|
||||
|
||||
func (s *Service) Register(req RegisterRequest) (*AuthSessionResult, error) {
|
||||
return s.authDomain().Register(req)
|
||||
}
|
||||
|
||||
func (s *Service) Login(req LoginRequest) (*AuthSessionResult, error) {
|
||||
return s.authDomain().Login(req)
|
||||
}
|
||||
|
||||
func (s *Service) Logout(cookieValue string) error {
|
||||
return s.authDomain().Logout(cookieValue)
|
||||
}
|
||||
|
||||
func (s *Service) CurrentUser(cookieValue string) (*model.User, error) {
|
||||
return s.authDomain().CurrentUser(cookieValue)
|
||||
}
|
||||
|
||||
func (s *Service) PublicAuthUser(user *model.User) (AuthUser, error) {
|
||||
return s.authDomain().PublicAuthUser(user)
|
||||
}
|
||||
|
||||
func (s *Service) AdminRegistrationSetting(actor *model.User) (*PublicRegistrationSetting, error) {
|
||||
return s.authDomain().AdminRegistrationSetting(actor)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateRegistrationSetting(actor *model.User, req RegistrationSettingRequest) (*PublicRegistrationSetting, error) {
|
||||
return s.authDomain().UpdateRegistrationSetting(actor, req)
|
||||
}
|
||||
|
||||
func (s *Service) RegistrationEnabled() (bool, error) {
|
||||
return s.authDomain().RegistrationEnabled()
|
||||
}
|
||||
|
||||
func (s *Service) SendPasswordResetEmailCode(rawEmail string) error {
|
||||
return s.authDomain().SendPasswordResetEmailCode(rawEmail)
|
||||
}
|
||||
|
||||
func (s *Service) ResetPassword(req PasswordResetRequest) error {
|
||||
return s.authDomain().ResetPassword(req)
|
||||
}
|
||||
|
||||
func (s *Service) AdminEmailSetting(actor *model.User) (*PublicEmailSetting, error) {
|
||||
return s.authDomain().AdminEmailSetting(actor)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateEmailSetting(actor *model.User, req EmailSettingRequest) (*PublicEmailSetting, error) {
|
||||
return s.authDomain().UpdateEmailSetting(actor, req)
|
||||
}
|
||||
|
||||
func (s *Service) EmailEnabled() (bool, error) {
|
||||
return s.authDomain().EmailEnabled()
|
||||
}
|
||||
|
||||
func (s *Service) SendRegistrationEmailCode(rawEmail string) error {
|
||||
return s.authDomain().SendRegistrationEmailCode(rawEmail)
|
||||
}
|
||||
|
||||
func (s *Service) VerifyRegistrationEmailCode(email string, rawCode string) (*model.EmailVerificationCode, error) {
|
||||
return s.authDomain().VerifyRegistrationEmailCode(email, rawCode)
|
||||
}
|
||||
|
||||
func (s *Service) AdminLinuxDOSetting(actor *model.User) (*PublicLinuxDOSetting, error) {
|
||||
return s.authDomain().AdminLinuxDOSetting(actor)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateLinuxDOSetting(actor *model.User, req LinuxDOSettingRequest) (*PublicLinuxDOSetting, error) {
|
||||
return s.authDomain().UpdateLinuxDOSetting(actor, req)
|
||||
}
|
||||
|
||||
func (s *Service) LinuxDOEnabled() bool {
|
||||
return s.authDomain().LinuxDOEnabled()
|
||||
}
|
||||
|
||||
func (s *Service) BeginLinuxDOLogin(nextPath string) (string, error) {
|
||||
return s.authDomain().BeginLinuxDOLogin(nextPath)
|
||||
}
|
||||
|
||||
func (s *Service) CompleteLinuxDOLogin(stateValue string, code string) (*LinuxDOCallbackResult, error) {
|
||||
return s.authDomain().CompleteLinuxDOLogin(stateValue, code)
|
||||
}
|
||||
|
||||
func (s *Service) AdminLibTVSetting(actor *model.User) (*PublicLibTVSetting, error) {
|
||||
return s.authDomain().AdminLibTVSetting(actor)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateLibTVSetting(actor *model.User, req LibTVSettingRequest) (*PublicLibTVSetting, error) {
|
||||
return s.authDomain().UpdateLibTVSetting(actor, req)
|
||||
}
|
||||
|
||||
func (s *Service) TestLibTV(actor *model.User, projectUUID string) error {
|
||||
return s.authDomain().TestLibTV(actor, projectUUID)
|
||||
}
|
||||
|
||||
func (s *Service) ImportLibTV(userID, canvasProjectID, projectUUID string) (*LibTVImportResult, error) {
|
||||
return s.authDomain().ImportLibTV(userID, canvasProjectID, projectUUID)
|
||||
}
|
||||
|
||||
func (s *Service) ImportTapNow(userID, canvasProjectID, shareID string) (*TapNowImportResult, error) {
|
||||
return s.authDomain().ImportTapNow(userID, canvasProjectID, shareID)
|
||||
}
|
||||
|
||||
func hashPassword(password string) (string, error) {
|
||||
return auth.HashPassword(password)
|
||||
}
|
||||
|
||||
func normalizeUsername(value string) string {
|
||||
return auth.NormalizeUsername(value)
|
||||
}
|
||||
|
||||
func normalizeEmail(value string) string {
|
||||
return auth.NormalizeEmail(value)
|
||||
}
|
||||
|
||||
func normalizeDisplayName(value string, fallback string) string {
|
||||
return auth.NormalizeDisplayName(value, fallback)
|
||||
}
|
||||
|
||||
func validateUsername(value string) error {
|
||||
return auth.ValidateUsername(value)
|
||||
}
|
||||
|
||||
func validatePassword(value string) error {
|
||||
return auth.ValidatePassword(value)
|
||||
}
|
||||
|
||||
func validateEmail(value string) error {
|
||||
return auth.ValidateEmail(value)
|
||||
}
|
||||
|
||||
func hashToken(token string) string {
|
||||
return auth.HashToken(token)
|
||||
}
|
||||
|
||||
func randomToken() string {
|
||||
return auth.RandomToken()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
236
backend/internal/app/canvas_bridge.go
Normal file
236
backend/internal/app/canvas_bridge.go
Normal file
@@ -0,0 +1,236 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"infinite-canvas/backend/internal/assets"
|
||||
"infinite-canvas/backend/internal/canvas"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
type (
|
||||
CanvasShareRequest = canvas.CanvasShareRequest
|
||||
CanvasShareStatus = canvas.CanvasShareStatus
|
||||
PublicCanvasShare = canvas.PublicCanvasShare
|
||||
AssetsSyncRequest = canvas.AssetsSyncRequest
|
||||
CanvasProjectsSyncRequest = canvas.CanvasProjectsSyncRequest
|
||||
UserDataSummary = canvas.UserDataSummary
|
||||
UserDataSnapshot = canvas.UserDataSnapshot
|
||||
CanvasLibrarySummary = canvas.CanvasLibrarySummary
|
||||
CanvasLibraryPage = canvas.CanvasLibraryPage
|
||||
)
|
||||
|
||||
type canvasHost struct {
|
||||
svc *Service
|
||||
}
|
||||
|
||||
func (h canvasHost) EncryptSecret(value string) (string, error) {
|
||||
if h.svc == nil {
|
||||
return value, nil
|
||||
}
|
||||
return h.svc.encryptSettingSecret(value)
|
||||
}
|
||||
|
||||
func (h canvasHost) DecryptSecret(value string) (string, error) {
|
||||
if h.svc == nil {
|
||||
return value, nil
|
||||
}
|
||||
return h.svc.decryptSettingSecret(value)
|
||||
}
|
||||
|
||||
func (h canvasHost) OpenResourceRange(userID string, resource *model.Resource, rangeHeader string) (*assets.ResourceStream, error) {
|
||||
if h.svc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return h.svc.openResourceRange(userID, resource, rangeHeader)
|
||||
}
|
||||
|
||||
func (h canvasHost) PrepareResourceDelivery(userID string, resource *model.Resource, options assets.ResourceDeliveryOptions) (*assets.ResourceDelivery, error) {
|
||||
if h.svc == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return h.svc.prepareResourceDelivery(userID, resource, options)
|
||||
}
|
||||
|
||||
func (h canvasHost) WithStorageLock(fn func() error) error {
|
||||
if h.svc == nil {
|
||||
if fn == nil {
|
||||
return nil
|
||||
}
|
||||
return fn()
|
||||
}
|
||||
h.svc.storageMu.Lock()
|
||||
defer h.svc.storageMu.Unlock()
|
||||
return fn()
|
||||
}
|
||||
|
||||
func (h canvasHost) StructuredQuota(userID, kind string, creating bool, deltaBytes int64) error {
|
||||
if h.svc == nil {
|
||||
return nil
|
||||
}
|
||||
policy, err := h.svc.RuntimePolicy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
usage, err := h.svc.repo.UserStorageUsage(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return validateStructuredStorageQuotaWithPolicy(usage, kind, creating, deltaBytes, policy.Resource)
|
||||
}
|
||||
|
||||
func (h canvasHost) StructuredReplacementQuota(userID, kind string, count int, bytes int64) error {
|
||||
if h.svc == nil {
|
||||
return nil
|
||||
}
|
||||
policy, err := h.svc.RuntimePolicy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
usage, err := h.svc.repo.UserStorageUsage(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return validateStructuredReplacementQuotaWithPolicy(usage, kind, count, bytes, policy.Resource)
|
||||
}
|
||||
|
||||
func (h canvasHost) DeleteUserAssetWithResources(userID, assetID string) error {
|
||||
if h.svc == nil {
|
||||
return nil
|
||||
}
|
||||
return h.svc.deleteUserAssetWithResources(userID, assetID)
|
||||
}
|
||||
|
||||
func (h canvasHost) RecordActivity(userID, event string, count int) {
|
||||
if h.svc == nil {
|
||||
return
|
||||
}
|
||||
h.svc.recordActivity(userID, event, count)
|
||||
}
|
||||
|
||||
func (s *Service) canvasDomain() *canvas.Service {
|
||||
if s == nil {
|
||||
return canvas.New(nil, nil)
|
||||
}
|
||||
if s.canvas != nil {
|
||||
return s.canvas
|
||||
}
|
||||
return canvas.New(s.repo, canvasHost{svc: s})
|
||||
}
|
||||
|
||||
func (s *Service) CanvasShareStatus(userID string, projectID string) (CanvasShareStatus, error) {
|
||||
return s.canvasDomain().CanvasShareStatus(userID, projectID)
|
||||
}
|
||||
|
||||
func (s *Service) CreateCanvasShare(userID string, projectID string, req CanvasShareRequest) (CanvasShareStatus, error) {
|
||||
return s.canvasDomain().CreateCanvasShare(userID, projectID, req)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteCanvasShare(userID string, projectID string) error {
|
||||
return s.canvasDomain().DeleteCanvasShare(userID, projectID)
|
||||
}
|
||||
|
||||
func (s *Service) PublicCanvasShare(token string) (PublicCanvasShare, error) {
|
||||
return s.canvasDomain().PublicCanvasShare(token)
|
||||
}
|
||||
|
||||
func (s *Service) OpenSharedCanvasResource(token string, resourceID string) (*model.Resource, io.ReadCloser, error) {
|
||||
return s.canvasDomain().OpenSharedCanvasResource(token, resourceID)
|
||||
}
|
||||
|
||||
func (s *Service) OpenSharedCanvasResourceRange(token string, resourceID string, rangeHeader string) (*ResourceStream, error) {
|
||||
return s.canvasDomain().OpenSharedCanvasResourceRange(token, resourceID, rangeHeader)
|
||||
}
|
||||
|
||||
func (s *Service) PrepareSharedCanvasResourceDelivery(token string, resourceID string, rangeHeader string) (*ResourceDelivery, error) {
|
||||
return s.canvasDomain().PrepareSharedCanvasResourceDelivery(token, resourceID, rangeHeader)
|
||||
}
|
||||
|
||||
func (s *Service) validateCanvasMediaAssets(userID string, raw json.RawMessage) error {
|
||||
return s.canvasDomain().ValidateCanvasMediaAssets(userID, raw)
|
||||
}
|
||||
|
||||
func (s *Service) validateAssetCanvasReferences(userID string, asset model.Asset) error {
|
||||
return s.canvasDomain().ValidateAssetCanvasReferences(userID, asset)
|
||||
}
|
||||
|
||||
func (s *Service) validateAssetReplacementCanvasReferences(userID string, replacement []model.Asset) error {
|
||||
return s.canvasDomain().ValidateAssetReplacementCanvasReferences(userID, replacement)
|
||||
}
|
||||
|
||||
func (s *Service) UserDataSnapshot(userID string) (UserDataSnapshot, error) {
|
||||
return s.canvasDomain().UserDataSnapshot(userID)
|
||||
}
|
||||
|
||||
func (s *Service) UserAssetSummaries(userID string) ([]UserDataSummary, error) {
|
||||
return s.canvasDomain().UserAssetSummaries(userID)
|
||||
}
|
||||
|
||||
func (s *Service) UserAsset(userID string, id string) (json.RawMessage, error) {
|
||||
return s.canvasDomain().UserAsset(userID, id)
|
||||
}
|
||||
|
||||
func (s *Service) UpsertUserAsset(userID string, raw json.RawMessage) (UserDataSummary, error) {
|
||||
return s.canvasDomain().UpsertUserAsset(userID, raw)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteUserAsset(userID string, id string) error {
|
||||
return s.canvasDomain().DeleteUserAsset(userID, id)
|
||||
}
|
||||
|
||||
func (s *Service) UserAssets(userID string) ([]json.RawMessage, error) {
|
||||
return s.canvasDomain().UserAssets(userID)
|
||||
}
|
||||
|
||||
func (s *Service) ReplaceUserAssets(userID string, req AssetsSyncRequest) ([]json.RawMessage, error) {
|
||||
return s.canvasDomain().ReplaceUserAssets(userID, req)
|
||||
}
|
||||
|
||||
func (s *Service) UserCanvasProjects(userID string) ([]json.RawMessage, error) {
|
||||
return s.canvasDomain().UserCanvasProjects(userID)
|
||||
}
|
||||
|
||||
func (s *Service) UserCanvasProjectSummaries(userID string) ([]UserDataSummary, error) {
|
||||
return s.canvasDomain().UserCanvasProjectSummaries(userID)
|
||||
}
|
||||
|
||||
func (s *Service) UserCanvasProject(userID string, id string) (json.RawMessage, error) {
|
||||
return s.canvasDomain().UserCanvasProject(userID, id)
|
||||
}
|
||||
|
||||
func (s *Service) UpsertUserCanvasProject(userID string, raw json.RawMessage) (UserDataSummary, error) {
|
||||
return s.canvasDomain().UpsertUserCanvasProject(userID, raw)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteUserCanvasProject(userID string, id string) error {
|
||||
return s.canvasDomain().DeleteUserCanvasProject(userID, id)
|
||||
}
|
||||
|
||||
func (s *Service) ReplaceUserCanvasProjects(userID string, req CanvasProjectsSyncRequest) ([]json.RawMessage, error) {
|
||||
return s.canvasDomain().ReplaceUserCanvasProjects(userID, req)
|
||||
}
|
||||
|
||||
func (s *Service) UserAssetsByIDs(userID string, ids []string) ([]json.RawMessage, error) {
|
||||
return s.canvasDomain().UserAssetsByIDs(userID, ids)
|
||||
}
|
||||
|
||||
func (s *Service) UserCanvasProjectsPage(userID string, page int, pageSize int, projectID string, search string, sort string) (CanvasLibraryPage, error) {
|
||||
return s.canvasDomain().UserCanvasProjectsPage(userID, page, pageSize, projectID, search, sort)
|
||||
}
|
||||
|
||||
func clientAssetPayload(asset model.Asset) json.RawMessage {
|
||||
return canvas.ClientAssetPayload(asset)
|
||||
}
|
||||
|
||||
func validateSyncedPayload(raw json.RawMessage, label string) error {
|
||||
return canvas.ValidateSyncedPayload(raw, label)
|
||||
}
|
||||
|
||||
func containsInlineMediaDataURL(value interface{}) bool {
|
||||
return canvas.ContainsInlineMediaDataURL(value)
|
||||
}
|
||||
|
||||
func assetFromJSON(userID string, raw json.RawMessage) (model.Asset, error) {
|
||||
return canvas.AssetFromJSON(userID, raw)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"infinite-canvas/backend/internal/model"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,12 +1,14 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"infinite-canvas/backend/internal/prompts"
|
||||
)
|
||||
|
||||
func TestChapterAssetsPromptContract(t *testing.T) {
|
||||
if !json.Valid([]byte(chapterAssetsJSONSchema)) {
|
||||
if !json.Valid([]byte(prompts.ChapterAssetsJSONSchema)) {
|
||||
t.Fatal("invalid chapter assets schema")
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import "testing"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
42
backend/internal/app/error_codes.go
Normal file
42
backend/internal/app/error_codes.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package app
|
||||
|
||||
import "infinite-canvas/backend/internal/kernel"
|
||||
|
||||
// 错误码和原因已迁到 kernel;此处保留兼容别名。
|
||||
const (
|
||||
CodeOK = kernel.CodeOK
|
||||
CodeInvalidArgument = kernel.CodeInvalidArgument
|
||||
CodeUnauthorized = kernel.CodeUnauthorized
|
||||
CodeForbidden = kernel.CodeForbidden
|
||||
CodeNotFound = kernel.CodeNotFound
|
||||
CodeConflict = kernel.CodeConflict
|
||||
CodeTooManyRequests = kernel.CodeTooManyRequests
|
||||
CodeInternal = kernel.CodeInternal
|
||||
CodeBadGateway = kernel.CodeBadGateway
|
||||
CodeUnavailable = kernel.CodeUnavailable
|
||||
CodeTimeout = kernel.CodeTimeout
|
||||
CodeQuotaExceeded = kernel.CodeQuotaExceeded
|
||||
CodeIdempotencyConflict = kernel.CodeIdempotencyConflict
|
||||
CodeRateLimited = kernel.CodeRateLimited
|
||||
)
|
||||
|
||||
type ErrorReason = kernel.ErrorReason
|
||||
|
||||
const (
|
||||
ReasonInvalidArgument = kernel.ReasonInvalidArgument
|
||||
ReasonUnauthorized = kernel.ReasonUnauthorized
|
||||
ReasonForbidden = kernel.ReasonForbidden
|
||||
ReasonNotFound = kernel.ReasonNotFound
|
||||
ReasonConflict = kernel.ReasonConflict
|
||||
ReasonFailedPrecondition = kernel.ReasonFailedPrecondition
|
||||
ReasonQuotaExceeded = kernel.ReasonQuotaExceeded
|
||||
ReasonRateLimited = kernel.ReasonRateLimited
|
||||
ReasonUnavailable = kernel.ReasonUnavailable
|
||||
ReasonTimeout = kernel.ReasonTimeout
|
||||
ReasonInternal = kernel.ReasonInternal
|
||||
ReasonBadGateway = kernel.ReasonBadGateway
|
||||
)
|
||||
|
||||
func ReasonForStatus(status int) ErrorReason {
|
||||
return kernel.ReasonForStatus(status)
|
||||
}
|
||||
@@ -1,48 +1,43 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import "fmt"
|
||||
import "infinite-canvas/backend/internal/kernel"
|
||||
|
||||
// AppError 是 service 层对外公开的结构化错误。
|
||||
// Message 必须可安全展示给用户,Cause 仅用于保留内部诊断链路,不得直接写入 HTTP 响应。
|
||||
type AppError struct {
|
||||
Status int
|
||||
Code int
|
||||
Reason ErrorReason
|
||||
Message string
|
||||
Retryable bool
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *AppError) Unwrap() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
return e.Cause
|
||||
}
|
||||
// AppError 是 service 层对外公开的结构化错误;实现已迁到 kernel。
|
||||
type AppError = kernel.AppError
|
||||
|
||||
func NewAppError(status int, message string) *AppError {
|
||||
return &AppError{Status: status, Code: status, Reason: ReasonForStatus(status), Message: message}
|
||||
return kernel.NewAppError(status, message)
|
||||
}
|
||||
|
||||
func WrapAppError(status int, message string, cause error) *AppError {
|
||||
err := NewAppError(status, message)
|
||||
err.Cause = cause
|
||||
return err
|
||||
return kernel.WrapAppError(status, message, cause)
|
||||
}
|
||||
|
||||
func RateLimited(message string) *AppError {
|
||||
return &AppError{Status: CodeTooManyRequests, Code: CodeRateLimited, Reason: ReasonRateLimited, Message: message, Retryable: true}
|
||||
return kernel.RateLimited(message)
|
||||
}
|
||||
|
||||
func QuotaExceeded(message string) *AppError {
|
||||
return &AppError{Status: CodeForbidden, Code: CodeQuotaExceeded, Reason: ReasonQuotaExceeded, Message: message}
|
||||
return kernel.QuotaExceeded(message)
|
||||
}
|
||||
|
||||
// AuthError 保留为兼容别名。
|
||||
type AuthError = AppError
|
||||
|
||||
func BadAuthRequest(message string) *AuthError {
|
||||
return kernel.BadAuthRequest(message)
|
||||
}
|
||||
|
||||
func NotFound(message string) *AuthError {
|
||||
return kernel.NotFound(message)
|
||||
}
|
||||
|
||||
func Unauthorized(message string) *AuthError {
|
||||
return kernel.Unauthorized(message)
|
||||
}
|
||||
|
||||
func Forbidden(message string) *AuthError {
|
||||
return kernel.Forbidden(message)
|
||||
}
|
||||
|
||||
// ModelErrorCode 定义模型相关的错误码
|
||||
@@ -161,7 +156,7 @@ func GetModelErrorCode(err error) ModelErrorCode {
|
||||
func FormatModelError(err error) string {
|
||||
if modelErr, ok := err.(*ModelError); ok {
|
||||
if len(modelErr.Details) > 0 {
|
||||
return fmt.Sprintf("%s (错误码: %s)", modelErr.Message, modelErr.ErrorCode)
|
||||
return kernel.StringValue(modelErr.Message) + " (错误码: " + string(modelErr.ErrorCode) + ")"
|
||||
}
|
||||
return modelErr.Message
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
53
backend/internal/app/generation_registry_bridge.go
Normal file
53
backend/internal/app/generation_registry_bridge.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"infinite-canvas/backend/internal/generation"
|
||||
"infinite-canvas/backend/internal/protocol"
|
||||
)
|
||||
|
||||
// 协议注册表运行时已迁到 internal/generation;此处保留未导出包装以兼容 service 内大量调用点。
|
||||
|
||||
// emptyProtocolRegistry 供单测显式注入「插件未安装」路径。
|
||||
var emptyProtocolRegistry, _ = protocol.NewRegistry()
|
||||
|
||||
func withProtocolRegistry(ctx context.Context, registry *protocol.Registry) context.Context {
|
||||
return generation.WithProtocolRegistry(ctx, registry)
|
||||
}
|
||||
|
||||
func protocolRegistryFromContext(ctx context.Context) (*protocol.Registry, bool) {
|
||||
return generation.ProtocolRegistryFromContext(ctx)
|
||||
}
|
||||
|
||||
func protocolAdapterForContext(ctx context.Context, id string) (protocol.Adapter, bool) {
|
||||
return generation.ProtocolAdapterForContext(ctx, id)
|
||||
}
|
||||
|
||||
func ensureOfficialProtocolAdapter(ctx context.Context, interfaceType string) context.Context {
|
||||
return generation.EnsureOfficialProtocolAdapter(ctx, interfaceType)
|
||||
}
|
||||
|
||||
func officialDeclarativeVideoInterface(interfaceType string) (string, bool) {
|
||||
return generation.OfficialDeclarativeVideoInterface(interfaceType)
|
||||
}
|
||||
|
||||
func officialDeclarativeImageInterface(interfaceType string) (string, bool) {
|
||||
return generation.OfficialDeclarativeImageInterface(interfaceType)
|
||||
}
|
||||
|
||||
func officialDeclarativeAudioInterface(interfaceType string) (string, bool) {
|
||||
return generation.OfficialDeclarativeAudioInterface(interfaceType)
|
||||
}
|
||||
|
||||
func declarativeProtocolAdapterForContext(ctx context.Context, id string) (protocol.Adapter, bool) {
|
||||
return generation.DeclarativeProtocolAdapterForContext(ctx, id)
|
||||
}
|
||||
|
||||
func agentProtocolAdapterForContext(ctx context.Context, id string) (protocol.AgentAdapter, bool) {
|
||||
return generation.AgentProtocolAdapterForContext(ctx, id)
|
||||
}
|
||||
|
||||
func loadOfficialFallbackRegistry() *protocol.Registry {
|
||||
return generation.LoadOfficialFallbackRegistry()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
28
backend/internal/app/json_fields_test.go
Normal file
28
backend/internal/app/json_fields_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExtractProviderTaskID(t *testing.T) {
|
||||
id, err := extractProviderTaskID([]byte(`{"id":12,"task_id":"abc"}`))
|
||||
if err != nil || id != "abc" {
|
||||
t.Fatalf("extractProviderTaskID() = %q, %v", id, err)
|
||||
}
|
||||
|
||||
_, err = extractProviderTaskID([]byte(`{"id":12}`))
|
||||
if err == nil || !strings.Contains(err.Error(), "expected string") || !strings.Contains(err.Error(), "float64") {
|
||||
t.Fatalf("extractProviderTaskID(numeric id) error = %v", err)
|
||||
}
|
||||
|
||||
id, err = extractProviderTaskID([]byte(`{"data":{"task_id":"nested"}}`))
|
||||
if err != nil || id != "nested" {
|
||||
t.Fatalf("extractProviderTaskID(nested) = %q, %v", id, err)
|
||||
}
|
||||
|
||||
id, err = extractProviderTaskID([]byte(`{"name":"task-from-name"}`))
|
||||
if err != nil || id != "task-from-name" {
|
||||
t.Fatalf("extractProviderTaskID(name) = %q, %v", id, err)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import "infinite-canvas/backend/internal/model"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -280,6 +280,55 @@ func capabilitySpecWithRoutePresets(spec CapabilitySpec, routes []CapabilitySpec
|
||||
}
|
||||
result.Options[name] = OptionConstraint{Values: values}
|
||||
}
|
||||
if result.ImageSize == nil || result.ImageSize.Parameter == "" || len(result.ImageSize.Presets) == 0 {
|
||||
if merged := mergeCapabilityImageSize(append([]CapabilitySpec{spec}, routes...)); merged != nil {
|
||||
if result.ImageSize == nil {
|
||||
result.ImageSize = merged
|
||||
} else {
|
||||
restored := *result.ImageSize
|
||||
if restored.Parameter == "" {
|
||||
restored.Parameter = merged.Parameter
|
||||
}
|
||||
if !restored.AllowCustom {
|
||||
restored.AllowCustom = merged.AllowCustom
|
||||
}
|
||||
if len(restored.Presets) == 0 {
|
||||
restored.Presets = merged.Presets
|
||||
}
|
||||
result.ImageSize = &restored
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeCapabilityImageSize(specs []CapabilitySpec) *CapabilityImageSize {
|
||||
var result *CapabilityImageSize
|
||||
seen := map[string]bool{}
|
||||
for _, spec := range specs {
|
||||
part := spec.ImageSize
|
||||
if part == nil {
|
||||
continue
|
||||
}
|
||||
if result == nil {
|
||||
result = &CapabilityImageSize{Parameter: part.Parameter, AllowCustom: part.AllowCustom}
|
||||
} else {
|
||||
if result.Parameter != "aspect_ratio" && part.Parameter == "aspect_ratio" {
|
||||
result.Parameter = "aspect_ratio"
|
||||
} else if result.Parameter == "" {
|
||||
result.Parameter = part.Parameter
|
||||
}
|
||||
result.AllowCustom = result.AllowCustom || part.AllowCustom
|
||||
}
|
||||
for _, preset := range part.Presets {
|
||||
key := preset.Tier + ":" + preset.Ratio + ":" + preset.Size
|
||||
if key == "::" || seen[key] {
|
||||
continue
|
||||
}
|
||||
seen[key] = true
|
||||
result.Presets = append(result.Presets, preset)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -7,6 +7,25 @@ import (
|
||||
"infinite-canvas/backend/internal/model"
|
||||
)
|
||||
|
||||
func TestCapabilitySpecWithRoutePresetsRestoresImageSize(t *testing.T) {
|
||||
product := CapabilitySpec{Version: 1, Capability: "image", Options: map[string]OptionConstraint{
|
||||
"size": {Values: []any{"16:9"}},
|
||||
}}
|
||||
route := CapabilitySpec{
|
||||
Version: 1,
|
||||
Capability: "image",
|
||||
Options: map[string]OptionConstraint{"size": {Values: []any{"16:9"}}},
|
||||
ImageSize: &CapabilityImageSize{
|
||||
Parameter: "aspect_ratio",
|
||||
Presets: []CapabilityImageSizePreset{{Size: "3840x2160", Tier: "4k", Ratio: "16:9", Width: 3840, Height: 2160}},
|
||||
},
|
||||
}
|
||||
got := capabilitySpecWithRoutePresets(product, []CapabilitySpec{route})
|
||||
if got.ImageSize == nil || got.ImageSize.Parameter != "aspect_ratio" || len(got.ImageSize.Presets) != 1 || got.ImageSize.Presets[0].Tier != "4k" {
|
||||
t.Fatalf("restored imageSize = %#v", got.ImageSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchCapabilityRoutesTextImagesOnlyToDeclaredProfiles(t *testing.T) {
|
||||
plainText := CapabilitySpec{
|
||||
Version: 1,
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -416,6 +416,7 @@ func CapabilitySpecFromModelCapabilityConfig(config *ModelCapabilityConfig, capa
|
||||
}
|
||||
if image.Size.Parameter != "none" {
|
||||
spec.Options["size"] = imageSizeOptionConstraint(image.Size)
|
||||
spec.ImageSize = capabilityImageSizeFromConfig(image.Size)
|
||||
}
|
||||
if image.Quality.Supported {
|
||||
spec.Options["quality"] = anyValues(image.Quality.Values)
|
||||
@@ -492,6 +493,25 @@ func imageSizeOptionConstraint(size ImageSizeConfig) OptionConstraint {
|
||||
return anyValues(values)
|
||||
}
|
||||
|
||||
func capabilityImageSizeFromConfig(size ImageSizeConfig) *CapabilityImageSize {
|
||||
if size.Parameter == "" || size.Parameter == "none" {
|
||||
return nil
|
||||
}
|
||||
result := &CapabilityImageSize{Parameter: size.Parameter, AllowCustom: size.AllowCustom}
|
||||
for _, preset := range size.Presets {
|
||||
tier := strings.ToLower(strings.TrimSpace(preset.Tier))
|
||||
ratio := strings.TrimSpace(preset.Ratio)
|
||||
value := strings.TrimSpace(preset.Size)
|
||||
if tier == "" || ratio == "" || value == "" {
|
||||
continue
|
||||
}
|
||||
result.Presets = append(result.Presets, CapabilityImageSizePreset{
|
||||
Size: value, Tier: tier, Ratio: ratio, Width: preset.Width, Height: preset.Height,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func addInputConstraint(inputs map[string]InputConstraint, name string, min int, max int) {
|
||||
if min <= 0 && max <= 0 {
|
||||
return
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -289,6 +289,35 @@ func TestNormalizeVideoCapabilityAllowsOmittedRatio(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilitySpecFromModelCapabilityConfigProjectsImageSizePresets(t *testing.T) {
|
||||
config := &ModelCapabilityConfig{
|
||||
Version: 1,
|
||||
Image: &ImageCapabilityConfig{
|
||||
References: ImageReferenceConfig{MaxImages: 4},
|
||||
Size: ImageSizeConfig{
|
||||
Parameter: "aspect_ratio",
|
||||
Values: []string{"16:9"},
|
||||
Default: "16:9",
|
||||
Presets: []ImageSizePreset{
|
||||
{Tier: "4k", Ratio: "16:9", Size: "3840x2160", Width: 3840, Height: 2160},
|
||||
},
|
||||
},
|
||||
Quality: ImageQualityConfig{Supported: false, Default: "auto"},
|
||||
MaxOutputs: 1,
|
||||
},
|
||||
}
|
||||
spec, err := CapabilitySpecFromModelCapabilityConfig(config, "image")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if spec.Options["quality"].Values != nil {
|
||||
t.Fatalf("quality must stay undeclared when unsupported: %#v", spec.Options["quality"])
|
||||
}
|
||||
if spec.ImageSize == nil || spec.ImageSize.Parameter != "aspect_ratio" || len(spec.ImageSize.Presets) != 1 || spec.ImageSize.Presets[0].Tier != "4k" {
|
||||
t.Fatalf("imageSize presets = %#v", spec.ImageSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilitySpecFromModelCapabilityConfigProjectsImageSizeOnce(t *testing.T) {
|
||||
config := &ModelCapabilityConfig{
|
||||
Version: 1,
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -18,12 +18,27 @@ import (
|
||||
"infinite-canvas/backend/internal/repository"
|
||||
)
|
||||
|
||||
type CapabilityImageSizePreset struct {
|
||||
Size string `json:"size"`
|
||||
Tier string `json:"tier"`
|
||||
Ratio string `json:"ratio"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
type CapabilityImageSize struct {
|
||||
Parameter string `json:"parameter,omitempty"`
|
||||
AllowCustom bool `json:"allowCustom,omitempty"`
|
||||
Presets []CapabilityImageSizePreset `json:"presets,omitempty"`
|
||||
}
|
||||
|
||||
type CapabilitySpec struct {
|
||||
Version int `json:"version"`
|
||||
Capability string `json:"capability"`
|
||||
Operations []string `json:"operations,omitempty"`
|
||||
Inputs map[string]InputConstraint `json:"inputs,omitempty"`
|
||||
Options map[string]OptionConstraint `json:"options,omitempty"`
|
||||
ImageSize *CapabilityImageSize `json:"imageSize,omitempty"`
|
||||
}
|
||||
|
||||
type InputConstraint struct {
|
||||
@@ -472,12 +487,12 @@ func (s *Service) invalidateRouteCatalog() {
|
||||
if s.coordinator != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), runtimeCoordinationTimeout)
|
||||
defer cancel()
|
||||
if err := s.coordinator.bumpRouteCatalogVersion(ctx); err != nil {
|
||||
if err := s.coordinator.BumpRouteCatalogVersion(ctx); err != nil {
|
||||
log.Printf("logical model route catalog distributed invalidation failed: %v", err)
|
||||
}
|
||||
}
|
||||
s.initReadCaches()
|
||||
s.routeVersionReadCache.clear()
|
||||
s.routeVersionReadCache.Clear()
|
||||
}
|
||||
|
||||
func (s *Service) routeCatalogSnapshot() (*routeCatalogSnapshot, error) {
|
||||
@@ -533,14 +548,14 @@ func (s *Service) currentRouteCatalogVersion() int64 {
|
||||
s.routeCatalogMu.RLock()
|
||||
localVersion := s.routeCatalogVersion
|
||||
s.routeCatalogMu.RUnlock()
|
||||
if s.coordinator == nil || s.coordinator.redis == nil {
|
||||
if s.coordinator == nil || !s.coordinator.HasRedis() {
|
||||
return localVersion
|
||||
}
|
||||
s.initReadCaches()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), runtimeCoordinationTimeout)
|
||||
defer cancel()
|
||||
version, err := s.routeVersionReadCache.get(ctx, routeCatalogVersionKey, func(ctx context.Context) (int64, int, error) {
|
||||
value, err := s.coordinator.routeCatalogVersion(ctx)
|
||||
version, err := s.routeVersionReadCache.Get(ctx, routeCatalogVersionKey, func(ctx context.Context) (int64, int, error) {
|
||||
value, err := s.coordinator.RouteCatalogVersion(ctx)
|
||||
if err != nil {
|
||||
log.Printf("logical model route catalog distributed version check failed: %v", err)
|
||||
}
|
||||
@@ -845,13 +860,13 @@ func (s *Service) logicalRouteBlocked(route cachedLogicalRoute) bool {
|
||||
}
|
||||
s.routeHealthMu.Unlock()
|
||||
|
||||
if s.coordinator == nil || s.coordinator.redis == nil {
|
||||
if s.coordinator == nil || !s.coordinator.HasRedis() {
|
||||
return false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
for _, key := range keys {
|
||||
until, err := s.coordinator.routeBlockedUntil(ctx, key)
|
||||
until, err := s.coordinator.RouteBlockedUntil(ctx, key)
|
||||
if err != nil {
|
||||
log.Printf("logical route distributed health check failed key=%s: %v", key, err)
|
||||
return false
|
||||
@@ -1234,10 +1249,10 @@ func (s *Service) blockLogicalRouteForFailure(attempt *model.RouteAttempt, taskE
|
||||
// 本地状态是 Redis 不可用时的降级,也能覆盖 Redis 写入瞬间的网络抖动。
|
||||
s.routeHealthBlocked[key] = until
|
||||
s.routeHealthMu.Unlock()
|
||||
if s.coordinator != nil && s.coordinator.redis != nil {
|
||||
if s.coordinator != nil && s.coordinator.HasRedis() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
if err := s.coordinator.blockRoute(ctx, key, until); err != nil {
|
||||
if err := s.coordinator.BlockRoute(ctx, key, until); err != nil {
|
||||
log.Printf("logical route distributed health update failed key=%s: %v", key, err)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
81
backend/internal/app/outbound_alias.go
Normal file
81
backend/internal/app/outbound_alias.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/outbound"
|
||||
)
|
||||
|
||||
// Outbound 符号从 internal/outbound 再导出,保持 service/handler 调用面稳定。
|
||||
type OutboundHeader = outbound.OutboundHeader
|
||||
|
||||
const (
|
||||
CustomRelayHeadersHeader = outbound.CustomRelayHeadersHeader
|
||||
DefaultOutboundUserAgent = outbound.DefaultOutboundUserAgent
|
||||
)
|
||||
|
||||
func ValidateOutboundURL(rawURL string) (*url.URL, error) {
|
||||
parsed, err := outbound.ValidateOutboundURL(rawURL)
|
||||
return parsed, mapOutboundError(err)
|
||||
}
|
||||
|
||||
func ValidateCustomRelayURL(rawURL string) (*url.URL, error) {
|
||||
parsed, err := outbound.ValidateCustomRelayURL(rawURL)
|
||||
return parsed, mapOutboundError(err)
|
||||
}
|
||||
|
||||
func OutboundHTTPClient(timeout time.Duration) *http.Client {
|
||||
return outbound.OutboundHTTPClient(timeout)
|
||||
}
|
||||
|
||||
func ApplyDefaultOutboundHeaders(req *http.Request) {
|
||||
outbound.ApplyDefaultOutboundHeaders(req)
|
||||
}
|
||||
|
||||
func NormalizeOutboundHeaders(headers []OutboundHeader) ([]OutboundHeader, error) {
|
||||
normalized, err := outbound.NormalizeOutboundHeaders(headers)
|
||||
return normalized, mapOutboundError(err)
|
||||
}
|
||||
|
||||
func ApplyOutboundHeaders(req *http.Request, headers []OutboundHeader) {
|
||||
outbound.ApplyOutboundHeaders(req, headers)
|
||||
}
|
||||
|
||||
func EncodeOutboundHeadersJSON(headers []OutboundHeader) (string, error) {
|
||||
encoded, err := outbound.EncodeOutboundHeadersJSON(headers)
|
||||
return encoded, mapOutboundError(err)
|
||||
}
|
||||
|
||||
func ParseOutboundHeadersJSON(raw string) ([]OutboundHeader, error) {
|
||||
decoded, err := outbound.ParseOutboundHeadersJSON(raw)
|
||||
return decoded, mapOutboundError(err)
|
||||
}
|
||||
|
||||
func DecodeRelayOutboundHeaders(encoded string) ([]OutboundHeader, error) {
|
||||
decoded, err := outbound.DecodeRelayOutboundHeaders(encoded)
|
||||
return decoded, mapOutboundError(err)
|
||||
}
|
||||
|
||||
func CustomRelayHTTPClient(timeout time.Duration) *http.Client {
|
||||
return outbound.CustomRelayHTTPClient(timeout)
|
||||
}
|
||||
|
||||
func ValidateOutboundHost(host string) error {
|
||||
return mapOutboundError(outbound.ValidateOutboundHost(host))
|
||||
}
|
||||
|
||||
func AllowedPrivateUpstreamHost(host string) bool {
|
||||
return outbound.AllowedPrivateUpstreamHost(host)
|
||||
}
|
||||
|
||||
func mapOutboundError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if req, ok := err.(*outbound.BadRequestError); ok {
|
||||
return BadAuthRequest(req.Message)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"infinite-canvas/backend/internal/protocol"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
240
backend/internal/app/platform_bridge.go
Normal file
240
backend/internal/app/platform_bridge.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"infinite-canvas/backend/internal/kernel"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/platform"
|
||||
)
|
||||
|
||||
type (
|
||||
RuntimeResourcePolicy = platform.RuntimeResourcePolicy
|
||||
RuntimeTaskPolicy = platform.RuntimeTaskPolicy
|
||||
RuntimeRequestPolicy = platform.RuntimeRequestPolicy
|
||||
RuntimePolicySetting = platform.RuntimePolicySetting
|
||||
PublicRuntimePolicySetting = platform.PublicRuntimePolicySetting
|
||||
PublicRuntimeLimits = platform.PublicRuntimeLimits
|
||||
FeatureAvailability = platform.FeatureAvailability
|
||||
PublicFeatureAvailability = platform.PublicFeatureAvailability
|
||||
)
|
||||
|
||||
const (
|
||||
FeatureShortDrama = platform.FeatureShortDrama
|
||||
FeatureTaskCenter = platform.FeatureTaskCenter
|
||||
FeatureCredits = platform.FeatureCredits
|
||||
FeatureCustomChannels = platform.FeatureCustomChannels
|
||||
FeatureFrontendModels = platform.FeatureFrontendModels
|
||||
FeaturePluginCenter = platform.FeaturePluginCenter
|
||||
FeatureSystemPlugins = platform.FeatureSystemPlugins
|
||||
FeatureTimelineTranscription = platform.FeatureTimelineTranscription
|
||||
)
|
||||
|
||||
type platformHost struct {
|
||||
svc *Service
|
||||
}
|
||||
|
||||
func (h platformHost) RequireAdmin(user *model.User) error {
|
||||
if h.svc == nil {
|
||||
return nil
|
||||
}
|
||||
return h.svc.RequireAdmin(user)
|
||||
}
|
||||
|
||||
func (h platformHost) AppendAudit(actor *model.User, action, targetType, targetID, summary string, metadata any) error {
|
||||
if h.svc == nil {
|
||||
return nil
|
||||
}
|
||||
return h.svc.appendAdminAudit(actor, action, targetType, targetID, summary, metadata)
|
||||
}
|
||||
|
||||
func (h platformHost) DesktopLocalChannelsEnabled() bool {
|
||||
if h.svc == nil {
|
||||
return false
|
||||
}
|
||||
return h.svc.DesktopLocalChannelsEnabled()
|
||||
}
|
||||
|
||||
func (h platformHost) ChannelConcurrencyLimit(channelID string) (int, error) {
|
||||
if h.svc == nil {
|
||||
return 0, nil
|
||||
}
|
||||
channel, err := h.svc.repo.SystemChannel(channelID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return channel.ConcurrencyLimit, nil
|
||||
}
|
||||
|
||||
func (s *Service) platformDomain() *platform.Service {
|
||||
if s == nil {
|
||||
return platform.New(nil, nil, nil)
|
||||
}
|
||||
if s.platform != nil {
|
||||
return s.platform
|
||||
}
|
||||
// Tests and small embedded callers may construct Service literals instead
|
||||
// of using New. Cache the lazily-created domain so its bounded read caches
|
||||
// retain their intended lifetime and do not turn every read into a DB hit.
|
||||
s.workerRuntimeMu.Lock()
|
||||
defer s.workerRuntimeMu.Unlock()
|
||||
if s.platform == nil {
|
||||
s.platform = platform.New(s.repo, s.coordinator, platformHost{svc: s})
|
||||
}
|
||||
return s.platform
|
||||
}
|
||||
|
||||
func (s *Service) RuntimePolicy() (RuntimePolicySetting, error) {
|
||||
return s.platformDomain().RuntimePolicy()
|
||||
}
|
||||
|
||||
func (s *Service) runtimeConcurrencySetting() (RuntimeTaskPolicy, error) {
|
||||
return s.platformDomain().RuntimeConcurrencySetting()
|
||||
}
|
||||
|
||||
func (s *Service) PublicRuntimeLimits() (*PublicRuntimeLimits, error) {
|
||||
return s.platformDomain().PublicRuntimeLimits()
|
||||
}
|
||||
|
||||
func (s *Service) AdminRuntimePolicySetting(actor *model.User) (*PublicRuntimePolicySetting, error) {
|
||||
return s.platformDomain().AdminRuntimePolicySetting(actor)
|
||||
}
|
||||
|
||||
func (s *Service) AdminSelfUseRuntimePolicy(actor *model.User) (*PublicRuntimePolicySetting, error) {
|
||||
return s.platformDomain().AdminSelfUseRuntimePolicy(actor)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateRuntimePolicySetting(actor *model.User, value RuntimePolicySetting) (*PublicRuntimePolicySetting, error) {
|
||||
return s.platformDomain().UpdateRuntimePolicySetting(actor, value)
|
||||
}
|
||||
|
||||
func (s *Service) ResetRuntimePolicySetting(actor *model.User) (*PublicRuntimePolicySetting, error) {
|
||||
return s.platformDomain().ResetRuntimePolicySetting(actor)
|
||||
}
|
||||
|
||||
func (s *Service) FeatureAvailability() (*PublicFeatureAvailability, error) {
|
||||
return s.platformDomain().FeatureAvailability()
|
||||
}
|
||||
|
||||
func (s *Service) AdminFeatureAvailability(actor *model.User) (*PublicFeatureAvailability, error) {
|
||||
return s.platformDomain().AdminFeatureAvailability(actor)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateFeatureAvailability(actor *model.User, value FeatureAvailability) (*PublicFeatureAvailability, error) {
|
||||
return s.platformDomain().UpdateFeatureAvailability(actor, value)
|
||||
}
|
||||
|
||||
func (s *Service) FeatureEnabled(feature string) (bool, error) {
|
||||
return s.platformDomain().FeatureEnabled(feature)
|
||||
}
|
||||
|
||||
func (s *Service) RequireFeature(feature string) error {
|
||||
return s.platformDomain().RequireFeature(feature)
|
||||
}
|
||||
|
||||
func (s *Service) AcquireChannelSlot(ctx context.Context, channelID string, fallbackScope string, ttl time.Duration) (func(), int, error) {
|
||||
return s.platformDomain().AcquireChannelSlot(ctx, channelID, fallbackScope, ttl)
|
||||
}
|
||||
|
||||
func (s *Service) AllowRequest(ctx context.Context, key string, limit int, window time.Duration) (bool, error) {
|
||||
return s.platformDomain().AllowRequest(ctx, key, limit, window)
|
||||
}
|
||||
|
||||
func (s *Service) RequestRetryAfter(ctx context.Context, key string, window time.Duration) time.Duration {
|
||||
return s.platformDomain().RequestRetryAfter(ctx, key, window)
|
||||
}
|
||||
|
||||
func (s *Service) AcquireCustomRelaySlot(ctx context.Context, userID string, limit int, ttl time.Duration) (func(), bool, error) {
|
||||
return s.platformDomain().AcquireCustomRelaySlot(ctx, userID, limit, ttl)
|
||||
}
|
||||
|
||||
func (s *Service) RecordChannelResult(ctx context.Context, channelID string, failed bool) error {
|
||||
return s.platformDomain().RecordChannelResult(ctx, channelID, failed)
|
||||
}
|
||||
|
||||
func (s *Service) Close() error {
|
||||
if s == nil || s.coordinator == nil {
|
||||
return nil
|
||||
}
|
||||
if !s.coordinator.HasRedis() {
|
||||
return nil
|
||||
}
|
||||
return s.coordinator.Redis().Close()
|
||||
}
|
||||
|
||||
func ChannelSlotFailureDetails(err error) (string, string) {
|
||||
return platform.ChannelSlotFailureDetails(err)
|
||||
}
|
||||
|
||||
func SanitizeAPICallPayload(data []byte, contentType string) string {
|
||||
return platform.SanitizeAPICallPayload(data, contentType)
|
||||
}
|
||||
|
||||
func optionalJSONString(payload map[string]any, key string) (string, error) {
|
||||
return platform.OptionalJSONString(payload, key)
|
||||
}
|
||||
|
||||
func requireJSONString(payload map[string]any, key string) (string, error) {
|
||||
return platform.RequireJSONString(payload, key)
|
||||
}
|
||||
|
||||
func firstJSONString(payload map[string]any, keys ...string) (string, error) {
|
||||
return platform.FirstJSONString(payload, keys...)
|
||||
}
|
||||
|
||||
func megabytes(value int64) int64 { return kernel.Megabytes(value) }
|
||||
func gigabytes(value int64) int64 { return kernel.Gigabytes(value) }
|
||||
|
||||
func requestPayloadForLog(req *http.Request) string {
|
||||
return platform.RequestPayloadForLog(req)
|
||||
}
|
||||
|
||||
func (s *Service) ValidateRuntime() error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
if s.pluginRuntimeErr != nil {
|
||||
return s.pluginRuntimeErr
|
||||
}
|
||||
return s.runtimeErr
|
||||
}
|
||||
|
||||
const (
|
||||
featureAvailabilitySettingKey = "feature_availability"
|
||||
runtimePolicySettingKey = "runtime_policy"
|
||||
)
|
||||
|
||||
func defaultFeatureAvailability() FeatureAvailability {
|
||||
return platform.DefaultFeatureAvailability()
|
||||
}
|
||||
|
||||
func publicFeatureAvailability(setting *model.SystemSetting, value FeatureAvailability) *PublicFeatureAvailability {
|
||||
return platform.ProjectFeatureAvailability(setting, value)
|
||||
}
|
||||
|
||||
func (s *Service) withRuntimeCapabilities(result *PublicFeatureAvailability) *PublicFeatureAvailability {
|
||||
if result != nil {
|
||||
result.DesktopLocalChannelsEnabled = s.DesktopLocalChannelsEnabled()
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func selfUseRuntimePolicy() RuntimePolicySetting { return platform.SelfUseRuntimePolicy() }
|
||||
func validateRuntimePolicy(value RuntimePolicySetting) error {
|
||||
return platform.ValidateRuntimePolicy(value)
|
||||
}
|
||||
|
||||
// Keep package-local test and legacy helpers source-compatible while the
|
||||
// runtime policy implementation lives in internal/platform.
|
||||
func defaultRuntimePolicy() RuntimePolicySetting {
|
||||
return platform.DefaultRuntimePolicy()
|
||||
}
|
||||
|
||||
const (
|
||||
minChannelConcurrencyLimit = platform.MinChannelConcurrencyLimit
|
||||
maxChannelConcurrencyLimit = platform.MaxChannelConcurrencyLimit
|
||||
runtimeCoordinationTimeout = platform.CoordinationTimeout
|
||||
routeCatalogVersionKey = platform.RouteCatalogVersionKey
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import "infinite-canvas/backend/internal/model"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"infinite-canvas/backend/internal/model"
|
||||
@@ -1,4 +1,4 @@
|
||||
package service
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user