refactor: js plugin api

This commit is contained in:
krau
2025-11-16 21:38:30 +08:00
parent 3f40acff55
commit 131dfeb4cd
14 changed files with 165 additions and 112 deletions

View File

@@ -1,19 +1,17 @@
package parsers package js
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log/slog"
"net/http" "net/http"
"sync"
"github.com/blang/semver" "github.com/blang/semver"
"github.com/charmbracelet/log" "github.com/charmbracelet/log"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/krau/SaveAny-Bot/common/utils/netutil" "github.com/krau/SaveAny-Bot/common/utils/netutil"
"github.com/playwright-community/playwright-go" "github.com/krau/SaveAny-Bot/parsers/parsers"
) )
func jsRegisterParser(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value { func jsRegisterParser(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value {
@@ -57,7 +55,7 @@ func jsRegisterParser(vm *goja.Runtime) func(call goja.FunctionCall) goja.Value
if parseFn == nil || goja.IsUndefined(parseFn) { if parseFn == nil || goja.IsUndefined(parseFn) {
return vm.NewGoError(errors.New("parser must provide a parse function")) return vm.NewGoError(errors.New("parser must provide a parse function"))
} }
AddParser(newJSParser(vm, handleFn, parseFn, metadata)) parsers.Add(newJSParser(vm, handleFn, parseFn, metadata))
return goja.Undefined() return goja.Undefined()
} }
} }
@@ -173,74 +171,3 @@ var jsGhttp = func(vm *goja.Runtime) *goja.Object {
}) })
return ghttp return ghttp
} }
var jsPlaywright = func(vm *goja.Runtime, logger *log.Logger) *goja.Object {
pwObj := vm.NewObject()
var installOnce sync.Once
slogger := slog.New(logger)
pwObj.Set("get", func(call goja.FunctionCall) goja.Value {
url := call.Argument(0).String()
var installErr error
installOnce.Do(func() {
installErr = playwright.Install(&playwright.RunOptions{
Browsers: []string{"chromium"},
DriverDirectory: "./playwright",
Logger: slogger,
})
})
if installErr != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to install playwright: %v", installErr),
})
}
pw, err := playwright.Run(&playwright.RunOptions{
DriverDirectory: "./playwright",
Logger: slogger,
})
if err != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to start playwright: %v", err),
})
}
defer pw.Stop()
browser, err := pw.Chromium.Launch()
if err != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to launch browser: %v", err),
})
}
defer browser.Close()
page, err := browser.NewPage()
if err != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to create page: %v", err),
})
}
resp, err := page.Goto(url, playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateNetworkidle,
Timeout: playwright.Float(60000),
})
if err != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to navigate: %v", err),
})
}
if resp != nil && resp.Status() >= 400 {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("bad status code: %d", resp.Status()),
})
}
content, err := page.Content()
if err != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to get page content: %v", err),
})
}
return vm.ToValue(content)
})
return pwObj
}

View File

@@ -0,0 +1,82 @@
package js
import (
"fmt"
"log/slog"
"sync"
"github.com/charmbracelet/log"
"github.com/dop251/goja"
"github.com/playwright-community/playwright-go"
)
var jsPlaywright = func(vm *goja.Runtime, logger *log.Logger) *goja.Object {
pwObj := vm.NewObject()
var installOnce sync.Once
slogger := slog.New(logger)
pwObj.Set("get", func(call goja.FunctionCall) goja.Value {
url := call.Argument(0).String()
var installErr error
installOnce.Do(func() {
installErr = playwright.Install(&playwright.RunOptions{
Browsers: []string{"chromium"},
DriverDirectory: "./playwright",
Logger: slogger,
})
})
if installErr != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to install playwright: %v", installErr),
})
}
pw, err := playwright.Run(&playwright.RunOptions{
DriverDirectory: "./playwright",
Logger: slogger,
})
if err != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to start playwright: %v", err),
})
}
defer pw.Stop()
browser, err := pw.Chromium.Launch()
if err != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to launch browser: %v", err),
})
}
defer browser.Close()
page, err := browser.NewPage()
if err != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to create page: %v", err),
})
}
resp, err := page.Goto(url, playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateNetworkidle,
Timeout: playwright.Float(60000),
})
if err != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to navigate: %v", err),
})
}
if resp != nil && resp.Status() >= 400 {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("bad status code: %d", resp.Status()),
})
}
content, err := page.Content()
if err != nil {
return vm.ToValue(map[string]any{
"error": fmt.Sprintf("failed to get page content: %v", err),
})
}
return vm.ToValue(content)
})
return pwObj
}

View File

