feat(platform): harden telemetry pipeline and unify Semi UI workspaces

This commit is contained in:
lingniu
2026-07-18 00:26:36 +08:00
parent 65b4e4f055
commit 159c80b0ae
136 changed files with 21616 additions and 1785 deletions

View File

@@ -0,0 +1,209 @@
package feichibridge
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
const feichiPublicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDOwmFtEk1oJxDU0NI4kVO0Jx0X
nt+Abx+JKGUzcRzPwjUkd5z9Ice5rh87CCmj0XjZ5pPac6TtA3f0v5FqiK/kjQY5
XMLti4weJ4dcp1/q1O7PCYxRX8WgetGxwsjxGn+uoZOZkclN1PFS4wRnKEso6+G/
e60QGB29cZoo4jZnZwIDAQAB
-----END PUBLIC KEY-----`
type LoginCredentials struct {
Username string
Password string
MaxAttempts int
RetryDelay time.Duration
}
type loginSession struct {
credentials LoginCredentials
solver CaptchaSolver
mu sync.Mutex
ready atomic.Bool
csrf atomic.Value
token atomic.Value
}
type firstLoginEnvelope struct {
Code any `json:"code"`
}
type captchaPayload struct {
Src string `json:"src"`
}
type loginPayload struct {
Token string `json:"token"`
}
func newLoginSession(credentials LoginCredentials, solver CaptchaSolver) (*loginSession, error) {
credentials.Username = strings.TrimSpace(credentials.Username)
if credentials.Username == "" || credentials.Password == "" {
return nil, errors.New("Feichi username and password are required")
}
if solver == nil {
return nil, errors.New("captcha solver is required")
}
if credentials.MaxAttempts <= 0 {
credentials.MaxAttempts = 20
}
if credentials.RetryDelay <= 0 {
credentials.RetryDelay = time.Second
}
session := &loginSession{credentials: credentials, solver: solver}
session.csrf.Store("")
session.token.Store("")
return session, nil
}
func (s *loginSession) invalidate() {
s.ready.Store(false)
}
func (s *loginSession) cookieHeader() string {
var cookies []string
if csrf := s.csrf.Load().(string); csrf != "" {
cookies = append(cookies, "CSRF="+csrf)
}
if token := s.token.Load().(string); token != "" {
cookies = append(cookies, "R_SESS="+token)
}
return strings.Join(cookies, "; ")
}
func (s *loginSession) csrfHeader() string {
return s.csrf.Load().(string)
}
func (s *loginSession) ensure(ctx context.Context, client *APIClient) error {
if s.ready.Load() {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
if s.ready.Load() {
return nil
}
var lastErr error
for attempt := 1; attempt <= s.credentials.MaxAttempts; attempt++ {
if err := s.loginAttempt(ctx, client); err == nil {
s.ready.Store(true)
return nil
} else {
lastErr = err
}
if attempt < s.credentials.MaxAttempts {
timer := time.NewTimer(s.credentials.RetryDelay)
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
}
}
return fmt.Errorf("Feichi automatic login failed after %d attempts: %w", s.credentials.MaxAttempts, lastErr)
}
func (s *loginSession) loginAttempt(ctx context.Context, client *APIClient) error {
s.ready.Store(false)
s.token.Store("")
var first firstLoginEnvelope
headers, err := client.rawJSON(ctx, http.MethodGet, "api/v1/first-login", nil, &first)
if err != nil {
return fmt.Errorf("initialize login session: %w", err)
}
if !apiCodeOK(first.Code) {
return apiCodeError(first.Code)
}
csrf := strings.TrimSpace(headers.Get("x-api-csrf"))
if csrf == "" {
for _, rawCookie := range headers.Values("Set-Cookie") {
cookie, parseErr := http.ParseSetCookie(rawCookie)
if parseErr == nil && cookie.Name == "CSRF" {
csrf = cookie.Value
break
}
}
}
if csrf == "" {
return errors.New("first-login response did not provide CSRF")
}
s.csrf.Store(csrf)
var captcha objectEnvelope[captchaPayload]
if _, err := client.rawJSON(ctx, http.MethodGet, "api/v1/login/randCode", nil, &captcha); err != nil {
return fmt.Errorf("request captcha: %w", err)
}
if !apiCodeOK(captcha.Code) {
return apiCodeError(captcha.Code)
}
encodedImage := captcha.Data.Src
if comma := strings.IndexByte(encodedImage, ','); comma >= 0 {
encodedImage = encodedImage[comma+1:]
}
image, err := base64.StdEncoding.DecodeString(encodedImage)
if err != nil {
return fmt.Errorf("decode captcha image: %w", err)
}
validCode, err := s.solver.Solve(ctx, image)
if err != nil {
return err
}
encrypted, err := encryptFeichiPassword(s.credentials.Password)
if err != nil {
return err
}
var login objectEnvelope[loginPayload]
if _, err := client.rawJSON(ctx, http.MethodPost, "api/v1/login", map[string]string{
"username": s.credentials.Username,
"password": encrypted,
"validCode": validCode,
}, &login); err != nil {
return fmt.Errorf("submit login: %w", err)
}
if !apiCodeOK(login.Code) {
return apiCodeError(login.Code)
}
token := strings.TrimSpace(login.Data.Token)
if token == "" {
return errors.New("login succeeded without R_SESS token")
}
s.token.Store(token)
return nil
}
func encryptFeichiPassword(password string) (string, error) {
block, _ := pem.Decode([]byte(feichiPublicKey))
if block == nil {
return "", errors.New("decode Feichi RSA public key")
}
parsed, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("parse Feichi RSA public key: %w", err)
}
publicKey, ok := parsed.(*rsa.PublicKey)
if !ok {
return "", errors.New("Feichi public key is not RSA")
}
encrypted, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(password))
if err != nil {
return "", fmt.Errorf("encrypt Feichi password: %w", err)
}
return base64.StdEncoding.EncodeToString(encrypted), nil
}