mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
channel: master
version: 1.2.0
source-ref: D:\code\open_source\ant-chrome master
source-commit: f3d7ec5
merged-public-base: 3d264eb
293 lines
8.3 KiB
Go
293 lines
8.3 KiB
Go
package launchcode
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"ant-chrome/backend/internal/automation"
|
|
"ant-chrome/backend/internal/browser"
|
|
"ant-chrome/backend/internal/logger"
|
|
)
|
|
|
|
// BrowserStarter 浏览器启动接口(由 App 层实现并注入)
|
|
type BrowserStarter interface {
|
|
StartInstance(profileId string) (*browser.Profile, error)
|
|
}
|
|
|
|
// BrowserStatusProvider 可选接口:提供实例运行态查询。
|
|
type BrowserStatusProvider interface {
|
|
StatusInstance(profileId string) (*browser.Profile, error)
|
|
}
|
|
|
|
// LaunchRequestParams 支持外部自动化透传的一次性启动参数
|
|
type LaunchRequestParams struct {
|
|
LaunchArgs []string `json:"launchArgs"`
|
|
StartURLs []string `json:"startUrls"`
|
|
SkipDefaultStartURLs bool `json:"skipDefaultStartUrls"`
|
|
}
|
|
|
|
// LaunchRequest POST /api/launch 的请求体
|
|
type LaunchRequest struct {
|
|
Code string `json:"code"`
|
|
Key string `json:"key"`
|
|
ProfileID string `json:"profileId"`
|
|
ProfileName string `json:"profileName"`
|
|
Keyword string `json:"keyword"`
|
|
Keywords []string `json:"keywords"`
|
|
Tag string `json:"tag"`
|
|
Tags []string `json:"tags"`
|
|
GroupID string `json:"groupId"`
|
|
MatchMode string `json:"matchMode"`
|
|
Selector *LaunchSelector `json:"selector"`
|
|
LaunchRequestParams
|
|
}
|
|
|
|
// BrowserStarterWithParams 可选接口:支持带参数启动实例
|
|
type BrowserStarterWithParams interface {
|
|
StartInstanceWithParams(profileId string, params LaunchRequestParams) (*browser.Profile, error)
|
|
}
|
|
|
|
// BrowserStopper 可选接口:支持停止运行中的实例。
|
|
type BrowserStopper interface {
|
|
StopInstance(profileId string) (*browser.Profile, error)
|
|
}
|
|
|
|
// BrowserDebugWaiter 可选接口:等待实例调试端口进入可接管状态。
|
|
type BrowserDebugWaiter interface {
|
|
WaitInstanceDebugReady(profileId string, debugPort int, timeout time.Duration) (*browser.Profile, bool, error)
|
|
}
|
|
|
|
// AutomationScriptLister 可选接口:提供自动化脚本列表。
|
|
type AutomationScriptLister interface {
|
|
AutomationScriptList() ([]automation.ScriptRecord, error)
|
|
}
|
|
|
|
// AutomationScriptGetter 可选接口:提供单个自动化脚本详情。
|
|
type AutomationScriptGetter interface {
|
|
AutomationScriptGet(scriptID string) (*automation.ScriptRecord, error)
|
|
}
|
|
|
|
// AutomationScriptRunner 可选接口:执行自动化脚本。
|
|
type AutomationScriptRunner interface {
|
|
AutomationScriptRunWithOptions(input automation.ScriptRunRequest) (*automation.ScriptRunRecord, error)
|
|
}
|
|
|
|
// AutomationScriptRunLister 可选接口:提供自动化脚本运行记录。
|
|
type AutomationScriptRunLister interface {
|
|
AutomationScriptRunList(limit int) ([]automation.ScriptRunRecord, error)
|
|
}
|
|
|
|
// LaunchCallRecord 接口调用记录
|
|
type LaunchCallRecord struct {
|
|
Timestamp string `json:"timestamp"`
|
|
Method string `json:"method"`
|
|
Path string `json:"path"`
|
|
ClientIP string `json:"clientIp"`
|
|
Code string `json:"code"`
|
|
Selector LaunchSelector `json:"selector,omitempty"`
|
|
ProfileID string `json:"profileId"`
|
|
ProfileName string `json:"profileName"`
|
|
Params LaunchRequestParams `json:"params"`
|
|
OK bool `json:"ok"`
|
|
Status int `json:"status"`
|
|
Error string `json:"error"`
|
|
DurationMs int64 `json:"durationMs"`
|
|
}
|
|
|
|
// LaunchServer 本地 HTTP 唤起服务
|
|
type LaunchServer struct {
|
|
service *LaunchCodeService
|
|
starter BrowserStarter
|
|
browserMgr *browser.Manager
|
|
port int
|
|
server *http.Server
|
|
mu sync.Mutex
|
|
authMu sync.RWMutex
|
|
logMu sync.Mutex
|
|
callLogs []LaunchCallRecord
|
|
activeMu sync.RWMutex
|
|
activePort int
|
|
activeID string
|
|
activeName string
|
|
apiAuth APIAuthConfig
|
|
}
|
|
|
|
// NewLaunchServer 创建 LaunchServer
|
|
func NewLaunchServer(service *LaunchCodeService, starter BrowserStarter, mgr *browser.Manager, port int) *LaunchServer {
|
|
srv := &LaunchServer{
|
|
service: service,
|
|
starter: starter,
|
|
browserMgr: mgr,
|
|
port: port,
|
|
}
|
|
srv.SetAPIAuthConfig(APIAuthConfig{})
|
|
return srv
|
|
}
|
|
|
|
// Start 非阻塞启动 HTTP 服务。
|
|
// 规则:
|
|
// - port <= 0:自动分配随机可用端口(仅内部测试/显式传 0 时)
|
|
// - port > 0:绑定指定固定端口;若被占用则直接返回错误
|
|
func (s *LaunchServer) Start() error {
|
|
handler := s.buildHandler(true)
|
|
|
|
preferredPort := s.port
|
|
ln, port, err := bindLaunchListener(preferredPort)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.port = port
|
|
s.server = &http.Server{Handler: handler}
|
|
s.mu.Unlock()
|
|
|
|
log := logger.New("LaunchServer")
|
|
if preferredPort <= 0 {
|
|
log.Info("LaunchServer 使用随机端口", logger.F("port", port))
|
|
} else {
|
|
log.Info("LaunchServer 使用固定端口", logger.F("port", port))
|
|
}
|
|
auth := s.apiAuthConfig()
|
|
if auth.Active() {
|
|
log.Info("LaunchServer API 认证已启用", logger.F("header", auth.Header))
|
|
} else if auth.Requested() && !auth.Configured() {
|
|
log.Warn("LaunchServer API 认证配置未生效", logger.F("reason", "api_key is empty"), logger.F("header", auth.Header))
|
|
}
|
|
log.Info("LaunchServer 已启动", logger.F("port", port))
|
|
|
|
go func() {
|
|
if serveErr := s.server.Serve(ln); serveErr != nil && serveErr != http.ErrServerClosed {
|
|
log.Error("LaunchServer 异常退出", logger.F("error", serveErr.Error()))
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func bindLaunchListener(preferredPort int) (net.Listener, int, error) {
|
|
if preferredPort <= 0 {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("自动分配端口失败: %w", err)
|
|
}
|
|
port, err := listenerPort(ln)
|
|
if err != nil {
|
|
_ = ln.Close()
|
|
return nil, 0, err
|
|
}
|
|
return ln, port, nil
|
|
}
|
|
|
|
addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(preferredPort))
|
|
ln, err := net.Listen("tcp", addr)
|
|
if err == nil {
|
|
return ln, preferredPort, nil
|
|
}
|
|
return nil, 0, fmt.Errorf("端口 %d 不可用: %w", preferredPort, err)
|
|
}
|
|
|
|
func listenerPort(ln net.Listener) (int, error) {
|
|
if ln == nil {
|
|
return 0, fmt.Errorf("listener is nil")
|
|
}
|
|
if tcpAddr, ok := ln.Addr().(*net.TCPAddr); ok {
|
|
return tcpAddr.Port, nil
|
|
}
|
|
|
|
_, rawPort, err := net.SplitHostPort(ln.Addr().String())
|
|
if err != nil {
|
|
return 0, fmt.Errorf("解析监听地址失败: %w", err)
|
|
}
|
|
port, err := strconv.Atoi(rawPort)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("解析端口失败: %w", err)
|
|
}
|
|
return port, nil
|
|
}
|
|
|
|
// Stop 优雅关闭(5 秒超时)
|
|
func (s *LaunchServer) Stop() error {
|
|
s.mu.Lock()
|
|
srv := s.server
|
|
s.mu.Unlock()
|
|
|
|
if srv == nil {
|
|
return nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
return srv.Shutdown(ctx)
|
|
}
|
|
|
|
// Port 返回实际绑定的端口
|
|
func (s *LaunchServer) Port() int {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.port
|
|
}
|
|
|
|
// CDPURL 返回对外暴露的固定 CDP 入口地址。
|
|
func (s *LaunchServer) CDPURL() string {
|
|
port := s.Port()
|
|
if port <= 0 {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("http://127.0.0.1:%d", port)
|
|
}
|
|
|
|
// ActiveDebugPort 返回当前活动实例的内部调试端口。
|
|
func (s *LaunchServer) ActiveDebugPort() int {
|
|
s.activeMu.RLock()
|
|
defer s.activeMu.RUnlock()
|
|
return s.activePort
|
|
}
|
|
|
|
// SetActiveProfile 将统一入口切换到指定实例的调试端口。
|
|
func (s *LaunchServer) SetActiveProfile(profile *browser.Profile) {
|
|
if profile == nil || profile.DebugPort <= 0 || !profile.DebugReady {
|
|
return
|
|
}
|
|
|
|
s.activeMu.Lock()
|
|
s.activePort = profile.DebugPort
|
|
s.activeID = profile.ProfileId
|
|
s.activeName = profile.ProfileName
|
|
s.activeMu.Unlock()
|
|
}
|
|
|
|
// ClearActiveProfile 在当前活动实例停止后清空统一入口。
|
|
func (s *LaunchServer) ClearActiveProfile(profileID string) {
|
|
profileID = strings.TrimSpace(profileID)
|
|
if profileID == "" {
|
|
return
|
|
}
|
|
|
|
s.activeMu.Lock()
|
|
if s.activeID == profileID {
|
|
s.activePort = 0
|
|
s.activeID = ""
|
|
s.activeName = ""
|
|
}
|
|
s.activeMu.Unlock()
|
|
}
|
|
|
|
func (s *LaunchServer) activeTarget() (int, string, string) {
|
|
s.activeMu.RLock()
|
|
defer s.activeMu.RUnlock()
|
|
return s.activePort, s.activeID, s.activeName
|
|
}
|
|
|
|
// ActiveProfile 返回当前统一 CDP 入口对应的实例信息。
|
|
func (s *LaunchServer) ActiveProfile() (string, string, int) {
|
|
port, profileID, profileName := s.activeTarget()
|
|
return profileID, profileName, port
|
|
}
|