This commit is contained in:
2026-05-22 14:36:34 +08:00
parent 40f13989c2
commit 1336695068
11 changed files with 115 additions and 16 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -90,6 +90,17 @@
"type": "string", "type": "string",
"default": "", "default": "",
"description": "Value of the __Host-APPLOVINID cookie for p.applov.in (log in at https://p.applov.in/playablePreview?create=1&qr=1 and copy the cookie from DevTools)." "description": "Value of the __Host-APPLOVINID cookie for p.applov.in (log in at https://p.applov.in/playablePreview?create=1&qr=1 and copy the cookie from DevTools)."
},
"hplToolbox.applovin.delayMs": {
"type": "number",
"default": 5000,
"minimum": 0,
"description": "Delay (in milliseconds) between consecutive file uploads, to avoid AppLovin rate limiting / 403 responses."
},
"hplToolbox.applovin.randomizeUserAgent": {
"type": "boolean",
"default": true,
"description": "Randomize the User-Agent header on every request to p.applov.in to reduce bot detection."
} }
} }
}, },

View File

@@ -50,9 +50,30 @@ export function openApplovinUpload(_context: vscode.ExtensionContext) {
}); });
} }
const USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
];
function pickUserAgent(randomize: boolean): string {
if (!randomize) return USER_AGENTS[0];
return USER_AGENTS[Math.floor(Math.random() * USER_AGENTS.length)];
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) { async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
const cfg = vscode.workspace.getConfiguration('hplToolbox.applovin'); const cfg = vscode.workspace.getConfiguration('hplToolbox.applovin');
const cookie = (cfg.get<string>('cookie') || '').trim(); const cookie = (cfg.get<string>('cookie') || '').trim();
const delayMs = Math.max(0, cfg.get<number>('delayMs') ?? 1500);
const randomizeUA = cfg.get<boolean>('randomizeUserAgent') ?? true;
const paths = Array.isArray(filePaths) ? filePaths : [filePaths]; const paths = Array.isArray(filePaths) ? filePaths : [filePaths];
if (!paths.length) { if (!paths.length) {
@@ -60,14 +81,17 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
return; return;
} }
const buildHeaders = (): Record<string, string> => {
const headers: Record<string, string> = { const headers: Record<string, string> = {
'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept': 'application/json, text/javascript, */*; q=0.01',
'Origin': 'https://p.applov.in', 'Origin': 'https://p.applov.in',
'Referer': 'https://p.applov.in/playablePreview?create=1&qr=1', 'Referer': 'https://p.applov.in/playablePreview?create=1&qr=1',
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
'User-Agent': 'Mozilla/5.0 (HPL-Toolbox-VSCode)', 'User-Agent': pickUserAgent(randomizeUA),
}; };
if (cookie) headers['Cookie'] = `__Host-APPLOVINID=${cookie}`; if (cookie) headers['Cookie'] = `__Host-APPLOVINID=${cookie}`;
return headers;
};
const buildForm = (filePath: string): FormData => { const buildForm = (filePath: string): FormData => {
const buf = fs.readFileSync(filePath); const buf = fs.readFileSync(filePath);
@@ -90,12 +114,17 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
if (!filePath || !fs.existsSync(filePath)) { reportError('File missing'); continue; } if (!filePath || !fs.existsSync(filePath)) { reportError('File missing'); continue; }
if (!/\.html?$/i.test(filePath)) { reportError('Must be .html'); continue; } if (!/\.html?$/i.test(filePath)) { reportError('Must be .html'); continue; }
if (i > 0 && delayMs > 0) {
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Waiting ${delayMs}ms before ${fileName}...` });
await sleep(delayMs);
}
panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Validating ${fileName}...` }); panel.webview.postMessage({ type: 'status', message: `[${idx}/${paths.length}] Validating ${fileName}...` });
let res: Response; let res: Response;
try { try {
res = await fetch('https://p.applov.in/validateHTMLContent', { res = await fetch('https://p.applov.in/validateHTMLContent', {
method: 'POST', body: buildForm(filePath), headers, method: 'POST', body: buildForm(filePath), headers: buildHeaders(),
}); });
} catch (err: any) { reportError('Network error (validate): ' + (err?.message || err)); continue; } } catch (err: any) { reportError('Network error (validate): ' + (err?.message || err)); continue; }
const validateText = await res.text(); const validateText = await res.text();
@@ -110,7 +139,7 @@ async function handleUpload(panel: vscode.WebviewPanel, filePaths: string[]) {
try { try {
res = await fetch('https://p.applov.in/getCachedAdURL', { res = await fetch('https://p.applov.in/getCachedAdURL', {
method: 'POST', body: buildForm(filePath), headers, method: 'POST', body: buildForm(filePath), headers: buildHeaders(),
}); });
} catch (err: any) { reportError('Network error (cache): ' + (err?.message || err)); continue; } } catch (err: any) { reportError('Network error (cache): ' + (err?.message || err)); continue; }
const cacheText = await res.text(); const cacheText = await res.text();

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"math/rand"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
urlpkg "net/url" urlpkg "net/url"
@@ -12,8 +13,26 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"time"
) )
var applovinUserAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
}
func pickApplovinUserAgent(randomize bool) string {
if !randomize {
return applovinUserAgents[0]
}
return applovinUserAgents[rand.Intn(len(applovinUserAgents))]
}
func ApplovinPage(w http.ResponseWriter, r *http.Request) { func ApplovinPage(w http.ResponseWriter, r *http.Request) {
body := ` body := `
<h2>AppLovin Playable Preview (QR)</h2> <h2>AppLovin Playable Preview (QR)</h2>
@@ -237,7 +256,16 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
cookie := LoadConfig().Applovin.Cookie cfg := LoadConfig().Applovin
cookie := cfg.Cookie
delayMs := cfg.DelayMs
if delayMs < 0 {
delayMs = 0
}
randomizeUA := true
if cfg.RandomizeUserAgent != nil {
randomizeUA = *cfg.RandomizeUserAgent
}
w.Header().Set("Content-Type", "application/x-ndjson") w.Header().Set("Content-Type", "application/x-ndjson")
flusher, _ := w.(http.Flusher) flusher, _ := w.(http.Flusher)
@@ -282,7 +310,7 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
req.Header.Set("Origin", "https://p.applov.in") req.Header.Set("Origin", "https://p.applov.in")
req.Header.Set("Referer", "https://p.applov.in/playablePreview?create=1&qr=1") req.Header.Set("Referer", "https://p.applov.in/playablePreview?create=1&qr=1")
req.Header.Set("X-Requested-With", "XMLHttpRequest") req.Header.Set("X-Requested-With", "XMLHttpRequest")
req.Header.Set("User-Agent", "Mozilla/5.0 (HPL-Toolbox-Standalone)") req.Header.Set("User-Agent", pickApplovinUserAgent(randomizeUA))
if cookie != "" { if cookie != "" {
req.Header.Set("Cookie", "__Host-APPLOVINID="+cookie) req.Header.Set("Cookie", "__Host-APPLOVINID="+cookie)
} }
@@ -311,6 +339,11 @@ func ApplovinUpload(w http.ResponseWriter, r *http.Request) {
continue continue
} }
if i > 0 && delayMs > 0 {
send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Waiting %dms before %s...", idx, len(req.Paths), delayMs, fileName)})
time.Sleep(time.Duration(delayMs) * time.Millisecond)
}
send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Validating %s...", idx, len(req.Paths), fileName)}) send(map[string]any{"type": "status", "message": fmt.Sprintf("[%d/%d] Validating %s...", idx, len(req.Paths), fileName)})
body, ct, err := buildForm(filePath) body, ct, err := buildForm(filePath)

Binary file not shown.

View File

@@ -19,7 +19,7 @@ import (
var currentHwnd atomic.Uintptr var currentHwnd atomic.Uintptr
func setupFileLogging() { func setupFileLogging() {
logPath := filepath.Join(appDir(), "debug.log") logPath := filepath.Join(userDataDir(), "debug.log")
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil { if err != nil {
return return
@@ -57,9 +57,16 @@ func main() {
} }
defer removeLockFile() defer removeLockFile()
webviewTmp, err := os.MkdirTemp("", "hpltoolbox-wv2-*")
if err != nil {
log.Fatalf("Failed to create webview temp dir: %v", err)
}
defer os.RemoveAll(webviewTmp)
w := webview.NewWithOptions(webview.WebViewOptions{ w := webview.NewWithOptions(webview.WebViewOptions{
Debug: false, Debug: false,
AutoFocus: true, AutoFocus: true,
DataPath: webviewTmp,
WindowOptions: webview.WindowOptions{ WindowOptions: webview.WindowOptions{
Title: "HPL Toolbox", Title: "HPL Toolbox",
Width: 1100, Width: 1100,

View File

@@ -19,6 +19,8 @@ type PlecConfig struct {
type ApplovinConfig struct { type ApplovinConfig struct {
Cookie string `json:"cookie"` Cookie string `json:"cookie"`
DelayMs int `json:"delayMs"`
RandomizeUserAgent *bool `json:"randomizeUserAgent,omitempty"`
} }
type SendToMobileConfig struct { type SendToMobileConfig struct {
@@ -38,6 +40,23 @@ type AppConfig struct {
var configMu sync.Mutex var configMu sync.Mutex
// userDataDir returns %LOCALAPPDATA%\HPLToolbox, creating it if needed.
// All mutable app files (config, lock, logs, WebView2 cache) live here so
// they are never placed next to the executable, which may be on a synced or
// antivirus-watched drive and would cause message-loop stalls.
func userDataDir() string {
local := os.Getenv("LOCALAPPDATA")
if local == "" {
local = os.Getenv("APPDATA")
}
if local == "" {
return appDir()
}
dir := filepath.Join(local, "HPLToolbox")
_ = os.MkdirAll(dir, 0755)
return dir
}
func defaultConfig() AppConfig { func defaultConfig() AppConfig {
return AppConfig{ return AppConfig{
Plec: PlecConfig{ Plec: PlecConfig{
@@ -46,7 +65,7 @@ func defaultConfig() AppConfig {
PreviewBase: "https://playable.applovindemo.com/phaser/", PreviewBase: "https://playable.applovindemo.com/phaser/",
SkipExistsCheck: false, SkipExistsCheck: false,
}, },
Applovin: ApplovinConfig{Cookie: ""}, Applovin: ApplovinConfig{Cookie: "", DelayMs: 5000},
SendToMobile: SendToMobileConfig{Port: 0}, SendToMobile: SendToMobileConfig{Port: 0},
DailyUpdate: DailyUpdateConfig{LastProject: ""}, DailyUpdate: DailyUpdateConfig{LastProject: ""},
} }
@@ -72,7 +91,7 @@ func appDir() string {
} }
func configPath() string { func configPath() string {
return filepath.Join(appDir(), "config.json") return filepath.Join(userDataDir(), "config.json")
} }
func LoadConfig() AppConfig { func LoadConfig() AppConfig {

View File

@@ -12,7 +12,7 @@ import (
) )
func lockFilePath() string { func lockFilePath() string {
return filepath.Join(appDir(), ".hpltoolbox.lock") return filepath.Join(userDataDir(), ".hpltoolbox.lock")
} }
// focusExistingInstance returns true if another instance was found and // focusExistingInstance returns true if another instance was found and