support use chrome

This commit is contained in:
Star 2025-07-21 00:09:21 +08:00
parent ea04fd82c8
commit bf65f6ccfa
7 changed files with 1499 additions and 30 deletions

80
chrome.go Normal file
View File

@ -0,0 +1,80 @@
package http
import (
"runtime"
"sync"
"apigo.cc/gojs"
"apigo.cc/gojs/goja"
"github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher"
"github.com/ssgo/log"
"github.com/ssgo/u"
)
type Chrome struct {
id string
launcher *launcher.Launcher
browser *rod.Browser
}
var chromes = map[string]*Chrome{}
var chromesLock sync.Mutex
func (ch *Chrome) Close() {
if ch.browser != nil {
ch.browser.Close()
ch.browser = nil
}
if ch.launcher != nil {
ch.launcher.Cleanup()
ch.launcher = nil
}
chromesLock.Lock()
delete(chromes, ch.id)
chromesLock.Unlock()
}
func CloseAllChrome() {
n := len(chromes)
if n > 0 {
for _, ch := range chromes {
ch.Close()
}
log.DefaultLogger.Info("关闭所有 Chrome 浏览器", "count", n)
}
}
func StartChrome(showWindow *bool, vm *goja.Runtime) (*Chrome, error) {
logger := gojs.GetLogger(vm)
ch := &Chrome{}
ch.browser = rod.New()
if localBrowserPath, hasLocalBrowser := launcher.LookPath(); hasLocalBrowser {
ch.launcher = launcher.New().Bin(localBrowserPath).Headless(!u.Bool(showWindow)).Set("no-sandbox").Set("disable-gpu").Set("disable-dev-shm-usage").Set("single-process")
switch runtime.GOOS {
case "linux":
ch.launcher.Set("disable-setuid-sandbox")
case "windows":
ch.launcher.Set("disable-features=RendererCodeIntegrity")
}
localChromeURL, err := ch.launcher.Launch()
if err != nil {
ch.Close()
return nil, gojs.Err(err)
}
logger.Info("启动本地 Chrome 浏览器", "url", localChromeURL, "path", localBrowserPath)
ch.browser.ControlURL(localChromeURL)
}
if err := ch.browser.Connect(); err != nil {
ch.Close()
return nil, gojs.Err(err)
}
ch.id = u.UniqueId()
chromesLock.Lock()
chromes[ch.id] = ch
chromesLock.Unlock()
return ch, nil
}

26
chrome_test.go Normal file
View File

@ -0,0 +1,26 @@
package http_test
import (
"fmt"
"testing"
"apigo.cc/gojs"
_ "apigo.cc/gojs/console"
_ "apigo.cc/gojs/file"
_ "apigo.cc/gojs/http"
_ "apigo.cc/gojs/util"
"github.com/ssgo/u"
)
func TestChrome(t *testing.T) {
gojs.ExportForDev()
r, err := gojs.RunFile("chrome_test.js")
if err != nil {
t.Fatal(u.Red("chrome_test"), u.BRed(err.Error()))
} else if r != true {
t.Fatal(u.Red("chrome_test"), u.BRed(u.JsonP(r)))
} else {
fmt.Println(u.Green("chrome_test"), u.BGreen("test succeess"))
}
gojs.WaitAll()
}

199
chrome_test.js Normal file
View File

