mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
236 lines
7.0 KiB
Go
236 lines
7.0 KiB
Go
package automation
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"ant-chrome/backend/internal/config"
|
||
)
|
||
|
||
type resolvedNodeRuntime struct {
|
||
Source string
|
||
Path string
|
||
Version string
|
||
SystemNodeDetected bool
|
||
SystemNodePath string
|
||
Resolution string
|
||
SystemNodeError string
|
||
}
|
||
|
||
type nodeProbeResult struct {
|
||
Path string `json:"path"`
|
||
Version string `json:"version"`
|
||
}
|
||
|
||
type SystemNodeProbeResult struct {
|
||
OK bool `json:"ok"`
|
||
Path string `json:"path"`
|
||
Version string `json:"version"`
|
||
}
|
||
|
||
func (m *Manager) resolveNodeRuntime(runtimeDir string, auto config.AutomationConfig) resolvedNodeRuntime {
|
||
mode := config.DefaultAutomationNodeSource
|
||
if auto.NodeSource != "" {
|
||
mode = strings.TrimSpace(auto.NodeSource)
|
||
}
|
||
|
||
if mode != config.AutomationNodeSourceBundled {
|
||
if systemNode, err := m.resolveSystemNode(context.Background(), auto.SystemNodePath); err == nil {
|
||
return systemNode
|
||
} else if mode == config.AutomationNodeSourceSystem {
|
||
return resolvedNodeRuntime{
|
||
Source: config.AutomationNodeSourceSystem,
|
||
Version: strings.TrimSpace(auto.NodeVersion),
|
||
SystemNodePath: strings.TrimSpace(auto.SystemNodePath),
|
||
Resolution: "已配置为 system,必须使用系统 Node",
|
||
SystemNodeError: err.Error(),
|
||
}
|
||
} else {
|
||
return resolvedNodeRuntime{
|
||
Source: config.AutomationNodeSourceBundled,
|
||
Path: m.nodeExecutablePath(runtimeDir),
|
||
Version: strings.TrimSpace(auto.NodeVersion),
|
||
SystemNodePath: strings.TrimSpace(auto.SystemNodePath),
|
||
Resolution: "系统 Node 不可用,已回退到内建 Node",
|
||
SystemNodeError: err.Error(),
|
||
}
|
||
}
|
||
}
|
||
|
||
return resolvedNodeRuntime{
|
||
Source: config.AutomationNodeSourceBundled,
|
||
Path: m.nodeExecutablePath(runtimeDir),
|
||
Version: strings.TrimSpace(auto.NodeVersion),
|
||
Resolution: "已配置为 bundled,始终使用内建 Node",
|
||
}
|
||
}
|
||
|
||
func (m *Manager) resolveSystemNode(ctx context.Context, explicitPath string) (resolvedNodeRuntime, error) {
|
||
type nodeCandidate struct {
|
||
path string
|
||
resolution string
|
||
}
|
||
|
||
candidatePaths := make([]nodeCandidate, 0, 2)
|
||
if trimmed := strings.TrimSpace(explicitPath); trimmed != "" {
|
||
candidatePaths = append(candidatePaths, nodeCandidate{
|
||
path: trimmed,
|
||
resolution: "已使用配置的系统 Node 路径",
|
||
})
|
||
}
|
||
if lookupPath, err := exec.LookPath("node"); err == nil && strings.TrimSpace(lookupPath) != "" {
|
||
lookupPath = strings.TrimSpace(lookupPath)
|
||
duplicate := false
|
||
for _, existing := range candidatePaths {
|
||
if strings.EqualFold(existing.path, lookupPath) {
|
||
duplicate = true
|
||
break
|
||
}
|
||
}
|
||
if !duplicate {
|
||
candidatePaths = append(candidatePaths, nodeCandidate{
|
||
path: lookupPath,
|
||
resolution: "已使用 PATH 中的系统 Node",
|
||
})
|
||
}
|
||
}
|
||
|
||
var lastErr error
|
||
for _, candidate := range candidatePaths {
|
||
probe, err := m.probeNodeExecutable(ctx, candidate.path)
|
||
if err != nil {
|
||
lastErr = err
|
||
continue
|
||
}
|
||
return resolvedNodeRuntime{
|
||
Source: config.AutomationNodeSourceSystem,
|
||
Path: probe.Path,
|
||
Version: probe.Version,
|
||
SystemNodeDetected: true,
|
||
SystemNodePath: probe.Path,
|
||
Resolution: candidate.resolution,
|
||
}, nil
|
||
}
|
||
|
||
if lastErr != nil {
|
||
return resolvedNodeRuntime{}, lastErr
|
||
}
|
||
return resolvedNodeRuntime{}, fmt.Errorf("未找到系统 Node")
|
||
}
|
||
|
||
func (m *Manager) probeNodeExecutable(ctx context.Context, nodePath string) (nodeProbeResult, error) {
|
||
nodePath = strings.TrimSpace(nodePath)
|
||
if nodePath == "" {
|
||
return nodeProbeResult{}, fmt.Errorf("Node 路径为空")
|
||
}
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
probeCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||
defer cancel()
|
||
|
||
script := `process.stdout.write(JSON.stringify({path: process.execPath, version: process.versions.node}));`
|
||
cmd := exec.CommandContext(probeCtx, nodePath, "-e", script)
|
||
hideWindow(cmd)
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
message := strings.TrimSpace(string(output))
|
||
if message == "" {
|
||
message = err.Error()
|
||
}
|
||
return nodeProbeResult{}, fmt.Errorf("检测 Node 可执行文件失败(%s): %s", nodePath, message)
|
||
}
|
||
|
||
var probe nodeProbeResult
|
||
if err := json.Unmarshal(output, &probe); err != nil {
|
||
return nodeProbeResult{}, fmt.Errorf("解析 Node 探测结果失败: %w", err)
|
||
}
|
||
probe.Path = strings.TrimSpace(probe.Path)
|
||
probe.Version = strings.TrimSpace(probe.Version)
|
||
if probe.Path == "" {
|
||
probe.Path = nodePath
|
||
}
|
||
if probe.Version == "" {
|
||
return nodeProbeResult{}, fmt.Errorf("Node 版本为空")
|
||
}
|
||
if absPath, err := filepath.Abs(probe.Path); err == nil {
|
||
probe.Path = absPath
|
||
}
|
||
return probe, nil
|
||
}
|
||
|
||
func (m *Manager) ProbeSystemNode(ctx context.Context, explicitPath string) (SystemNodeProbeResult, error) {
|
||
resolved, err := m.resolveSystemNode(ctx, explicitPath)
|
||
if err != nil {
|
||
return SystemNodeProbeResult{}, err
|
||
}
|
||
return SystemNodeProbeResult{
|
||
OK: true,
|
||
Path: strings.TrimSpace(resolved.Path),
|
||
Version: strings.TrimSpace(resolved.Version),
|
||
}, nil
|
||
}
|
||
|
||
func (m *Manager) verifyNodeWithPlaywright(ctx context.Context, nodePath, runtimeDir string) (RuntimeCheckResult, error) {
|
||
nodePath = strings.TrimSpace(nodePath)
|
||
runtimeDir = strings.TrimSpace(runtimeDir)
|
||
if nodePath == "" {
|
||
return RuntimeCheckResult{}, fmt.Errorf("node path is empty")
|
||
}
|
||
if runtimeDir == "" {
|
||
return RuntimeCheckResult{}, fmt.Errorf("runtime dir is empty")
|
||
}
|
||
if ctx == nil {
|
||
ctx = context.Background()
|
||
}
|
||
checkCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||
defer cancel()
|
||
|
||
script := `
|
||
const path = require('path');
|
||
const pkg = require(path.join(process.argv[1], 'node_modules', 'playwright-core', 'package.json'));
|
||
const playwright = require(path.join(process.argv[1], 'node_modules', 'playwright-core'));
|
||
process.stdout.write(JSON.stringify({
|
||
nodeVersion: process.versions.node,
|
||
playwrightVersion: pkg.version,
|
||
hasChromium: !!playwright.chromium
|
||
}));
|
||
`
|
||
|
||
cmd := exec.CommandContext(checkCtx, nodePath, "-e", script, runtimeDir)
|
||
cmd.Dir = runtimeDir
|
||
hideWindow(cmd)
|
||
output, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
message := strings.TrimSpace(string(output))
|
||
if message == "" {
|
||
message = err.Error()
|
||
}
|
||
return RuntimeCheckResult{}, fmt.Errorf("playwright probe failed: %s", message)
|
||
}
|
||
|
||
var payload struct {
|
||
NodeVersion string `json:"nodeVersion"`
|
||
PlaywrightVersion string `json:"playwrightVersion"`
|
||
HasChromium bool `json:"hasChromium"`
|
||
}
|
||
if err := json.Unmarshal(output, &payload); err != nil {
|
||
return RuntimeCheckResult{}, fmt.Errorf("parse playwright probe result failed: %w", err)
|
||
}
|
||
|
||
result := RuntimeCheckResult{
|
||
OK: strings.TrimSpace(payload.NodeVersion) != "" && strings.TrimSpace(payload.PlaywrightVersion) != "" && payload.HasChromium,
|
||
NodeVersion: strings.TrimSpace(payload.NodeVersion),
|
||
PlaywrightVersion: strings.TrimSpace(payload.PlaywrightVersion),
|
||
}
|
||
if !result.OK {
|
||
return RuntimeCheckResult{}, fmt.Errorf("playwright probe returned incomplete result")
|
||
}
|
||
return result, nil
|
||
}
|