@@ -0,0 +1,19 @@
//go:build no_playwright
package js
import (
"github.com/charmbracelet/log"
"github.com/dop251/goja"
)
var jsPlaywright = func(vm *goja.Runtime, _ *log.Logger) *goja.Object {
pwObj := vm.NewObject()
unsupported := vm.ToValue(map[string]any{
"error": "playwright is not supported in this build",
})
pwObj.Set("get", func(call goja.FunctionCall) goja.Value {
return unsupported
})
return pwObj
}

View File

@@ -1,4 +1,4 @@
package parsers package js
import ( import (
"context" "context"

View File

@@ -1,4 +1,4 @@
package parsers package js
import "github.com/blang/semver" import "github.com/blang/semver"

View File

@@ -3,41 +3,16 @@ package parsers
import ( import (
"context" "context"
"fmt" "fmt"
"sync"
"github.com/krau/SaveAny-Bot/config" "github.com/krau/SaveAny-Bot/parsers/js"
"github.com/krau/SaveAny-Bot/parsers/kemono" "github.com/krau/SaveAny-Bot/parsers/native/kemono"
"github.com/krau/SaveAny-Bot/parsers/twitter" "github.com/krau/SaveAny-Bot/parsers/native/twitter"
"github.com/krau/SaveAny-Bot/parsers/parsers"
"github.com/krau/SaveAny-Bot/pkg/parser" "github.com/krau/SaveAny-Bot/pkg/parser"
) )
var (
parsers []parser.Parser
parsersMu sync.Mutex
doConfig sync.Once
configParsers = func() {
if len(parsers) == 0 {
return
}
for _, pser := range parsers {
if configurable, ok := pser.(parser.ConfigurableParser); ok {
cfg := config.C().GetParserConfigByName(configurable.Name())
if err := configurable.Configure(cfg); err != nil {
fmt.Printf("Error configuring parser %s: %v\n", configurable.Name(), err)
}
}
}
}
)
func AddParser(p ...parser.Parser) {
parsersMu.Lock()
defer parsersMu.Unlock()
parsers = append(parsers, p...)
}
func init() { func init() {
AddParser(new(twitter.TwitterParser), new(kemono.KemonoParser)) parsers.Add(new(twitter.TwitterParser), new(kemono.KemonoParser))
} }
var ( var (
@@ -45,12 +20,11 @@ var (
) )
func ParseWithContext(ctx context.Context, url string) (*parser.Item, error) { func ParseWithContext(ctx context.Context, url string) (*parser.Item, error) {
doConfig.Do(configParsers)
ch := make(chan *parser.Item, 1) ch := make(chan *parser.Item, 1)
errCh := make(chan error, 1) errCh := make(chan error, 1)
go func() { go func() {
for _, pser := range parsers { for _, pser := range parsers.Get() {
if !pser.CanHandle(url) { if !pser.CanHandle(url) {
continue continue
} }
@@ -76,11 +50,18 @@ func ParseWithContext(ctx context.Context, url string) (*parser.Item, error) {
} }
func CanHandle(url string) (bool, parser.Parser) { func CanHandle(url string) (bool, parser.Parser) {
doConfig.Do(configParsers) for _, pser := range parsers.Get() {
for _, pser := range parsers {
if pser.CanHandle(url) { if pser.CanHandle(url) {
return true, pser return true, pser
} }
} }
return false, nil return false, nil
} }
func LoadPlugins(ctx context.Context, dir string) error {
return js.LoadPlugins(ctx, dir)
}
func AddPlugin(ctx context.Context, code string, name string) error {
return js.AddPlugin(ctx, code, name)
}

View File

@@ -0,0 +1,44 @@
package parsers
import (
"fmt"
"sync"
"github.com/krau/SaveAny-Bot/config"
"github.com/krau/SaveAny-Bot/pkg/parser"
)
var (
parsers []parser.Parser
mu sync.Mutex
configOnce sync.Once
configParsers = func() {
mu.Lock()
defer mu.Unlock()
if len(parsers) == 0 {
return
}
for _, pser := range parsers {
if configurable, ok := pser.(parser.ConfigurableParser); ok {
cfg := config.C().GetParserConfigByName(configurable.Name())
if err := configurable.Configure(cfg); err != nil {
fmt.Printf("Error configuring parser %s: %v\n", configurable.Name(), err)
}
}
}
}
)
func Add(p ...parser.Parser) {
configOnce.Do(configParsers)
mu.Lock()
defer mu.Unlock()
parsers = append(parsers, p...)
}
func Get() []parser.Parser {
configOnce.Do(configParsers)
mu.Lock()
defer mu.Unlock()
return parsers
}