@ -0,0 +1,199 @@
import http from 'apigo.cc/gojs/http'
import co from 'apigo.cc/gojs/console'
import u from 'apigo.cc/gojs/util'
import fs from 'apigo.cc/gojs/file'
let t1, t2
function printOK(...testResult) {
t2 = u.timestampMS()
co.info('. >> 成功', '耗时[' + (t2 - t1) + ']', !testResult ? '' : testResult.join(' '))
t1 = t2
}
co.log("开始测试...")
t1 = u.timestampMS()
co.log("启动Chrome浏览器...")
let ch = http.startChrome()
printOK("Chrome启动成功")
var page
co.log("打开z20首页...")
page = ch.open("https://z20.cc/")
printOK()
co.log("设置视口大小...")
page.setViewport(1280, 720)
printOK("1280×720")
co.log("获取当前URL...")
let url = page.url()
printOK(url)
co.log("获取页面标题...")
const title = page.title()
printOK(title.substring(0, 15) + (title.length > 15 ? "..." : ""))
co.log("导航到Apigo.cc首页...")
page.navigate("https://apigo.cc")
printOK()
co.log("获取当前URL...")
url = page.url()
printOK(url)
co.log("获取资源列表...")
let res = page.getResList()
printOK(`资源列表(${res.length})获取成功`)
co.log("获取资源列表按类型script...")
res = page.getResListByType("script")
printOK(`资源列表(${res.length})获取成功`)
co.log("获取资源列表按MimeType...")
res = page.getResListByMimeType("text/*")
printOK(`资源列表(${res.length})获取成功`)
co.log("获取资源内容...")
let content = page.getResContent(res[0].url)
printOK(`资源(${res[0].url})内容(${content.length}/${res[0].size})获取成功`)
co.log("执行后退操作...")
page.back()
printOK()
co.log("获取当前URL...")
url = page.url()
printOK(url)
co.log("执行前进操作...")
page.forward()
printOK()
co.log("获取当前URL...")
url = page.url()
printOK(url)
co.log("滚动页面...")
page.scrollTo(0, 500)
printOK()
co.log("查找Logo...")
const logo = page.find(".logo")
printOK("已找到")
co.log("检查Logo可见性...")
printOK(logo.isVisible())
co.log("获取Logo 尺寸...")
printOK(logo.getAttribute("width"), logo.getAttribute("height"))
co.log("获取Logo alt...")
printOK(logo.getAttribute("alt"))
co.log("获取Logo URL...")
printOK(logo.getAttribute("src"))
co.log("获取Logo位置信息...")
const rect = logo.getBoundingRect()
printOK(`X:${rect.x} Y:${rect.y} W:${rect.width} H:${rect.height}`)
co.log("导航到Apigo.cc探索页...")
// page = ch.open("https://apigo.cc/explore/repos")
page.navigate("https://apigo.cc/explore/repos")
printOK()
co.log("获取当前URL...")
url = page.url()
printOK(url)
co.log("查找搜索框...")
var searchInput = page.findN("input[type=search]")
if (!searchInput) {
return "搜索框不存在"
}
printOK("存在")
co.log("查找搜索框父...")
const parent = searchInput.parent()
printOK("存在", parent.getAttribute('class'))
co.log("查找搜索按钮...")
const searchBtn = parent.find("button")
printOK("存在")
printOK("存在", searchBtn.getAttribute('class') == searchBtn.getProperty('className') ? 'class检查通过' : 'class检查失败')
co.log("查找搜索按钮findChild...")
parent.findChild("button")
printOK("成功")
co.log("查找搜索按钮findX...")
parent.findX("./button")
printOK("成功")
co.log("查找表单...")
const form = searchInput.findParent('form')
printOK("存在", form.getAttribute('id'))
co.log("检查搜索框可交互性...")
printOK(searchInput.isInteractable())
co.log("设置搜索框值...")
searchInput.setValue("ag")
printOK("值已设置")
co.log("等待跳转搜索结果...")
page.waitPageLoad()
searchInput = page.find("input[type=search]")
printOK("完成", page.url())
co.log("获取搜索框值...")
printOK(searchInput.getValue())
co.log("查找所有结果链接...")
var list = page.findAll(".flex-list > .flex-item")
printOK(`找到 ${list.length} 个链接`)
co.log("查找第二个项目的仓库连接...")
const link = list[0].find("a:nth-child(2)")
printOK(link.getAttribute("href"))
co.log("计算按钮中心位置...")
const linkCenter = link.getCenter()
printOK(`X:${linkCenter.x.toFixed(1)} Y:${linkCenter.y.toFixed(1)}`)
co.log("移动鼠标到按钮中心...")
page.mouseMoveTo(linkCenter.x, linkCenter.y)
printOK()
co.log("按下鼠标左键...")
page.mouseClick()
printOK()
co.log("等待搜索结果稳定...")
page.waitPageLoad()
printOK()
co.log("获取当前URL...")
url = page.url()
printOK(url)
co.log("截取页面截图...")
const screenshot1 = page.screenshot()
printOK(`size:${screenshot1.length}`)
// fs.write('./screenshot1.png', screenshot1)
co.log("截取页面截图(全部)...")
const screenshot2 = page.screenshotFullPage()
printOK(`size:${screenshot2.length}`)
// fs.write('./screenshot2.png', screenshot2)
co.log("生成PDF...")
const pdf = page.pDF()
printOK(`size:${pdf.length}`)
// fs.write('./page.pdf', pdf)
co.log("所有测试步骤完成!")
return true

