server: 平台抽象层+bilibili、服务端加密凭据库、角色中间件;web: 精简页面/路由、macOS 客户端(Swift)与多份方案文档;移除误入库的编译产物
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoginState is deliberately small: the web UI maps it to pending/scanned/
|
||||
// solved without knowing platform-specific numeric QR codes.
|
||||
type LoginState string
|
||||
|
||||
const (
|
||||
LoginPending LoginState = "pending"
|
||||
LoginScanned LoginState = "scanned"
|
||||
LoginConfirmed LoginState = "confirmed"
|
||||
)
|
||||
|
||||
type LoginSession struct {
|
||||
Token string
|
||||
URL string
|
||||
}
|
||||
|
||||
type LoginPoll struct {
|
||||
State LoginState
|
||||
Cookies string
|
||||
Prompt string
|
||||
}
|
||||
|
||||
type PublishInput struct {
|
||||
WorkspaceID uint64
|
||||
AccountID uint64
|
||||
TaskID uint64
|
||||
Title string
|
||||
Content string
|
||||
Tags []string
|
||||
MaterialURLs []string
|
||||
CategoryID int
|
||||
ScheduleAt *time.Time
|
||||
}
|
||||
|
||||
type PublishResult struct {
|
||||
URL string
|
||||
Receipt string
|
||||
}
|
||||
|
||||
type ErrorCategory string
|
||||
|
||||
const (
|
||||
AuthRequired ErrorCategory = "auth_required"
|
||||
ChallengeRequired ErrorCategory = "challenge_required"
|
||||
Validation ErrorCategory = "validation"
|
||||
RateLimited ErrorCategory = "rate_limited"
|
||||
Network ErrorCategory = "network"
|
||||
PlatformChanged ErrorCategory = "platform_changed"
|
||||
Unknown ErrorCategory = "unknown"
|
||||
)
|
||||
|
||||
// Error is safe to show in task history: it carries a category and excludes
|
||||
// cookies, tokens and raw response bodies by construction.
|
||||
type Error struct {
|
||||
Category ErrorCategory
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
if e.Message != "" {
|
||||
return fmt.Sprintf("%s: %s", e.Category, e.Message)
|
||||
}
|
||||
return string(e.Category)
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() error { return e.Err }
|
||||
|
||||
func CategoryOf(err error) ErrorCategory {
|
||||
var pe *Error
|
||||
if errors.As(err, &pe) && pe.Category != "" {
|
||||
return pe.Category
|
||||
}
|
||||
return Unknown
|
||||
}
|
||||
|
||||
// Adapter is the server-side boundary for a platform. Implementations own
|
||||
// cookies/profile details; API handlers only persist challenge state.
|
||||
type Adapter interface {
|
||||
Platform() string
|
||||
BeginLogin(context.Context) (LoginSession, error)
|
||||
PollLogin(context.Context, string) (LoginPoll, error)
|
||||
SaveCredentials(workspaceID, accountID uint64, cookies string) error
|
||||
Publish(context.Context, PublishInput) (PublishResult, error)
|
||||
}
|
||||
|
||||
// CredentialChecker is optional so a platform can expose a cheap login health
|
||||
// probe without forcing every adapter to implement a platform-specific API.
|
||||
type CredentialChecker interface {
|
||||
CheckCredentials(context.Context, uint64, uint64) error
|
||||
}
|
||||
|
||||
type Registry struct {
|
||||
mu sync.RWMutex
|
||||
adapters map[string]Adapter
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry { return &Registry{adapters: make(map[string]Adapter)} }
|
||||
|
||||
func (r *Registry) Register(adapter Adapter) error {
|
||||
if r == nil || adapter == nil || adapter.Platform() == "" {
|
||||
return errors.New("invalid platform adapter")
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.adapters[adapter.Platform()] = adapter
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Registry) Get(name string) Adapter {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.adapters[name]
|
||||
}
|
||||
|
||||
func (r *Registry) Has(name string) bool { return r.Get(name) != nil }
|
||||
|
||||
func (r *Registry) Names() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
names := make([]string, 0, len(r.adapters))
|
||||
for name := range r.adapters {
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
Reference in New Issue
Block a user