Files
Ant-Browser/backend/internal/launchcode/automation_api.go
T
2026-05-09 17:30:16 +08:00

398 lines
11 KiB
Go

package launchcode
import (
"encoding/json"
"io"
"net/http"
"os"
"strconv"
"strings"
"ant-chrome/backend/internal/automation"
)
type automationScriptRunAPIRequest struct {
ScriptID string `json:"scriptId"`
Selector json.RawMessage `json:"selector"`
Params json.RawMessage `json:"params"`
UseScriptSelector *bool `json:"useScriptSelector"`
UseScriptParams *bool `json:"useScriptParams"`
TimeoutMs int `json:"timeoutMs"`
}
type automationScriptSummary struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
Status string `json:"status"`
EntryFile string `json:"entryFile"`
Tags []string `json:"tags"`
Selector map[string]interface{} `json:"selector"`
Params map[string]interface{} `json:"params"`
Notes string `json:"notes"`
TargetConfig automation.ScriptTargetConfig `json:"targetConfig"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}
type automationScriptDetail struct {
automationScriptSummary
PackageFormat string `json:"packageFormat"`
ManifestVersion int `json:"manifestVersion"`
Source automation.ScriptSource `json:"source"`
}
func (s *LaunchServer) handleAutomationScripts(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]interface{}{
"ok": false,
"error": "method not allowed",
})
return
}
lister, ok := s.starter.(AutomationScriptLister)
if !ok {
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
"ok": false,
"error": "automation script api is unavailable",
})
return
}
items, err := lister.AutomationScriptList()
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
result := make([]automationScriptSummary, 0, len(items))
for _, item := range items {
result = append(result, summarizeAutomationScript(item))
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"count": len(result),
"items": result,
})
}
func (s *LaunchServer) handleAutomationScriptByID(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]interface{}{
"ok": false,
"error": "method not allowed",
})
return
}
scriptID, ok := parseAutomationScriptPathID(r.URL.Path)
if !ok {
writeJSON(w, http.StatusNotFound, map[string]interface{}{
"ok": false,
"error": "script not found",
})
return
}
getter, ok := s.starter.(AutomationScriptGetter)
if !ok {
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
"ok": false,
"error": "automation script api is unavailable",
})
return
}
item, err := getter.AutomationScriptGet(scriptID)
if err != nil {
message := strings.TrimSpace(err.Error())
if os.IsNotExist(err) {
writeJSON(w, http.StatusNotFound, map[string]interface{}{
"ok": false,
"error": "script not found",
})
return
}
if strings.Contains(strings.ToLower(message), "script id is invalid") || strings.Contains(strings.ToLower(message), "script id is required") {
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
"ok": false,
"error": message,
})
return
}
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": message,
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"item": detailAutomationScript(*item),
})
}
func (s *LaunchServer) handleAutomationScriptRun(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeJSON(w, http.StatusMethodNotAllowed, map[string]interface{}{
"ok": false,
"error": "method not allowed",
})
return
}
runner, ok := s.starter.(AutomationScriptRunner)
if !ok {
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
"ok": false,
"error": "automation script api is unavailable",
})
return
}
var req automationScriptRunAPIRequest
dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
"ok": false,
"error": "invalid request body",
})
return
}
input, err := normalizeAutomationRunRequest(req)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
run, err := runner.AutomationScriptRunWithOptions(input)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"run": run,
})
}
func (s *LaunchServer) handleAutomationScriptRuns(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
writeJSON(w, http.StatusMethodNotAllowed, map[string]interface{}{
"ok": false,
"error": "method not allowed",
})
return
}
lister, ok := s.starter.(AutomationScriptRunLister)
if !ok {
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
"ok": false,
"error": "automation script api is unavailable",
})
return
}
limit := 20
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
if n, err := strconv.Atoi(raw); err == nil {
if n < 1 {
n = 1
}
if n > 200 {
n = 200
}
limit = n
}
}
items, err := lister.AutomationScriptRunList(limit)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"count": len(items),
"limit": limit,
"items": items,
})
}
func summarizeAutomationScript(record automation.ScriptRecord) automationScriptSummary {
return automationScriptSummary{
ID: strings.TrimSpace(record.ID),
Name: strings.TrimSpace(record.Name),
Description: strings.TrimSpace(record.Description),
Type: strings.TrimSpace(record.Type),
Status: strings.TrimSpace(record.Status),
EntryFile: strings.TrimSpace(record.EntryFile),
Tags: append([]string(nil), record.Tags...),
Selector: parseJSONObjectText(record.SelectorText),
Params: parseJSONObjectText(record.ParamsText),
Notes: strings.TrimSpace(record.Notes),
TargetConfig: record.TargetConfig,
CreatedAt: strings.TrimSpace(record.CreatedAt),
UpdatedAt: strings.TrimSpace(record.UpdatedAt),
}
}
func detailAutomationScript(record automation.ScriptRecord) automationScriptDetail {
return automationScriptDetail{
automationScriptSummary: summarizeAutomationScript(record),
PackageFormat: strings.TrimSpace(record.PackageFormat),
ManifestVersion: record.ManifestVersion,
Source: record.Source,
}
}
func parseAutomationScriptPathID(path string) (string, bool) {
path = strings.TrimPrefix(path, "/api/automation/scripts/")
path = strings.Trim(path, "/")
path = strings.TrimSpace(path)
if path == "" || strings.Contains(path, "/") {
return "", false
}
return path, true
}
func normalizeAutomationRunRequest(req automationScriptRunAPIRequest) (automation.ScriptRunRequest, error) {
scriptID := strings.TrimSpace(req.ScriptID)
if scriptID == "" {
return automation.ScriptRunRequest{}, badAutomationRequest("scriptId is required")
}
selector, hasSelector, err := decodeJSONObjectRaw(req.Selector, "selector")
if err != nil {
return automation.ScriptRunRequest{}, err
}
params, hasParams, err := decodeJSONObjectRaw(req.Params, "params")
if err != nil {
return automation.ScriptRunRequest{}, err
}
useScriptSelector, err := resolveUseScriptField("selector", req.UseScriptSelector, hasSelector)
if err != nil {
return automation.ScriptRunRequest{}, err
}
useScriptParams, err := resolveUseScriptField("params", req.UseScriptParams, hasParams)
if err != nil {
return automation.ScriptRunRequest{}, err
}
selectorText := ""
if !useScriptSelector {
encodedSelector, err := json.Marshal(selector)
if err != nil {
return automation.ScriptRunRequest{}, badAutomationRequest("selector must be a JSON object")
}
selectorText = string(encodedSelector)
}
paramsText := ""
if !useScriptParams {
encodedParams, err := json.Marshal(params)
if err != nil {
return automation.ScriptRunRequest{}, badAutomationRequest("params must be a JSON object")
}
paramsText = string(encodedParams)
}
return automation.ScriptRunRequest{
ScriptID: scriptID,
SelectorText: selectorText,
ParamsText: paramsText,
UseScriptSelector: useScriptSelector,
UseScriptParams: useScriptParams,
TimeoutMs: req.TimeoutMs,
}, nil
}
func resolveUseScriptField(name string, explicit *bool, hasObject bool) (bool, error) {
if explicit == nil {
return !hasObject, nil
}
if *explicit && hasObject {
return false, badAutomationRequest(name + " conflicts with useScript" + upperFirst(name) + "=true")
}
if !*explicit && !hasObject {
return false, badAutomationRequest(name + " is required when useScript" + upperFirst(name) + "=false")
}
return *explicit, nil
}
func decodeJSONObjectRaw(raw json.RawMessage, fieldName string) (map[string]interface{}, bool, error) {
trimmed := strings.TrimSpace(string(raw))
if trimmed == "" || trimmed == "null" {
return nil, false, nil
}
var value interface{}
if err := json.Unmarshal(raw, &value); err != nil {
return nil, false, badAutomationRequest(fieldName + " must be a JSON object")
}
obj, ok := value.(map[string]interface{})
if !ok {
return nil, false, badAutomationRequest(fieldName + " must be a JSON object")
}
return obj, true, nil
}
func parseJSONObjectText(text string) map[string]interface{} {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
return nil
}
var value interface{}
if err := json.Unmarshal([]byte(trimmed), &value); err != nil {
return nil
}
obj, ok := value.(map[string]interface{})
if !ok {
return nil
}
return obj
}
func upperFirst(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
return strings.ToUpper(value[:1]) + value[1:]
}
func badAutomationRequest(message string) error {
return automationRequestError(strings.TrimSpace(message))
}
type automationRequestError string
func (e automationRequestError) Error() string {
return strings.TrimSpace(string(e))
}