stella-coder 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/stella-cli/adb.mjs +200 -0
- package/stella-cli/browser-control.mjs +274 -0
- package/stella-cli/charts.mjs +411 -0
- package/stella-cli/game-engine.mjs +708 -0
- package/stella-cli/gdrive-backup.mjs +338 -0
- package/stella-cli/git-api.mjs +407 -0
- package/stella-cli/gmail.mjs +415 -0
- package/stella-cli/home-assistant.mjs +168 -0
- package/stella-cli/index.mjs +1364 -0
- package/stella-cli/web-parser.mjs +229 -0
- package/stella-cli/yandex-maps.mjs +426 -0
- package/capture.ps1 +0 -14
- package/test_screen.png +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stella-coder",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Stella Coder 5.0 — AI coding agent with Telegram bot, huge context, TDD, Git ecosystem, presentations, computer control, smart home, Office automation, and antivirus",
|
|
6
6
|
"main": "stella-cli/index.mjs",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"class-variance-authority": "^0.7.1",
|
|
37
37
|
"clsx": "^2.1.1",
|
|
38
38
|
"gsap": "^3.15.0",
|
|
39
|
+
"jsdom": "^29.1.1",
|
|
39
40
|
"lucide-react": "^1.16.0",
|
|
40
41
|
"next": "16.2.6",
|
|
41
42
|
"react": "^19",
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { execSync } from "node:child_process"
|
|
2
|
+
import fs from "node:fs"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
|
|
6
|
+
const ADB_DIR = path.join(os.homedir(), ".stella", "adb")
|
|
7
|
+
|
|
8
|
+
function ensureDir() { if (!fs.existsSync(ADB_DIR)) fs.mkdirSync(ADB_DIR, { recursive: true }) }
|
|
9
|
+
|
|
10
|
+
function adb(args) {
|
|
11
|
+
try {
|
|
12
|
+
return execSync(`adb ${args}`, { encoding: "utf8", timeout: 10000 }).trim()
|
|
13
|
+
} catch (e) {
|
|
14
|
+
return { error: e.stderr?.trim() || e.message }
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ADB {
|
|
19
|
+
constructor() { ensureDir() }
|
|
20
|
+
|
|
21
|
+
isAvailable() {
|
|
22
|
+
try {
|
|
23
|
+
execSync("adb --version", { encoding: "utf8", stdio: "ignore" })
|
|
24
|
+
return true
|
|
25
|
+
} catch { return false }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getDevices() {
|
|
29
|
+
const out = adb("devices")
|
|
30
|
+
if (out.error) return { success: false, error: out.error }
|
|
31
|
+
const lines = out.split("\n").filter(l => l.trim() && !l.includes("List of devices"))
|
|
32
|
+
const devices = lines.map(l => { const [id, status] = l.trim().split(/\s+/); return { id, status } })
|
|
33
|
+
return { success: true, devices }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getState(serial) {
|
|
37
|
+
const s = serial ? `-s ${serial}` : ""
|
|
38
|
+
const out = adb(`${s} get-state`)
|
|
39
|
+
return out.error ? { success: false, error: out.error } : { success: true, state: out }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
shell(serial, cmd) {
|
|
43
|
+
const s = serial ? `-s ${serial}` : ""
|
|
44
|
+
const out = adb(`${s} shell ${cmd}`)
|
|
45
|
+
return out.error ? { success: false, error: out.error } : { success: true, output: out }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
screenshot(serial) {
|
|
49
|
+
ensureDir()
|
|
50
|
+
const s = serial ? `-s ${serial}` : ""
|
|
51
|
+
const filename = `screenshot_${Date.now()}.png`
|
|
52
|
+
const localPath = path.join(ADB_DIR, filename)
|
|
53
|
+
adb(`${s} exec-out screencap -p > "${localPath}"`)
|
|
54
|
+
if (fs.existsSync(localPath) && fs.statSync(localPath).size > 0) {
|
|
55
|
+
return { success: true, path: localPath }
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
adb(`${s} shell screencap -p /sdcard/screen.png`)
|
|
59
|
+
adb(`${s} pull /sdcard/screen.png "${localPath}"`)
|
|
60
|
+
adb(`${s} shell rm /sdcard/screen.png`)
|
|
61
|
+
if (fs.existsSync(localPath) && fs.statSync(localPath).size > 0) return { success: true, path: localPath }
|
|
62
|
+
} catch {}
|
|
63
|
+
return { success: false, error: "Screenshot failed" }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
tap(serial, x, y) {
|
|
67
|
+
const s = serial ? `-s ${serial}` : ""
|
|
68
|
+
const out = adb(`${s} shell input tap ${x} ${y}`)
|
|
69
|
+
return out.error ? { success: false, error: out.error } : { success: true }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
swipe(serial, x1, y1, x2, y2, duration = 300) {
|
|
73
|
+
const s = serial ? `-s ${serial}` : ""
|
|
74
|
+
const out = adb(`${s} shell input swipe ${x1} ${y1} ${x2} ${y2} ${duration}`)
|
|
75
|
+
return out.error ? { success: false, error: out.error } : { success: true }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
text(serial, text) {
|
|
79
|
+
const s = serial ? `-s ${serial}` : ""
|
|
80
|
+
const escaped = text.replace(/ /g, "%s").replace(/"/g, '\\"')
|
|
81
|
+
const out = adb(`${s} shell input text "${escaped}"`)
|
|
82
|
+
return out.error ? { success: false, error: out.error } : { success: true }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
key(serial, keycode) {
|
|
86
|
+
const s = serial ? `-s ${serial}` : ""
|
|
87
|
+
const out = adb(`${s} shell input keyevent ${keycode}`)
|
|
88
|
+
return out.error ? { success: false, error: out.error } : { success: true }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
pressHome(serial) { return this.key(serial, 3) }
|
|
92
|
+
pressBack(serial) { return this.key(serial, 4) }
|
|
93
|
+
pressMenu(serial) { return this.key(serial, 82) }
|
|
94
|
+
pressPower(serial) { return this.key(serial, 26) }
|
|
95
|
+
pressVolumeUp(serial) { return this.key(serial, 24) }
|
|
96
|
+
pressVolumeDown(serial) { return this.key(serial, 25) }
|
|
97
|
+
|
|
98
|
+
installApp(serial, apkPath) {
|
|
99
|
+
if (!fs.existsSync(apkPath)) return { success: false, error: "APK not found" }
|
|
100
|
+
const s = serial ? `-s ${serial}` : ""
|
|
101
|
+
const out = adb(`${s} install "${apkPath}"`)
|
|
102
|
+
if (out.error) return { success: false, error: out.error }
|
|
103
|
+
return { success: true, output: out }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
uninstallApp(serial, packageName) {
|
|
107
|
+
const s = serial ? `-s ${serial}` : ""
|
|
108
|
+
const out = adb(`${s} uninstall ${packageName}`)
|
|
109
|
+
if (out.error) return { success: false, error: out.error }
|
|
110
|
+
return { success: true, output: out }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
listPackages(serial, filter = "") {
|
|
114
|
+
const s = serial ? `-s ${serial}` : ""
|
|
115
|
+
const out = adb(`${s} shell pm list packages ${filter}`)
|
|
116
|
+
if (out.error) return { success: false, error: out.error }
|
|
117
|
+
const packages = out.split("\n").filter(l => l.trim()).map(l => l.replace("package:", "").trim())
|
|
118
|
+
return { success: true, packages }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getBattery(serial) {
|
|
122
|
+
const s = serial ? `-s ${serial}` : ""
|
|
123
|
+
const out = adb(`${s} shell dumpsys battery`)
|
|
124
|
+
if (out.error) return { success: false, error: out.error }
|
|
125
|
+
const level = out.match(/level:\s*(\d+)/)?.[1]
|
|
126
|
+
const temp = out.match(/temperature:\s*(\d+)/)?.[1]
|
|
127
|
+
const status = out.match(/status:\s*(\d+)/)?.[1]
|
|
128
|
+
const statusMap = { 1: "unknown", 2: "charging", 3: "discharging", 4: "not charging", 5: "full" }
|
|
129
|
+
return { success: true, level: parseInt(level), temperature: temp ? parseInt(temp) / 10 : null, status: statusMap[status] || "unknown" }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getInfo(serial) {
|
|
133
|
+
const s = serial ? `-s ${serial}` : ""
|
|
134
|
+
const model = adb(`${s} shell getprop ro.product.model`)
|
|
135
|
+
const brand = adb(`${s} shell getprop ro.product.brand`)
|
|
136
|
+
const android = adb(`${s} shell getprop ro.build.version.release`)
|
|
137
|
+
const sdk = adb(`${s} shell getprop ro.build.version.sdk`)
|
|
138
|
+
const resolution = adb(`${s} shell wm size`)
|
|
139
|
+
const density = adb(`${s} shell wm density`)
|
|
140
|
+
return {
|
|
141
|
+
success: true,
|
|
142
|
+
model: model.error ? "unknown" : model,
|
|
143
|
+
brand: brand.error ? "unknown" : brand,
|
|
144
|
+
android: android.error ? "unknown" : android,
|
|
145
|
+
sdk: sdk.error ? "unknown" : sdk,
|
|
146
|
+
resolution: resolution.error ? "unknown" : resolution.replace("Physical size: ", ""),
|
|
147
|
+
density: density.error ? "unknown" : density.replace("Physical density: ", ""),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
startApp(serial, packageName) {
|
|
152
|
+
return this.shell(serial, `monkey -p ${packageName} -c android.intent.category.LAUNCHER 1`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
killApp(serial, packageName) {
|
|
156
|
+
return this.shell(serial, `am force-stop ${packageName}`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
pullFile(serial, remotePath, localPath) {
|
|
160
|
+
const s = serial ? `-s ${serial}` : ""
|
|
161
|
+
ensureDir()
|
|
162
|
+
const dest = localPath || path.join(ADB_DIR, path.basename(remotePath))
|
|
163
|
+
const out = adb(`${s} pull "${remotePath}" "${dest}"`)
|
|
164
|
+
if (out.error) return { success: false, error: out.error }
|
|
165
|
+
return { success: true, path: dest }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
pushFile(serial, localPath, remotePath) {
|
|
169
|
+
const s = serial ? `-s ${serial}` : ""
|
|
170
|
+
const out = adb(`${s} push "${localPath}" "${remotePath}"`)
|
|
171
|
+
if (out.error) return { success: false, error: out.error }
|
|
172
|
+
return { success: true }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
reboot(serial) {
|
|
176
|
+
const s = serial ? `-s ${serial}` : ""
|
|
177
|
+
const out = adb(`${s} reboot`)
|
|
178
|
+
if (out.error) return { success: false, error: out.error }
|
|
179
|
+
return { success: true }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
logcat(serial, filter = "", lines = 50) {
|
|
183
|
+
const s = serial ? `-s ${serial}` : ""
|
|
184
|
+
const out = adb(`${s} logcat -d -t ${lines} ${filter}`)
|
|
185
|
+
if (out.error) return { success: false, error: out.error }
|
|
186
|
+
return { success: true, log: out }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
wifiConnect(ip, port = 5555) {
|
|
190
|
+
const out = adb(`connect ${ip}:${port}`)
|
|
191
|
+
if (out.error) return { success: false, error: out.error }
|
|
192
|
+
return { success: true, output: out }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
wifiDisconnect(ip) {
|
|
196
|
+
const out = adb(`disconnect ${ip}`)
|
|
197
|
+
if (out.error) return { success: false, error: out.error }
|
|
198
|
+
return { success: true, output: out }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { execSync, spawn } from "child_process"
|
|
2
|
+
import { writeFileSync, existsSync, mkdirSync } from "fs"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
import { homedir } from "os"
|
|
5
|
+
import http from "http"
|
|
6
|
+
|
|
7
|
+
const STELLA_DIR = join(homedir(), ".stella")
|
|
8
|
+
const BROWSER_DIR = join(STELLA_DIR, "browser")
|
|
9
|
+
const PROFILES_DIR = join(BROWSER_DIR, "profiles")
|
|
10
|
+
|
|
11
|
+
function ensureDir() {
|
|
12
|
+
if (!existsSync(BROWSER_DIR)) mkdirSync(BROWSER_DIR, { recursive: true })
|
|
13
|
+
if (!existsSync(PROFILES_DIR)) mkdirSync(PROFILES_DIR, { recursive: true })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function httpGet(url) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
http.get(url, (res) => {
|
|
19
|
+
let data = ""
|
|
20
|
+
res.on("data", (chunk) => data += chunk)
|
|
21
|
+
res.on("end", () => {
|
|
22
|
+
try { resolve(JSON.parse(data)) } catch { resolve(data) }
|
|
23
|
+
})
|
|
24
|
+
}).on("error", reject)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class ChromeBrowser {
|
|
29
|
+
constructor() {
|
|
30
|
+
ensureDir()
|
|
31
|
+
this.ws = null
|
|
32
|
+
this.pageId = null
|
|
33
|
+
this.debugPort = 9222
|
|
34
|
+
this.proc = null
|
|
35
|
+
this.msgId = 0
|
|
36
|
+
this.callbacks = new Map()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async launch(headless = false) {
|
|
40
|
+
const chromePaths = [
|
|
41
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
42
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
43
|
+
join(homedir(), "AppData\\Local\\Google\\Chrome\\Application\\chrome.exe"),
|
|
44
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
45
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
let chromePath = null
|
|
49
|
+
for (const p of chromePaths) {
|
|
50
|
+
if (existsSync(p)) { chromePath = p; break }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!chromePath) {
|
|
54
|
+
return { success: false, error: "Chrome/Edge not found" }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const args = [
|
|
58
|
+
`--remote-debugging-port=${this.debugPort}`,
|
|
59
|
+
"--no-first-run",
|
|
60
|
+
"--no-default-browser-check",
|
|
61
|
+
headless ? "--headless=new" : "",
|
|
62
|
+
`--user-data-dir=${join(PROFILES_DIR, "default")}`,
|
|
63
|
+
"about:blank",
|
|
64
|
+
].filter(Boolean)
|
|
65
|
+
|
|
66
|
+
this.proc = spawn(chromePath, args, { detached: true, stdio: "ignore" })
|
|
67
|
+
this.proc.unref()
|
|
68
|
+
|
|
69
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const tabs = await httpGet(`http://127.0.0.1:${this.debugPort}/json`)
|
|
73
|
+
if (tabs && tabs.length > 0) {
|
|
74
|
+
this.pageId = tabs[0].id
|
|
75
|
+
return { success: true, port: this.debugPort, pid: this.proc.pid }
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return { success: false, error: `Chrome launched but DevTools not ready: ${e.message}` }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { success: true, port: this.debugPort }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async connect() {
|
|
85
|
+
try {
|
|
86
|
+
const tabs = await httpGet(`http://127.0.0.1:${this.debugPort}/json`)
|
|
87
|
+
if (!tabs || tabs.length === 0) {
|
|
88
|
+
return { success: false, error: "No tabs found. Launch Chrome first." }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const wsUrl = tabs[0].webSocketDebuggerUrl
|
|
92
|
+
if (!wsUrl) {
|
|
93
|
+
return { success: false, error: "WebSocket URL not available" }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
this.ws = new WebSocket(wsUrl)
|
|
98
|
+
this.ws.onopen = () => {
|
|
99
|
+
this.pageId = tabs[0].id
|
|
100
|
+
resolve({ success: true, title: tabs[0].title })
|
|
101
|
+
}
|
|
102
|
+
this.ws.onmessage = (event) => {
|
|
103
|
+
const msg = JSON.parse(typeof event.data === "string" ? event.data : event.data.toString())
|
|
104
|
+
if (msg.id && this.callbacks.has(msg.id)) {
|
|
105
|
+
this.callbacks.get(msg.id)(msg)
|
|
106
|
+
this.callbacks.delete(msg.id)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.ws.onerror = (err) => {
|
|
110
|
+
resolve({ success: false, error: err.message || "WebSocket error" })
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { success: false, error: e.message }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async sendCommand(method, params = {}) {
|
|
119
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
120
|
+
const conn = await this.connect()
|
|
121
|
+
if (!conn.success) return { success: false, error: conn.error }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const id = ++this.msgId
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
this.callbacks.set(id, (msg) => {
|
|
127
|
+
if (msg.error) {
|
|
128
|
+
resolve({ success: false, error: msg.error.message })
|
|
129
|
+
} else {
|
|
130
|
+
resolve({ success: true, result: msg.result })
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
this.ws.send(JSON.stringify({ id, method, params }))
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
if (this.callbacks.has(id)) {
|
|
136
|
+
this.callbacks.delete(id)
|
|
137
|
+
resolve({ success: false, error: "Command timeout" })
|
|
138
|
+
}
|
|
139
|
+
}, 30000)
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async navigate(url) {
|
|
144
|
+
const result = await this.sendCommand("Page.navigate", { url })
|
|
145
|
+
if (result.success) await new Promise(r => setTimeout(r, 2000))
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async getPageContent() {
|
|
150
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
151
|
+
expression: "document.documentElement.outerHTML",
|
|
152
|
+
returnByValue: true,
|
|
153
|
+
})
|
|
154
|
+
return result.success ? { success: true, html: result.result?.value || "" } : result
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async getText() {
|
|
158
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
159
|
+
expression: "document.body.innerText",
|
|
160
|
+
returnByValue: true,
|
|
161
|
+
})
|
|
162
|
+
return result.success ? { success: true, text: result.result?.value || "" } : result
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async getTitle() {
|
|
166
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
167
|
+
expression: "document.title",
|
|
168
|
+
returnByValue: true,
|
|
169
|
+
})
|
|
170
|
+
return result.success ? { success: true, title: result.result?.value || "" } : result
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async screenshot(path) {
|
|
174
|
+
const result = await this.sendCommand("Page.captureScreenshot", { format: "png" })
|
|
175
|
+
if (result.success && result.result?.data) {
|
|
176
|
+
const imgPath = path || join(BROWSER_DIR, `screenshot_${Date.now()}.png`)
|
|
177
|
+
writeFileSync(imgPath, Buffer.from(result.result.data, "base64"))
|
|
178
|
+
return { success: true, path: imgPath }
|
|
179
|
+
}
|
|
180
|
+
return result
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async evaluate(expression) {
|
|
184
|
+
return await this.sendCommand("Runtime.evaluate", {
|
|
185
|
+
expression,
|
|
186
|
+
returnByValue: true,
|
|
187
|
+
awaitPromise: true,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async click(selector) {
|
|
192
|
+
const posResult = await this.sendCommand("Runtime.evaluate", {
|
|
193
|
+
expression: `(() => { const el = document.querySelector('${selector.replace(/'/g, "\\'")}'); if (!el) return null; const r = el.getBoundingClientRect(); return { x: r.x + r.width/2, y: r.y + r.height/2 }; })()`,
|
|
194
|
+
returnByValue: true,
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
if (!posResult.success || !posResult.result?.value) {
|
|
198
|
+
return { success: false, error: `Element not found: ${selector}` }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const { x, y } = posResult.result.value
|
|
202
|
+
await this.sendCommand("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 })
|
|
203
|
+
await this.sendCommand("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 })
|
|
204
|
+
return { success: true, x, y }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async type(selector, text) {
|
|
208
|
+
await this.sendCommand("Runtime.evaluate", {
|
|
209
|
+
expression: `document.querySelector('${selector.replace(/'/g, "\\'")}')?.focus()`,
|
|
210
|
+
})
|
|
211
|
+
for (const char of text) {
|
|
212
|
+
await this.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", text: char, key: char })
|
|
213
|
+
await this.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", key: char })
|
|
214
|
+
}
|
|
215
|
+
return { success: true }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async scroll(deltaX = 0, deltaY = 300) {
|
|
219
|
+
await this.sendCommand("Input.dispatchMouseEvent", { type: "mouseWheel", x: 400, y: 400, deltaX, deltaY })
|
|
220
|
+
return { success: true }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async pressKey(key) {
|
|
224
|
+
const keys = {
|
|
225
|
+
enter: { key: "Enter", code: "Enter", windowsVirtualKeyCode: 13 },
|
|
226
|
+
tab: { key: "Tab", code: "Tab", windowsVirtualKeyCode: 9 },
|
|
227
|
+
escape: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
|
|
228
|
+
backspace: { key: "Backspace", code: "Backspace", windowsVirtualKeyCode: 8 },
|
|
229
|
+
space: { key: " ", code: "Space", windowsVirtualKeyCode: 32 },
|
|
230
|
+
arrowup: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
|
|
231
|
+
arrowdown: { key: "ArrowDown", code: "ArrowDown", windowsVirtualKeyCode: 40 },
|
|
232
|
+
}
|
|
233
|
+
const k = keys[key.toLowerCase()] || { key, code: `Key${key.toUpperCase()}` }
|
|
234
|
+
await this.sendCommand("Input.dispatchKeyEvent", { type: "keyDown", ...k })
|
|
235
|
+
await this.sendCommand("Input.dispatchKeyEvent", { type: "keyUp", ...k })
|
|
236
|
+
return { success: true }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async fillForm(fields) {
|
|
240
|
+
const results = []
|
|
241
|
+
for (const { selector, value } of fields) {
|
|
242
|
+
results.push({ selector, ...(await this.type(selector, value)) })
|
|
243
|
+
}
|
|
244
|
+
return { success: true, results }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async getLinks() {
|
|
248
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
249
|
+
expression: `Array.from(document.querySelectorAll('a[href]')).map(a => ({ text: a.innerText.trim().slice(0, 100), href: a.href })).filter(a => a.text).slice(0, 50)`,
|
|
250
|
+
returnByValue: true,
|
|
251
|
+
})
|
|
252
|
+
return result.success ? { success: true, links: result.result?.value || [] } : result
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async getForms() {
|
|
256
|
+
const result = await this.sendCommand("Runtime.evaluate", {
|
|
257
|
+
expression: `Array.from(document.querySelectorAll('form')).map(f => ({ action: f.action, method: f.method, inputs: Array.from(f.querySelectorAll('input,textarea,select')).map(i => ({ name: i.name, type: i.type, placeholder: i.placeholder })) }))`,
|
|
258
|
+
returnByValue: true,
|
|
259
|
+
})
|
|
260
|
+
return result.success ? { success: true, forms: result.result?.value || [] } : result
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
close() {
|
|
264
|
+
if (this.ws) this.ws.close()
|
|
265
|
+
if (this.proc) this.proc.kill()
|
|
266
|
+
return { success: true }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
static async listTabs(port = 9222) {
|
|
270
|
+
try { return await httpGet(`http://127.0.0.1:${port}/json`) } catch { return [] }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export default ChromeBrowser
|