统一单入口 Go 工程与前端依赖目录,增加后台供应商配置、候选审核、计划预算、成交冲正、持仓复盘和站内通知。接入策略版本、Python 回测、可配置 A 股公开行情及 zap 文件轮转日志,补充迁移、契约与验证。
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/apperror"
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/model"
|
||||
"git.sundynix.cn/Blizzard/Veyra/internal/secret"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type ProviderRepository interface {
|
||||
Get(context.Context, string) (model.ProviderConfig, error)
|
||||
List(context.Context) ([]model.ProviderConfig, error)
|
||||
Save(context.Context, model.ProviderConfig, int64, string, string, bool) (model.ProviderConfig, error)
|
||||
Audit(context.Context) ([]model.AuditEvent, error)
|
||||
}
|
||||
type ProviderService struct {
|
||||
repo ProviderRepository
|
||||
vault *secret.Vault
|
||||
}
|
||||
|
||||
func NewProviderService(repo ProviderRepository, vault *secret.Vault) *ProviderService {
|
||||
return &ProviderService{repo: repo, vault: vault}
|
||||
}
|
||||
|
||||
type ProviderInput struct {
|
||||
QuoteBaseURL string `json:"quote_base_url"`
|
||||
MarketAdapter string `json:"market_adapter"`
|
||||
HistoryBaseURL string `json:"history_base_url"`
|
||||
Name string `json:"name"`
|
||||
BaseURL string `json:"base_url"`
|
||||
ModelName string `json:"model_name"`
|
||||
WireAPI string `json:"wire_api"`
|
||||
ModelCatalogURL string `json:"model_catalog_url"`
|
||||
DisableResponseStorage *bool `json:"disable_response_storage"`
|
||||
RequiresOpenAIAuth *bool `json:"requires_openai_auth"`
|
||||
Enabled bool `json:"enabled"`
|
||||
TimeoutSeconds int `json:"timeout_seconds"`
|
||||
APIKey *string `json:"api_key,omitempty"`
|
||||
ClearAPIKey bool `json:"clear_api_key"`
|
||||
ExpectedVersion int64 `json:"expected_version"`
|
||||
}
|
||||
|
||||
func providerKey(key string) bool {
|
||||
return key == "model_primary" || key == "model_backup" || key == "model_review" || key == "market_primary"
|
||||
}
|
||||
func (s *ProviderService) authorize(actor model.User) error {
|
||||
if actor.Role != "ADMIN" || actor.Status != "ACTIVE" {
|
||||
return apperror.ErrForbidden
|
||||
}
|
||||
if s == nil || s.repo == nil {
|
||||
return apperror.ErrUnavailable
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (s *ProviderService) List(ctx context.Context, actor model.User) ([]model.ProviderConfig, error) {
|
||||
if err := s.authorize(actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.List(ctx)
|
||||
}
|
||||
func (s *ProviderService) Audit(ctx context.Context, actor model.User) ([]model.AuditEvent, error) {
|
||||
if err := s.authorize(actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.Audit(ctx)
|
||||
}
|
||||
func (s *ProviderService) Save(ctx context.Context, actor model.User, key, requestID string, input ProviderInput) (model.ProviderConfig, error) {
|
||||
var c model.ProviderConfig
|
||||
if err := s.authorize(actor); err != nil {
|
||||
return c, err
|
||||
}
|
||||
if !providerKey(key) || input.ExpectedVersion < 0 {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
if _, err := uuid.Parse(requestID); err != nil {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
if input.MarketAdapter != "" && (key != "market_primary" || (input.MarketAdapter != "eastmoney_public" && input.MarketAdapter != "sina_public")) {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
if input.QuoteBaseURL != "" {
|
||||
u, err := url.Parse(input.QuoteBaseURL)
|
||||
if key != "market_primary" || err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || len(input.QuoteBaseURL) > 2048 {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
}
|
||||
if input.Enabled && input.MarketAdapter == "sina_public" && (input.HistoryBaseURL == "" || input.QuoteBaseURL == "") {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
if input.HistoryBaseURL != "" {
|
||||
u, err := url.Parse(input.HistoryBaseURL)
|
||||
if key != "market_primary" || err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || len(input.HistoryBaseURL) > 2048 {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
}
|
||||
if input.Enabled && input.MarketAdapter == "eastmoney_public" && input.HistoryBaseURL == "" {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
input.Name = strings.TrimSpace(input.Name)
|
||||
input.BaseURL = strings.TrimSpace(input.BaseURL)
|
||||
input.ModelName = strings.TrimSpace(input.ModelName)
|
||||
input.ModelCatalogURL = strings.TrimSpace(input.ModelCatalogURL)
|
||||
if input.WireAPI == "" {
|
||||
input.WireAPI = "responses"
|
||||
}
|
||||
if input.WireAPI != "responses" && input.WireAPI != "chat_completions" {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
if utf8.RuneCountInString(input.Name) > 80 || utf8.RuneCountInString(input.ModelName) > 120 || len(input.BaseURL) > 2048 || len(input.ModelCatalogURL) > 2048 || input.TimeoutSeconds < 1 || input.TimeoutSeconds > 30 {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
if input.BaseURL != "" {
|
||||
u, err := url.Parse(input.BaseURL)
|
||||
if err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Opaque != "" {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
// Credentials belong exclusively in the encrypted key field.
|
||||
if strings.ContainsAny(u.Path, "\r\n") {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
}
|
||||
if input.ModelCatalogURL != "" {
|
||||
catalog, err := url.Parse(input.ModelCatalogURL)
|
||||
base, baseErr := url.Parse(input.BaseURL)
|
||||
if err != nil || baseErr != nil || catalog.Scheme != "https" || catalog.Host == "" || catalog.User != nil || catalog.RawQuery != "" || catalog.Fragment != "" || catalog.Opaque != "" || !strings.EqualFold(catalog.Host, base.Host) || base.Scheme != "https" {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
}
|
||||
if input.APIKey != nil && (*input.APIKey == "" || len(*input.APIKey) > 4096 || strings.ContainsAny(*input.APIKey, "\r\n") || input.ClearAPIKey) {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
existing, err := s.repo.Get(ctx, key)
|
||||
if err != nil && !errors.Is(err, apperror.ErrNotFound) {
|
||||
return c, err
|
||||
}
|
||||
if existing.Version != input.ExpectedVersion {
|
||||
return c, apperror.ErrConflict
|
||||
}
|
||||
if existing.BaseURL != input.BaseURL && len(existing.EncryptedAPIKey) > 0 && input.APIKey == nil && !input.ClearAPIKey {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
c = model.ProviderConfig{QuoteBaseURL: input.QuoteBaseURL, MarketAdapter: input.MarketAdapter, HistoryBaseURL: input.HistoryBaseURL, ConfigKey: key, Name: input.Name, BaseURL: input.BaseURL, ModelName: input.ModelName, Enabled: input.Enabled, TimeoutSeconds: input.TimeoutSeconds, EncryptedAPIKey: existing.EncryptedAPIKey, LastTestStatus: "NOT_CONFIGURED", WireAPI: input.WireAPI, ModelCatalogURL: input.ModelCatalogURL, DisableResponseStorage: true, RequiresOpenAIAuth: true}
|
||||
if existing.Version > 0 {
|
||||
c.DisableResponseStorage = existing.DisableResponseStorage
|
||||
c.RequiresOpenAIAuth = existing.RequiresOpenAIAuth
|
||||
}
|
||||
if input.DisableResponseStorage != nil {
|
||||
c.DisableResponseStorage = *input.DisableResponseStorage
|
||||
}
|
||||
if input.RequiresOpenAIAuth != nil {
|
||||
c.RequiresOpenAIAuth = *input.RequiresOpenAIAuth
|
||||
}
|
||||
if input.ClearAPIKey {
|
||||
c.EncryptedAPIKey = nil
|
||||
}
|
||||
if input.APIKey != nil {
|
||||
c.EncryptedAPIKey, err = s.vault.Encrypt(*input.APIKey, key)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
}
|
||||
if c.Enabled {
|
||||
if c.Name == "" || c.BaseURL == "" || (key != "market_primary" && (c.ModelName == "" || c.RequiresOpenAIAuth && len(c.EncryptedAPIKey) == 0)) {
|
||||
return c, apperror.ErrInvalidInput
|
||||
}
|
||||
c.LastTestStatus = "NOT_TESTED"
|
||||
}
|
||||
return s.repo.Save(ctx, c, input.ExpectedVersion, actor.ID, requestID, input.APIKey != nil || input.ClearAPIKey)
|
||||
}
|
||||
Reference in New Issue
Block a user