social-agent-cli 4.3.0 → 4.5.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 +1 -1
- package/platforms/browser/driver.ts +183 -126
package/package.json
CHANGED
|
@@ -1,58 +1,128 @@
|
|
|
1
|
-
import { execSync
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { existsSync, mkdirSync, copyFileSync } from "node:fs";
|
|
5
5
|
import { PATHS } from "../../core/config.js";
|
|
6
|
-
import { loadMap } from "../../ai/mapper.js";
|
|
6
|
+
import { loadMap, saveMap } from "../../ai/mapper.js";
|
|
7
7
|
import { pickChromeProfile, getSavedProfile } from "../../core/profiles.js";
|
|
8
|
-
import type { SelectorMap } from "../../core/types.js";
|
|
8
|
+
import type { SelectorMap, SelectorStep } from "../../core/types.js";
|
|
9
9
|
|
|
10
10
|
const CHROME_PATH = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
11
11
|
const CDP_PORT = 9222;
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Browser Driver - agent-browser + gerçek Chrome profili (CDP)
|
|
15
|
-
*/
|
|
16
13
|
export class BrowserDriver {
|
|
17
14
|
private chromeStarted = false;
|
|
18
|
-
private profileDirName = "";
|
|
19
15
|
|
|
20
16
|
constructor(private platform: string, private homeUrl: string) {}
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
* agent-browser komutu çalıştır (CDP ile)
|
|
24
|
-
*/
|
|
18
|
+
// ── agent-browser komutu ──────────────────────────────────
|
|
25
19
|
private cmd(command: string, timeout = 15000): string {
|
|
26
20
|
try {
|
|
27
21
|
return execSync(`agent-browser --cdp ${CDP_PORT} ${command}`, {
|
|
28
|
-
encoding: "utf-8",
|
|
29
|
-
timeout,
|
|
30
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
22
|
+
encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"],
|
|
31
23
|
}).trim();
|
|
32
24
|
} catch (err: any) {
|
|
33
|
-
throw new Error(err.stderr?.toString()?.trim() || err.
|
|
25
|
+
throw new Error(err.stderr?.toString()?.trim() || err.message);
|
|
34
26
|
}
|
|
35
27
|
}
|
|
36
28
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
// ── Claude'a snapshot gönder, doğru ref'i bulsun ────────
|
|
30
|
+
private askClaudeForRef(snapshot: string, description: string, action: string): string | null {
|
|
31
|
+
try {
|
|
32
|
+
// Screenshot al - Claude hem görsel hem text ile analiz etsin
|
|
33
|
+
const ssPath = join(PATHS.screenshots, `${this.platform}_ai_ref.png`);
|
|
34
|
+
try { this.cmd(`screenshot "${ssPath}"`); } catch {}
|
|
35
|
+
|
|
36
|
+
const prompt = `Screenshot: ${ssPath}
|
|
37
|
+
|
|
38
|
+
Accessibility tree:
|
|
39
|
+
${snapshot}
|
|
40
|
+
|
|
41
|
+
Görev: "${description}" (${action})
|
|
42
|
+
|
|
43
|
+
Bu snapshot'ta ve screenshot'ta yukarıdaki görevi yerine getirebilecek elementin ref değerini bul. Sadece ref değerini yaz, başka bir şey yazma. Örnek: e19`;
|
|
44
|
+
|
|
45
|
+
const result = execSync(
|
|
46
|
+
`echo '${prompt.replace(/'/g, "'\\''")}' | claude -p --model opus --effort high --max-turns 1 --tools ""`,
|
|
47
|
+
{ encoding: "utf-8", timeout: 30000, stdio: ["pipe", "pipe", "pipe"] }
|
|
48
|
+
).trim();
|
|
49
|
+
|
|
50
|
+
// ref değerini çıkar (e19, e7 gibi)
|
|
51
|
+
const refMatch = result.match(/\b(e\d+)\b/);
|
|
52
|
+
if (refMatch) {
|
|
53
|
+
console.log(` [ai] ${description} → @${refMatch[1]}`);
|
|
54
|
+
return refMatch[1];
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Snapshot al ve parse et ───────────────────────────────
|
|
61
|
+
private getSnapshot(): { raw: string; refs: Map<string, { ref: string; text: string; role: string }> } {
|
|
62
|
+
const raw = this.cmd("snapshot -i -c", 10000);
|
|
63
|
+
const refs = new Map<string, { ref: string; text: string; role: string }>();
|
|
64
|
+
|
|
65
|
+
// "button "Gönderi başlatın" [ref=e19]" → {ref: "e19", text: "Gönderi başlatın", role: "button"}
|
|
66
|
+
for (const match of raw.matchAll(/(\w+)\s+"([^"]*)"[^[]*\[ref=(\w+)\]/g)) {
|
|
67
|
+
refs.set(match[3], { ref: match[3], text: match[2], role: match[1] });
|
|
68
|
+
}
|
|
69
|
+
return { raw, refs };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Snapshot'tan element bul (text match) ─────────────────
|
|
73
|
+
private findRef(snapshot: ReturnType<typeof this.getSnapshot>, description: string, role?: string): string | null {
|
|
74
|
+
// Description'dan tırnak içi text çıkar
|
|
75
|
+
const textMatch = description.match(/["'""\u201C\u201D]([^"'""\u201C\u201D]+)["'""\u201C\u201D]/);
|
|
76
|
+
const searchText = textMatch?.[1] || "";
|
|
77
|
+
if (!searchText) return null;
|
|
78
|
+
|
|
79
|
+
for (const [ref, info] of snapshot.refs) {
|
|
80
|
+
// Tam eşleşme
|
|
81
|
+
if (info.text === searchText) {
|
|
82
|
+
if (!role || info.role === role) return ref;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Kısmi eşleşme
|
|
86
|
+
for (const [ref, info] of snapshot.refs) {
|
|
87
|
+
if (info.text.includes(searchText)) {
|
|
88
|
+
if (!role || info.role === role) return ref;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Element'in CSS selector'ını al ────────────────────────
|
|
95
|
+
private getSelector(ref: string): string {
|
|
96
|
+
try {
|
|
97
|
+
const result = this.cmd(`eval "
|
|
98
|
+
(function() {
|
|
99
|
+
var el = document.querySelector('[data-ref=\\"${ref}\\"]');
|
|
100
|
+
if (!el) {
|
|
101
|
+
// ref ile bulamadıysa snapshot'taki sırayla dene
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
// Selector üret
|
|
105
|
+
if (el.id) return '#' + el.id;
|
|
106
|
+
if (el.getAttribute('data-testid')) return '[data-testid=\\\"' + el.getAttribute('data-testid') + '\\\"]';
|
|
107
|
+
if (el.getAttribute('aria-label')) return el.tagName.toLowerCase() + '[aria-label=\\\"' + el.getAttribute('aria-label') + '\\\"]';
|
|
108
|
+
return '';
|
|
109
|
+
})()
|
|
110
|
+
"`, 5000);
|
|
111
|
+
return result.replace(/^"|"$/g, "");
|
|
112
|
+
} catch {
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Chrome başlat ─────────────────────────────────────────
|
|
40
118
|
async launch(): Promise<void> {
|
|
41
119
|
const profile = getSavedProfile(this.platform);
|
|
42
120
|
if (!profile) throw new Error(`Profil yok. Önce: social-agent login ${this.platform}`);
|
|
43
|
-
this.profileDirName = profile.dirName;
|
|
44
121
|
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
execSync(`curl -sf http://127.0.0.1:${CDP_PORT}/json/version`, { stdio: "pipe", timeout: 2000 });
|
|
48
|
-
return; // zaten çalışıyor
|
|
49
|
-
} catch {}
|
|
122
|
+
try { execSync(`curl -sf http://127.0.0.1:${CDP_PORT}/json/version`, { stdio: "pipe", timeout: 2000 }); return; } catch {}
|
|
50
123
|
|
|
51
|
-
// Chrome'u CDP ile başlat
|
|
52
124
|
const workDir = join(PATHS.profiles, "chrome-cdp");
|
|
53
125
|
mkdirSync(join(workDir, profile.dirName), { recursive: true });
|
|
54
|
-
|
|
55
|
-
// Cookie'leri kopyala
|
|
56
126
|
for (const f of ["Cookies", "Cookies-journal", "Login Data", "Login Data-journal", "Preferences", "Secure Preferences"]) {
|
|
57
127
|
const src = join(profile.fullPath, f);
|
|
58
128
|
if (existsSync(src)) { try { copyFileSync(src, join(workDir, profile.dirName, f)); } catch {} }
|
|
@@ -60,69 +130,45 @@ export class BrowserDriver {
|
|
|
60
130
|
const ls = join(homedir(), "Library", "Application Support", "Google", "Chrome", "Local State");
|
|
61
131
|
if (existsSync(ls)) { try { copyFileSync(ls, join(workDir, "Local State")); } catch {} }
|
|
62
132
|
|
|
63
|
-
|
|
64
|
-
execSync("agent-browser close", { stdio: "pipe" }).toString();
|
|
65
|
-
|
|
133
|
+
const { spawn } = await import("node:child_process");
|
|
134
|
+
execSync("agent-browser close", { stdio: "pipe" }).toString().catch?.(() => {});
|
|
66
135
|
const child = spawn(CHROME_PATH, [
|
|
67
|
-
`--remote-debugging-port=${CDP_PORT}`,
|
|
68
|
-
`--
|
|
69
|
-
`--profile-directory=${profile.dirName}`,
|
|
70
|
-
"--no-first-run", "--no-default-browser-check",
|
|
136
|
+
`--remote-debugging-port=${CDP_PORT}`, `--user-data-dir=${workDir}`,
|
|
137
|
+
`--profile-directory=${profile.dirName}`, "--no-first-run", "--no-default-browser-check",
|
|
71
138
|
], { detached: true, stdio: "ignore" });
|
|
72
139
|
child.unref();
|
|
73
|
-
this.chromeStarted = true;
|
|
74
140
|
|
|
75
|
-
// Bağlantıyı bekle
|
|
76
141
|
for (let i = 0; i < 30; i++) {
|
|
77
142
|
await new Promise(r => setTimeout(r, 1000));
|
|
78
|
-
try {
|
|
79
|
-
execSync(`curl -sf http://127.0.0.1:${CDP_PORT}/json/version`, { stdio: "pipe", timeout: 3000 });
|
|
80
|
-
console.log(`[${this.platform}] Chrome bağlandı`);
|
|
81
|
-
return;
|
|
82
|
-
} catch {}
|
|
143
|
+
try { execSync(`curl -sf http://127.0.0.1:${CDP_PORT}/json/version`, { stdio: "pipe", timeout: 2000 }); console.log(`[${this.platform}] Chrome bağlandı`); return; } catch {}
|
|
83
144
|
}
|
|
84
|
-
throw new Error("Chrome başlatılamadı
|
|
145
|
+
throw new Error("Chrome başlatılamadı");
|
|
85
146
|
}
|
|
86
147
|
|
|
87
148
|
async close(): Promise<void> {}
|
|
88
149
|
|
|
89
|
-
/**
|
|
90
|
-
* Login
|
|
91
|
-
*/
|
|
92
150
|
async login(): Promise<void> {
|
|
93
151
|
await pickChromeProfile(this.platform);
|
|
94
152
|
await this.launch();
|
|
95
153
|
this.cmd(`open "${this.homeUrl}"`, 30000);
|
|
96
|
-
|
|
97
154
|
console.log(`\n[${this.platform}] Tarayıcı açıldı: ${this.homeUrl}`);
|
|
98
155
|
const { createInterface } = await import("node:readline");
|
|
99
156
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
100
|
-
await new Promise<void>(resolve => {
|
|
101
|
-
rl.question("Hesabın açıksa Enter'a bas: ", () => { rl.close(); resolve(); });
|
|
102
|
-
});
|
|
103
|
-
console.log(`[${this.platform}] Kayıt tamamlandı.`);
|
|
157
|
+
await new Promise<void>(resolve => { rl.question("Enter'a bas: ", () => { rl.close(); resolve(); }); });
|
|
104
158
|
process.exit(0);
|
|
105
159
|
}
|
|
106
160
|
|
|
107
|
-
|
|
108
|
-
* Learn - ARIA snapshot al ve dosyaya yaz (Claude Code okuyup map oluşturur)
|
|
109
|
-
*/
|
|
110
|
-
async learn(taskDescription = "yeni bir metin postu at", actionName = "post"): Promise<SelectorMap> {
|
|
161
|
+
async learn(taskDescription = "post at", actionName = "post"): Promise<SelectorMap> {
|
|
111
162
|
await this.launch();
|
|
112
163
|
this.cmd(`open "${this.homeUrl}"`, 30000);
|
|
113
164
|
this.cmd("wait 3000");
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
this.cmd(`screenshot "${screenshotPath}"`);
|
|
117
|
-
|
|
165
|
+
const ssPath = join(PATHS.screenshots, `${this.platform}_learn.png`);
|
|
166
|
+
this.cmd(`screenshot "${ssPath}"`);
|
|
118
167
|
const snapshot = this.cmd("snapshot -i -c", 10000);
|
|
119
|
-
const
|
|
120
|
-
const { writeFileSync
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
console.log(`Screenshot: ${screenshotPath}`);
|
|
124
|
-
console.log(`Snapshot: ${snapshotPath}`);
|
|
125
|
-
console.log(`\nBu dosyaları Claude Code'a ver, map oluştursun.`);
|
|
168
|
+
const snapPath = join(PATHS.screenshots, `${this.platform}_snapshot.txt`);
|
|
169
|
+
const { writeFileSync } = await import("node:fs");
|
|
170
|
+
writeFileSync(snapPath, snapshot);
|
|
171
|
+
console.log(`Screenshot: ${ssPath}\nSnapshot: ${snapPath}`);
|
|
126
172
|
process.exit(0);
|
|
127
173
|
return {} as SelectorMap;
|
|
128
174
|
}
|
|
@@ -132,7 +178,8 @@ export class BrowserDriver {
|
|
|
132
178
|
this.cmd("wait 2000");
|
|
133
179
|
}
|
|
134
180
|
|
|
135
|
-
|
|
181
|
+
// ── Step çalıştır: önce cached selector, bozuksa snapshot+ref ──
|
|
182
|
+
executeStep(step: SelectorStep, content: string, imagePath?: string): void {
|
|
136
183
|
const val = (step.value || "").replace("{{CONTENT}}", content).replace("{{IMAGE}}", imagePath || "");
|
|
137
184
|
|
|
138
185
|
switch (step.action) {
|
|
@@ -145,48 +192,69 @@ export class BrowserDriver {
|
|
|
145
192
|
break;
|
|
146
193
|
|
|
147
194
|
case "click": {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
195
|
+
// 1. Cached selector dene
|
|
196
|
+
if (step.selector) {
|
|
197
|
+
try { this.cmd(`click "${step.selector}"`); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
|
|
198
|
+
}
|
|
199
|
+
for (const sel of step.fallbackSelectors || []) {
|
|
152
200
|
try { this.cmd(`click "${sel}"`); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
|
|
153
201
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
202
|
+
|
|
203
|
+
// 2. Selector bozuk → screenshot + snapshot al, ref ile bul
|
|
204
|
+
try { this.cmd(`screenshot "${join(PATHS.screenshots, `${this.platform}_refmatch.png`)}"`); } catch {}
|
|
205
|
+
const snapshot = this.getSnapshot();
|
|
206
|
+
const ref = this.findRef(snapshot, step.description || "", "button");
|
|
207
|
+
if (ref) {
|
|
208
|
+
this.cmd(`click @${ref}`);
|
|
209
|
+
// Yeni selector'ı cache'le
|
|
210
|
+
const newSel = this.getSelector(ref);
|
|
211
|
+
if (newSel && newSel !== step.selector) {
|
|
212
|
+
console.log(` [cache] ${step.selector || "?"} → ${newSel}`);
|
|
213
|
+
step.selector = newSel;
|
|
214
|
+
}
|
|
215
|
+
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
216
|
+
return;
|
|
167
217
|
}
|
|
168
|
-
|
|
218
|
+
|
|
219
|
+
// 3. Son çare: Claude'a sor - snapshot'tan doğru elementi bulsun
|
|
220
|
+
const aiRef = this.askClaudeForRef(snapshot.raw, step.description || "", "click");
|
|
221
|
+
if (aiRef) {
|
|
222
|
+
this.cmd(`click @${aiRef}`);
|
|
223
|
+
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
throw new Error(`Click: ${step.description}`);
|
|
169
227
|
}
|
|
170
228
|
|
|
171
229
|
case "type": {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
// 1.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
this.cmd(`type "${sel}" "${escaped}"`, 30000);
|
|
179
|
-
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
180
|
-
return;
|
|
181
|
-
} catch {}
|
|
230
|
+
const escaped = val.replace(/"/g, '\\"');
|
|
231
|
+
|
|
232
|
+
// 1. Cached selector dene
|
|
233
|
+
if (step.selector) {
|
|
234
|
+
try { this.cmd(`type "${step.selector}" "${escaped}"`, 30000); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
|
|
182
235
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
236
|
+
for (const sel of step.fallbackSelectors || []) {
|
|
237
|
+
try { this.cmd(`type "${sel}" "${escaped}"`, 30000); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 2. Screenshot + snapshot → ref ile textbox bul
|
|
241
|
+
try { this.cmd(`screenshot "${join(PATHS.screenshots, `${this.platform}_refmatch.png`)}"`); } catch {}
|
|
242
|
+
const snapshot = this.getSnapshot();
|
|
243
|
+
const ref = this.findRef(snapshot, step.description || "", "textbox");
|
|
244
|
+
if (ref) {
|
|
245
|
+
this.cmd(`type @${ref} "${escaped}"`, 30000);
|
|
186
246
|
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
187
247
|
return;
|
|
188
|
-
}
|
|
189
|
-
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 3. Claude'a sor
|
|
251
|
+
const aiRef = this.askClaudeForRef(snapshot.raw, step.description || "", "type");
|
|
252
|
+
if (aiRef) {
|
|
253
|
+
this.cmd(`type @${aiRef} "${escaped}"`, 30000);
|
|
254
|
+
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
throw new Error(`Type: ${step.description}`);
|
|
190
258
|
}
|
|
191
259
|
|
|
192
260
|
case "keypress":
|
|
@@ -198,46 +266,35 @@ export class BrowserDriver {
|
|
|
198
266
|
const filePath = val || imagePath || "";
|
|
199
267
|
if (!filePath || filePath.includes("{{")) break;
|
|
200
268
|
|
|
201
|
-
// 1.
|
|
202
|
-
try {
|
|
203
|
-
this.cmd(`upload "input[type='file']" "${filePath}"`, 5000);
|
|
204
|
-
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
205
|
-
return;
|
|
206
|
-
} catch {}
|
|
269
|
+
// 1. input[type=file] direkt (X'te çalışır)
|
|
270
|
+
try { this.cmd(`upload "input[type='file']" "${filePath}"`, 5000); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
|
|
207
271
|
|
|
208
|
-
// 2. Playwright
|
|
209
|
-
// Snapshot'tan medya
|
|
210
|
-
let btnSel = "button[aria-label
|
|
272
|
+
// 2. Playwright helper (LinkedIn gibi native dialog için)
|
|
273
|
+
// Snapshot'tan medya butonunu bul
|
|
274
|
+
let btnSel = "button[aria-label='Medya ekle']";
|
|
211
275
|
try {
|
|
212
|
-
const snap = this.
|
|
213
|
-
const
|
|
214
|
-
|
|
276
|
+
const snap = this.getSnapshot();
|
|
277
|
+
for (const [ref, info] of snap.refs) {
|
|
278
|
+
if (info.text.match(/medya|fotoğraf|photo|media|video/i) && info.role === "button") {
|
|
279
|
+
btnSel = `button[aria-label='${info.text}']`;
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
215
283
|
} catch {}
|
|
284
|
+
|
|
216
285
|
try {
|
|
217
286
|
const thisDir = new URL(".", import.meta.url).pathname;
|
|
218
287
|
const pkgDir = process.env.SOCIAL_AGENT_PKG || join(thisDir, "../..");
|
|
219
|
-
|
|
220
|
-
const helperResult = execSync(
|
|
288
|
+
const result = execSync(
|
|
221
289
|
`npx tsx "${join(pkgDir, 'core/upload-helper.ts')}" ${CDP_PORT} "${btnSel}" "${filePath}"`,
|
|
222
290
|
{ encoding: "utf-8", timeout: 60000 }
|
|
223
291
|
).trim();
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
} catch (e: any) {
|
|
228
|
-
console.log(` [upload] helper hata: ${e.message?.substring(0, 80)}`);
|
|
229
|
-
}
|
|
230
|
-
|
|
292
|
+
if (result === "OK") return;
|
|
293
|
+
} catch {}
|
|
231
294
|
throw new Error(`Upload: ${filePath}`);
|
|
232
295
|
}
|
|
233
296
|
}
|
|
234
297
|
}
|
|
235
298
|
|
|
236
|
-
|
|
237
|
-
try { return this.cmd("snapshot -i -c", 10000); } catch { return ""; }
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
takeScreenshot(path: string): void {
|
|
241
|
-
try { this.cmd(`screenshot "${path}"`); } catch {}
|
|
242
|
-
}
|
|
299
|
+
takeScreenshot(path: string): void { try { this.cmd(`screenshot "${path}"`); } catch {} }
|
|
243
300
|
}
|