57
go.mod
View File

@ -1,41 +1,48 @@
module apigo.cc/gojs/http
go 1.18
go 1.23.0
require (
apigo.cc/gojs v0.0.1
apigo.cc/gojs v0.0.21
apigo.cc/gojs/console v0.0.2
apigo.cc/gojs/file v0.0.4
apigo.cc/gojs/util v0.0.12
github.com/go-rod/rod v0.112.8
github.com/gorilla/websocket v1.5.3
github.com/ssgo/config v1.7.9
github.com/ssgo/httpclient v1.7.8
github.com/ssgo/u v1.7.9
github.com/ssgo/log v1.7.7
github.com/ssgo/s v1.7.24
github.com/ssgo/u v1.7.21
)
require (
apigo.cc/gojs/console v0.0.1 // indirect
github.com/ZZMarquis/gm v1.3.2 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/emmansun/gmsm v0.30.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/gomodule/redigo v1.9.2 // indirect
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/obscuren/ecies v0.0.0-20150213224233-7c0f4a9b18d9 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/ssgo/discover v1.7.8 // indirect
github.com/ssgo/redis v1.7.7 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
)
require (
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/gorilla/websocket v1.5.3
github.com/ssgo/config v1.7.7 // indirect
github.com/ssgo/log v1.7.7
github.com/ssgo/s v1.7.13
github.com/ssgo/discover v1.7.10 // indirect
github.com/ssgo/redis v1.7.8 // indirect
github.com/ssgo/standard v1.7.7 // indirect
github.com/ssgo/tool v0.4.27 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/text v0.19.0 // indirect
github.com/ssgo/tool v0.4.29 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

33
http.go
View File

@ -12,6 +12,7 @@ import (
"apigo.cc/gojs"
"apigo.cc/gojs/goja"
"github.com/gorilla/websocket"
"github.com/ssgo/config"
"github.com/ssgo/httpclient"
"github.com/ssgo/log"
"github.com/ssgo/u"
@ -31,8 +32,13 @@ var defaultHttp = &Http{
globalHeaders: make(map[string]string),
}
// TODO ws
var conf = struct {
Timeout int
ChromePath string
}{}
func init() {
config.LoadConfig("http", &conf)
obj := map[string]any{
"new": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
return newClient("HTTP", argsIn, vm)
@ -51,11 +57,28 @@ func init() {
"connect": defaultHttp.Connect,
}
obj1 := map[string]any{
"startChrome": StartChrome,
"closeAllChrome": CloseAllChrome,
}
tsCode := gojs.MakeTSCode(obj1)
mappedObj := gojs.ToMap(obj1)
for k, v := range mappedObj {
obj[k] = v
}
gojs.Register("apigo.cc/gojs/http", gojs.Module{
Object: obj,
TsCode: httpTS,
Desc: "",
Example: "",
Object: obj,
// TsCode: httpTS,
TsCodeMaker: func() string {
a := strings.SplitN(tsCode, "export default {", 2)
exportPart1 := "export default {\n" + strings.TrimRight(a[1], "}")
return strings.Replace(httpTS, "export default {", exportPart1, 1) + "\n" + a[0]
},
Desc: "",
Example: "",
WaitForStop: CloseAllChrome,
})
}

View File

@ -26,6 +26,9 @@ function upload(url: string, form: Object, files: Object, headers?: Object): Res
function download(filename: string, url: string, callback?: (finished: number, total: number) => void, headers?: Object): Result { return null as any }
function connect(url: string, config?: WSConfig): WS { return null as any }
interface Chrome {
}
interface Client {
get(url: string, headers?: Object): Result
head(url: string, headers?: Object): Result

1131
page.go Normal file

File diff suppressed because it is too large Load Diff