Files
dobotai-platform/internal/settings/settings.go
2026-08-14 12:35:33 +08:00

182 lines
4.8 KiB
Go

package settings
import (
"context"
"errors"
"net/url"
"path"
"strings"
)
type SystemState string
const (
StateUninitialized SystemState = "uninitialized"
StateCredentialsSaved SystemState = "credentials_saved"
StateDingTalkVerified SystemState = "dingtalk_verified"
StateWaitingAdmin SystemState = "waiting_first_admin"
StateActive SystemState = "active"
StateCredentialError SystemState = "credential_error"
)
type Snapshot struct {
ClientID string
SecretConfigured bool
CredentialsVerified bool
AdministratorCount int
}
func DetermineState(s Snapshot) SystemState {
id := strings.TrimSpace(s.ClientID) != ""
if !id && !s.SecretConfigured {
return StateUninitialized
}
if !id || !s.SecretConfigured {
return StateCredentialError
}
if !s.CredentialsVerified {
return StateCredentialsSaved
}
if s.AdministratorCount < 1 {
return StateWaitingAdmin
}
return StateActive
}
type Command struct {
Name string
Args []string
}
func ParseCommand(raw string) (Command, bool) {
fields := strings.Fields(strings.TrimSpace(raw))
if len(fields) == 0 {
return Command{}, false
}
name := strings.ToLower(fields[0])
switch name {
case "/setting", "/设置":
default:
return Command{}, false
}
return Command{Name: name, Args: fields[1:]}, true
}
type NotableLink struct{ BaseID, SheetID, ViewID string }
func ParseNotableLink(raw string) (NotableLink, error) {
if len(raw) > 4096 {
return NotableLink{}, errors.New("AI Table link is too long")
}
input := strings.TrimSpace(raw)
u, err := url.Parse(input)
if err != nil {
return NotableLink{}, errors.New("invalid AI Table link")
}
if u.Scheme == "" {
if u.Host != "" || u.User != nil || strings.Contains(input, "\\") {
return NotableLink{}, errors.New("invalid AI Table relative path")
}
relativePath := "/" + strings.TrimPrefix(u.Path, "/")
if !strings.HasPrefix(relativePath, "/i/nodes/") || path.Clean(relativePath) != relativePath {
return NotableLink{}, errors.New("invalid AI Table relative path")
}
u = &url.URL{Scheme: "https", Host: "alidocs.dingtalk.com", Path: relativePath, RawQuery: u.RawQuery}
}
if u.Scheme != "https" {
return NotableLink{}, errors.New("invalid AI Table link")
}
host := strings.ToLower(u.Hostname())
if host != "alidocs.dingtalk.com" && host != "docs.dingtalk.com" {
return NotableLink{}, errors.New("untrusted AI Table host")
}
base := path.Base(strings.TrimRight(u.Path, "/"))
q := u.Query()
sheetID, err := uniqueNotableQueryValue(q, "sheetId")
if err != nil {
return NotableLink{}, err
}
viewID, err := uniqueNotableQueryValue(q, "viewId")
if err != nil {
return NotableLink{}, err
}
iframeQuery, err := uniqueNotableQueryValue(q, "iframeQuery")
if err != nil {
return NotableLink{}, err
}
if iframeQuery != "" {
nested, parseErr := url.ParseQuery(iframeQuery)
if parseErr != nil {
return NotableLink{}, errors.New("invalid AI Table iframeQuery")
}
nestedSheetID, valueErr := uniqueNotableQueryValue(nested, "sheetId")
if valueErr != nil {
return NotableLink{}, valueErr
}
nestedViewID, valueErr := uniqueNotableQueryValue(nested, "viewId")
if valueErr != nil {
return NotableLink{}, valueErr
}
if sheetID != "" && nestedSheetID != "" && sheetID != nestedSheetID {
return NotableLink{}, errors.New("conflicting AI Table sheet IDs")
}
if viewID != "" && nestedViewID != "" && viewID != nestedViewID {
return NotableLink{}, errors.New("conflicting AI Table view IDs")
}
if sheetID == "" {
sheetID = nestedSheetID
}
if viewID == "" {
viewID = nestedViewID
}
}
result := NotableLink{BaseID: base, SheetID: sheetID, ViewID: viewID}
if result.BaseID == "" || result.BaseID == "." || result.BaseID == ".." || result.SheetID == "" {
return NotableLink{}, errors.New("AI Table link is missing base or sheet")
}
return result, nil
}
func uniqueNotableQueryValue(values url.Values, key string) (string, error) {
items := values[key]
if len(items) > 1 {
return "", errors.New("conflicting AI Table query values")
}
if len(items) == 0 {
return "", nil
}
return strings.TrimSpace(items[0]), nil
}
type Runtime interface {
Start(context.Context, string) error
Stop(context.Context) error
}
type Switcher struct {
runtime Runtime
active string
}
func NewSwitcher(r Runtime, active string) *Switcher { return &Switcher{runtime: r, active: active} }
func (s *Switcher) Switch(ctx context.Context, candidate string) error {
if s == nil || s.runtime == nil {
return errors.New("runtime is required")
}
candidate = strings.TrimSpace(candidate)
if candidate == "" {
return errors.New("candidate is required")
}
old := s.active
if err := s.runtime.Stop(ctx); err != nil {
return err
}
if err := s.runtime.Start(ctx, candidate); err != nil {
if old != "" {
_ = s.runtime.Start(ctx, old)
}
return err
}
s.active = candidate
return nil
}