mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
373 lines
10 KiB
Go
373 lines
10 KiB
Go
package automation
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
func normalizeScriptRecord(input ScriptRecord, existing ScriptRecord) (ScriptRecord, error) {
|
|
now := time.Now().Format(time.RFC3339)
|
|
|
|
id := strings.TrimSpace(input.ID)
|
|
if id == "" {
|
|
id = uuid.NewString()
|
|
}
|
|
if !isSafeScriptID(id) {
|
|
return ScriptRecord{}, fmt.Errorf("script id is invalid")
|
|
}
|
|
|
|
entryFile := normalizeScriptEntryFile(input.EntryFile)
|
|
recordType := normalizeScriptType(input.Type)
|
|
recordStatus := normalizeScriptStatus(input.Status)
|
|
packageFormat := normalizeScriptPackageFormat(firstNonEmpty(strings.TrimSpace(input.PackageFormat), strings.TrimSpace(existing.PackageFormat)))
|
|
manifestVersion := normalizeScriptManifestVersion(input.ManifestVersion, existing.ManifestVersion)
|
|
createdAt := firstNonEmpty(strings.TrimSpace(existing.CreatedAt), strings.TrimSpace(input.CreatedAt), now)
|
|
updatedAt := firstNonEmpty(strings.TrimSpace(input.UpdatedAt), now)
|
|
|
|
if strings.TrimSpace(input.Name) == "" {
|
|
return ScriptRecord{}, fmt.Errorf("script name is required")
|
|
}
|
|
|
|
return ScriptRecord{
|
|
PackageFormat: packageFormat,
|
|
ManifestVersion: manifestVersion,
|
|
ID: id,
|
|
Name: strings.TrimSpace(input.Name),
|
|
Description: strings.TrimSpace(input.Description),
|
|
Type: recordType,
|
|
Status: recordStatus,
|
|
EntryFile: entryFile,
|
|
Tags: normalizeScriptTags(input.Tags),
|
|
SelectorText: normalizeScriptJSONText(input.SelectorText),
|
|
ParamsText: normalizeScriptJSONText(input.ParamsText),
|
|
ScriptText: normalizeScriptText(input.ScriptText),
|
|
Notes: strings.TrimSpace(input.Notes),
|
|
TargetConfig: normalizeScriptTargetConfig(input.TargetConfig),
|
|
PublicAPI: normalizeScriptPublicAPIConfig(input.PublicAPI),
|
|
Source: normalizeScriptSource(input.Source, existing.Source),
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: updatedAt,
|
|
}, nil
|
|
}
|
|
|
|
func normalizeScriptPackageFormat(value string) string {
|
|
normalized := strings.TrimSpace(value)
|
|
if normalized == "" {
|
|
return defaultScriptPackageFormat
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func normalizeScriptManifestVersion(value int, fallback int) int {
|
|
if value > 0 {
|
|
return value
|
|
}
|
|
if fallback > 0 {
|
|
return fallback
|
|
}
|
|
return defaultScriptManifestVersion
|
|
}
|
|
|
|
func normalizeScriptType(value string) string {
|
|
switch strings.TrimSpace(value) {
|
|
case "launch-api":
|
|
return "launch-api"
|
|
default:
|
|
return "playwright-cdp"
|
|
}
|
|
}
|
|
|
|
func normalizeScriptStatus(value string) string {
|
|
switch strings.TrimSpace(value) {
|
|
case "ready":
|
|
return "ready"
|
|
case "disabled":
|
|
return "disabled"
|
|
default:
|
|
return "draft"
|
|
}
|
|
}
|
|
|
|
func normalizeScriptEntryFile(value string) string {
|
|
normalized := strings.TrimSpace(value)
|
|
if normalized == "" {
|
|
return defaultScriptEntryFile
|
|
}
|
|
normalized = filepath.ToSlash(filepath.Clean(normalized))
|
|
if normalized == "." || normalized == "/" || normalized == scriptStoreConfigFileName {
|
|
return defaultScriptEntryFile
|
|
}
|
|
if strings.HasPrefix(normalized, "../") || normalized == ".." || filepath.IsAbs(normalized) {
|
|
return defaultScriptEntryFile
|
|
}
|
|
return normalized
|
|
}
|
|
|
|
func normalizeBundleFilePath(value string) (string, error) {
|
|
normalized := filepath.ToSlash(filepath.Clean(strings.TrimSpace(value)))
|
|
if normalized == "." || normalized == "/" || normalized == "" {
|
|
return "", fmt.Errorf("bundle file path is invalid")
|
|
}
|
|
if strings.HasPrefix(normalized, "../") || normalized == ".." || filepath.IsAbs(normalized) {
|
|
return "", fmt.Errorf("bundle file path is invalid")
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func normalizeScriptTags(tags []string) []string {
|
|
deduped := make(map[string]struct{}, len(tags))
|
|
result := make([]string, 0, len(tags))
|
|
for _, tag := range tags {
|
|
normalized := strings.TrimSpace(tag)
|
|
if normalized == "" {
|
|
continue
|
|
}
|
|
if _, exists := deduped[normalized]; exists {
|
|
continue
|
|
}
|
|
deduped[normalized] = struct{}{}
|
|
result = append(result, normalized)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func normalizeScriptJSONText(value string) string {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func normalizeScriptText(value string) string {
|
|
return strings.ReplaceAll(value, "\r\n", "\n")
|
|
}
|
|
|
|
func normalizeScriptSource(input ScriptSource, existing ScriptSource) ScriptSource {
|
|
source := ScriptSource{
|
|
Type: firstNonEmpty(strings.TrimSpace(input.Type), strings.TrimSpace(existing.Type)),
|
|
URI: firstNonEmpty(strings.TrimSpace(input.URI), strings.TrimSpace(existing.URI)),
|
|
Ref: firstNonEmpty(strings.TrimSpace(input.Ref), strings.TrimSpace(existing.Ref)),
|
|
Path: firstNonEmpty(strings.TrimSpace(input.Path), strings.TrimSpace(existing.Path)),
|
|
ImportedAt: firstNonEmpty(strings.TrimSpace(input.ImportedAt), strings.TrimSpace(existing.ImportedAt)),
|
|
}
|
|
if source.Type == "" && (source.URI != "" || source.Ref != "" || source.Path != "" || source.ImportedAt != "") {
|
|
source.Type = "manual"
|
|
}
|
|
return source
|
|
}
|
|
|
|
func normalizeScriptTargetConfig(input ScriptTargetConfig) ScriptTargetConfig {
|
|
mode := normalizeScriptTargetMode(input.Mode)
|
|
createNameTemplate := strings.TrimSpace(input.CreateNameTemplate)
|
|
if createNameTemplate == "" && mode == "create" {
|
|
createNameTemplate = defaultScriptCreateNameTemplate
|
|
}
|
|
|
|
return ScriptTargetConfig{
|
|
Mode: mode,
|
|
Selector: normalizeScriptTargetSelector(input.Selector),
|
|
TemplateSelector: normalizeScriptTargetSelector(input.TemplateSelector),
|
|
CreateNameTemplate: createNameTemplate,
|
|
}
|
|
}
|
|
|
|
func normalizeScriptTargetMode(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "existing":
|
|
return "existing"
|
|
case "create":
|
|
return "create"
|
|
case "rotate":
|
|
return "rotate"
|
|
default:
|
|
return "manual"
|
|
}
|
|
}
|
|
|
|
func normalizeScriptTargetSelector(input ScriptTargetSelector) ScriptTargetSelector {
|
|
return ScriptTargetSelector{
|
|
Code: strings.ToUpper(strings.TrimSpace(input.Code)),
|
|
ProfileID: strings.TrimSpace(input.ProfileID),
|
|
ProfileName: strings.TrimSpace(input.ProfileName),
|
|
GroupID: strings.TrimSpace(input.GroupID),
|
|
Keywords: normalizeScriptTags(input.Keywords),
|
|
Tags: normalizeScriptTags(input.Tags),
|
|
}
|
|
}
|
|
|
|
func normalizeScriptPublicAPIConfig(input ScriptPublicAPIConfig) ScriptPublicAPIConfig {
|
|
return ScriptPublicAPIConfig{
|
|
Enabled: input.Enabled,
|
|
Method: normalizeScriptPublicAPIMethod(input.Method),
|
|
Path: normalizeScriptPublicAPIPath(input.Path),
|
|
RequestMode: normalizeScriptPublicAPIRequestMode(input.RequestMode),
|
|
ResponseMode: normalizeScriptPublicAPIResponseMode(input.ResponseMode),
|
|
TimeoutMs: normalizeScriptPublicAPITimeout(input.TimeoutMs),
|
|
RequestBodyText: normalizeScriptJSONText(input.RequestBodyText),
|
|
ResponseBodyText: normalizeScriptJSONText(input.ResponseBodyText),
|
|
Variables: normalizeScriptPublicAPIVariables(input.Variables),
|
|
}
|
|
}
|
|
|
|
func normalizeScriptPublicAPIVariables(variables []ScriptPublicAPIVariable) []ScriptPublicAPIVariable {
|
|
seen := make(map[string]struct{}, len(variables))
|
|
result := make([]ScriptPublicAPIVariable, 0, len(variables))
|
|
for _, variable := range variables {
|
|
name := strings.TrimSpace(variable.Name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[name]; exists {
|
|
continue
|
|
}
|
|
seen[name] = struct{}{}
|
|
result = append(result, ScriptPublicAPIVariable{
|
|
Name: name,
|
|
DefaultValue: strings.TrimSpace(variable.DefaultValue),
|
|
Description: strings.TrimSpace(variable.Description),
|
|
Required: variable.Required,
|
|
})
|
|
}
|
|
return result
|
|
}
|
|
|
|
func normalizeScriptPublicAPIMethod(value string) string {
|
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
|
case "POST":
|
|
return "POST"
|
|
default:
|
|
return "POST"
|
|
}
|
|
}
|
|
|
|
func normalizeScriptPublicAPIRequestMode(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "params-only":
|
|
return "params-only"
|
|
default:
|
|
return "standard"
|
|
}
|
|
}
|
|
|
|
func normalizeScriptPublicAPIResponseMode(value string) string {
|
|
switch strings.ToLower(strings.TrimSpace(value)) {
|
|
case "result-only":
|
|
return "result-only"
|
|
default:
|
|
return "envelope"
|
|
}
|
|
}
|
|
|
|
func normalizeScriptPublicAPITimeout(value int) int {
|
|
if value <= 0 {
|
|
return defaultScriptPublicAPITimeoutMs
|
|
}
|
|
if value < 1000 {
|
|
return 1000
|
|
}
|
|
if value > 30*60*1000 {
|
|
return 30 * 60 * 1000
|
|
}
|
|
return value
|
|
}
|
|
|
|
func normalizeScriptPublicAPIPath(value string) string {
|
|
normalized := strings.TrimSpace(value)
|
|
if normalized == "" {
|
|
return ""
|
|
}
|
|
|
|
normalized = strings.ReplaceAll(normalized, "\\", "/")
|
|
lower := strings.ToLower(normalized)
|
|
if strings.HasPrefix(lower, scriptPublicAPIBasePath+"/") {
|
|
normalized = normalized[len(scriptPublicAPIBasePath)+1:]
|
|
} else if strings.HasPrefix(lower, strings.TrimPrefix(scriptPublicAPIBasePath, "/")+"/") {
|
|
normalized = normalized[len(strings.TrimPrefix(scriptPublicAPIBasePath, "/"))+1:]
|
|
}
|
|
|
|
cleaned := path.Clean("/" + normalized)
|
|
cleaned = strings.Trim(cleaned, "/")
|
|
if cleaned == "" || cleaned == "." {
|
|
return ""
|
|
}
|
|
|
|
parts := strings.Split(cleaned, "/")
|
|
result := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
if normalizedPart := normalizeScriptPublicAPIPathSegment(part); normalizedPart != "" {
|
|
result = append(result, normalizedPart)
|
|
}
|
|
}
|
|
return strings.Join(result, "/")
|
|
}
|
|
|
|
func normalizeScriptPublicAPIPathSegment(value string) string {
|
|
var builder strings.Builder
|
|
lastDash := false
|
|
|
|
for _, ch := range strings.TrimSpace(value) {
|
|
switch {
|
|
case ch >= 'a' && ch <= 'z':
|
|
builder.WriteRune(ch)
|
|
lastDash = false
|
|
case ch >= 'A' && ch <= 'Z':
|
|
builder.WriteRune(ch + 32)
|
|
lastDash = false
|
|
case ch >= '0' && ch <= '9':
|
|
builder.WriteRune(ch)
|
|
lastDash = false
|
|
case ch == '-', ch == '_', ch == '.':
|
|
builder.WriteRune(ch)
|
|
lastDash = false
|
|
default:
|
|
if !lastDash {
|
|
builder.WriteByte('-')
|
|
lastDash = true
|
|
}
|
|
}
|
|
}
|
|
|
|
return strings.Trim(builder.String(), "-")
|
|
}
|
|
|
|
func scriptPublicAPIRoute(pathValue string) string {
|
|
pathValue = normalizeScriptPublicAPIPath(pathValue)
|
|
if pathValue == "" {
|
|
return scriptPublicAPIBasePath
|
|
}
|
|
return scriptPublicAPIBasePath + "/" + pathValue
|
|
}
|
|
|
|
func isSafeScriptID(value string) bool {
|
|
for _, ch := range value {
|
|
switch {
|
|
case ch >= 'a' && ch <= 'z':
|
|
case ch >= 'A' && ch <= 'Z':
|
|
case ch >= '0' && ch <= '9':
|
|
case ch == '-', ch == '_', ch == '.':
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func parseRFC3339OrZero(value string) time.Time {
|
|
ts, err := time.Parse(time.RFC3339, strings.TrimSpace(value))
|
|
if err != nil {
|
|
return time.Time{}
|
|
}
|
|
return ts
|
|
}
|
|
|
|
func firstNonEmpty(values ...string) string {
|
|
for _, value := range values {
|
|
if strings.TrimSpace(value) != "" {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
return ""
|
|
}
|