social-agent-cli 2.2.3 → 2.3.1
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/ai/mapper.ts +29 -20
- package/ai/runner.ts +80 -256
- package/package.json +1 -1
- package/core/page-watcher.ts +0 -172
- package/core/reflexes.ts +0 -187
package/ai/mapper.ts
CHANGED
|
@@ -350,30 +350,39 @@ GÖREV: Ekranın mevcut durumunu analiz et. Amaca ulaşmak için kalan adımlar
|
|
|
350
350
|
|
|
351
351
|
console.log(`\n[heal] AI ekranı analiz ediyor (effort: ${effort})...\n`);
|
|
352
352
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
353
|
+
let result: any;
|
|
354
|
+
try {
|
|
355
|
+
result = await claude({
|
|
356
|
+
prompt,
|
|
357
|
+
jsonSchema: REMAINING_STEPS_SCHEMA,
|
|
358
|
+
imagePath: screenshotPath,
|
|
359
|
+
stream: false,
|
|
360
|
+
effort,
|
|
361
|
+
});
|
|
362
|
+
} catch (err: any) {
|
|
363
|
+
console.log(`\n[heal] AI hatası: ${err.message}`);
|
|
364
|
+
return originalSteps.slice(failedStepIndex + 1); // başarısız step'i atla
|
|
365
|
+
}
|
|
360
366
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return originalSteps.slice(failedStepIndex);
|
|
367
|
+
// result çeşitli formatlarda gelebilir
|
|
368
|
+
if (!result) {
|
|
369
|
+
return originalSteps.slice(failedStepIndex + 1);
|
|
364
370
|
}
|
|
365
371
|
|
|
366
|
-
|
|
367
|
-
|
|
372
|
+
// structured_output içinde olabilir
|
|
373
|
+
if (result.structured_output?.remainingSteps) {
|
|
374
|
+
result = result.structured_output;
|
|
375
|
+
}
|
|
368
376
|
|
|
369
|
-
//
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
saveMap(currentMap);
|
|
377
|
+
// remainingSteps yoksa steps dene
|
|
378
|
+
const steps = result.remainingSteps || result.steps || result;
|
|
379
|
+
|
|
380
|
+
if (!Array.isArray(steps)) {
|
|
381
|
+
console.log(`\n[heal] Plan parse edilemedi, başarısız adım atlanıyor.`);
|
|
382
|
+
return originalSteps.slice(failedStepIndex + 1);
|
|
376
383
|
}
|
|
377
384
|
|
|
378
|
-
|
|
385
|
+
console.log(`\n[heal] ${steps.length} yeni adım`);
|
|
386
|
+
|
|
387
|
+
return steps;
|
|
379
388
|
}
|
package/ai/runner.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { BrowserDriver } from "../platforms/browser/driver.js";
|
|
2
|
-
import { loadMap } from "./mapper.js";
|
|
3
|
-
import { createPlan
|
|
4
|
-
import { healAndContinue } from "./mapper.js";
|
|
2
|
+
import { loadMap, healAndContinue } from "./mapper.js";
|
|
3
|
+
import { createPlan } from "./planner.js";
|
|
5
4
|
import { saveToHistory } from "../core/history.js";
|
|
6
|
-
import {
|
|
7
|
-
import { tryReflex } from "../core/reflexes.js";
|
|
8
|
-
import type { Post, PostResult } from "../core/types.js";
|
|
5
|
+
import type { Post } from "../core/types.js";
|
|
9
6
|
import { join } from "node:path";
|
|
10
7
|
import { PATHS } from "../core/config.js";
|
|
11
8
|
|
|
@@ -14,62 +11,35 @@ const BROWSER_PLATFORMS: Record<string, string> = {
|
|
|
14
11
|
linkedin: "https://www.linkedin.com",
|
|
15
12
|
};
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
* Doğal dil komutunu planla ve çalıştır.
|
|
19
|
-
*/
|
|
20
|
-
export async function runCommand(
|
|
21
|
-
platform: string,
|
|
22
|
-
command: string,
|
|
23
|
-
autoConfirm = false
|
|
24
|
-
): Promise<void> {
|
|
14
|
+
export async function runCommand(platform: string, command: string): Promise<void> {
|
|
25
15
|
const url = BROWSER_PLATFORMS[platform];
|
|
26
|
-
if (!url) {
|
|
27
|
-
console.log(`"${platform}" browser platformu değil.`);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
16
|
+
if (!url) { console.log(`"${platform}" desteklenmiyor.`); return; }
|
|
30
17
|
|
|
31
|
-
// 1. Plan oluştur
|
|
32
18
|
const plan = await createPlan(platform, command);
|
|
33
|
-
|
|
34
|
-
// 2. Onay al
|
|
35
|
-
// Onay sormadan çalıştır - Claude Code zaten kullanıcıdan onay almıştır
|
|
36
|
-
console.log("");
|
|
37
|
-
|
|
38
|
-
// 3. Adımları çalıştır
|
|
39
|
-
console.log("\n[runner] Plan çalıştırılıyor...\n");
|
|
19
|
+
console.log("\n[runner] Çalıştırılıyor...\n");
|
|
40
20
|
|
|
41
21
|
for (const [i, step] of plan.steps.entries()) {
|
|
42
|
-
console.log(`━━━
|
|
22
|
+
console.log(`━━━ ${i + 1}/${plan.steps.length}: ${step.description} ━━━`);
|
|
43
23
|
|
|
44
24
|
if (step.type === "learn_new") {
|
|
45
|
-
console.log(`[runner] Yeni aksiyon öğreniliyor: "${step.learnDescription}"`);
|
|
46
25
|
const driver = new BrowserDriver(platform, url);
|
|
47
|
-
await driver.learn(
|
|
48
|
-
step.learnDescription || step.description,
|
|
49
|
-
step.learnActionName || "custom"
|
|
50
|
-
);
|
|
26
|
+
await driver.learn(step.learnDescription || step.description, step.learnActionName || "custom");
|
|
51
27
|
console.log(`[runner] Öğrenildi!\n`);
|
|
52
28
|
}
|
|
53
29
|
|
|
54
|
-
const mapAction = step.type === "use_map"
|
|
55
|
-
? step.mapAction!
|
|
56
|
-
: step.learnActionName || "custom";
|
|
57
|
-
|
|
30
|
+
const mapAction = step.type === "use_map" ? step.mapAction! : step.learnActionName || "custom";
|
|
58
31
|
const map = loadMap(platform, mapAction);
|
|
59
|
-
if (!map) {
|
|
60
|
-
console.log(`[runner] HATA: "${mapAction}" map'i bulunamadı, atlanıyor.`);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
32
|
+
if (!map) { console.log(`"${mapAction}" bulunamadı, atlanıyor.`); continue; }
|
|
63
33
|
|
|
64
34
|
const repeatCount = step.repeat || 1;
|
|
65
35
|
|
|
66
36
|
for (let r = 0; r < repeatCount; r++) {
|
|
67
37
|
if (repeatCount > 1) console.log(` [${r + 1}/${repeatCount}]`);
|
|
68
38
|
|
|
69
|
-
const
|
|
39
|
+
const params = { ...step.parameters };
|
|
70
40
|
const post: Post = {
|
|
71
|
-
content:
|
|
72
|
-
images:
|
|
41
|
+
content: params["{{CONTENT}}"] || params["CONTENT"] || "",
|
|
42
|
+
images: (params["{{IMAGE}}"] || params["IMAGE"]) ? [params["{{IMAGE}}"] || params["IMAGE"]] : undefined,
|
|
73
43
|
platform,
|
|
74
44
|
};
|
|
75
45
|
|
|
@@ -77,304 +47,158 @@ export async function runCommand(
|
|
|
77
47
|
const page = await driver.launch();
|
|
78
48
|
|
|
79
49
|
try {
|
|
80
|
-
// Map'in ilk adımı goto değilse platform ana sayfasına git
|
|
81
50
|
if (map.steps[0]?.action !== "goto") {
|
|
82
|
-
console.log(` [${platform}] ${url} açılıyor...`);
|
|
83
51
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
84
52
|
await page.waitForTimeout(3000);
|
|
85
53
|
}
|
|
86
54
|
|
|
87
|
-
// Periferik görüş - MutationObserver enjekte et
|
|
88
|
-
await injectWatcher(page);
|
|
89
|
-
|
|
90
55
|
let steps = [...map.steps];
|
|
91
|
-
let
|
|
92
|
-
let healAttempt = 0;
|
|
56
|
+
let healCount = 0;
|
|
93
57
|
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
// Placeholder'ları doldur - boş kalanları akıllıca handle et
|
|
98
|
-
const resolvedParams = resolveParams(step.parameters, r);
|
|
58
|
+
for (let idx = 0; idx < steps.length; idx++) {
|
|
59
|
+
const s = steps[idx];
|
|
60
|
+
const rp = (t: string) => replacePlaceholders(t, post, params);
|
|
99
61
|
|
|
100
62
|
try {
|
|
101
|
-
|
|
102
|
-
let gotoUrl = mapStep.url || url;
|
|
103
|
-
gotoUrl = replacePlaceholders(gotoUrl, post, resolvedParams);
|
|
63
|
+
console.log(` ${idx + 1}. ${s.description}`);
|
|
104
64
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
} else {
|
|
110
|
-
console.log(` Step ${stepIdx + 1}: ${mapStep.description}`);
|
|
111
|
-
await page.goto(gotoUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
112
|
-
}
|
|
113
|
-
if (mapStep.waitMs) await page.waitForTimeout(mapStep.waitMs);
|
|
65
|
+
if (s.action === "goto") {
|
|
66
|
+
let gotoUrl = rp(s.url || url);
|
|
67
|
+
if (gotoUrl.includes("{{")) gotoUrl = url;
|
|
68
|
+
await page.goto(gotoUrl, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
114
69
|
} else {
|
|
115
|
-
|
|
116
|
-
await executeMapStep(page, mapStep, post, resolvedParams);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
stepIdx++;
|
|
120
|
-
healAttempt = 0;
|
|
121
|
-
|
|
122
|
-
// Aksiyon-algı döngüsü: step sonrası beklenmeyen değişiklik var mı?
|
|
123
|
-
await page.waitForTimeout(300);
|
|
124
|
-
const change = await hasUnexpectedChange(page);
|
|
125
|
-
if (change.changed) {
|
|
126
|
-
console.log(` ⚡ ${change.description}`);
|
|
127
|
-
|
|
128
|
-
// Sistem 1: Refleks - hızlı, otomatik
|
|
129
|
-
const reflex = await tryReflex(page, change.description);
|
|
130
|
-
if (reflex.handled) {
|
|
131
|
-
console.log(` ↩ Refleks: ${reflex.action}`);
|
|
132
|
-
} else {
|
|
133
|
-
// Sistem 2: AI - yavaş, bilinçli (sadece refleks çözemediğinde)
|
|
134
|
-
console.log(` 🧠 Refleks çözemedi, AI devreye giriyor...`);
|
|
135
|
-
const healScreenshot = join(PATHS.screenshots, `${platform}_unexpected.png`);
|
|
136
|
-
await page.screenshot({ path: healScreenshot });
|
|
137
|
-
const ariaSnap = await page.locator('body').ariaSnapshot();
|
|
138
|
-
|
|
139
|
-
const newSteps = await healAndContinue(
|
|
140
|
-
platform, stepIdx, steps, healScreenshot, ariaSnap,
|
|
141
|
-
`Beklenmeyen değişiklik: ${change.description}. Bu element'i handle edip kalan adımlara devam et.`,
|
|
142
|
-
post.content || step.description, "low"
|
|
143
|
-
);
|
|
144
|
-
steps = [...steps.slice(0, stepIdx), ...newSteps];
|
|
145
|
-
}
|
|
70
|
+
await executeStep(page, s, post, params);
|
|
146
71
|
}
|
|
72
|
+
if (s.waitMs) await page.waitForTimeout(s.waitMs);
|
|
147
73
|
} catch (err: any) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const reflex = await tryReflex(page, err.message);
|
|
152
|
-
if (reflex.handled) {
|
|
153
|
-
console.log(` ↩ Refleks: ${reflex.action}`);
|
|
154
|
-
continue; // aynı step'i tekrar dene
|
|
74
|
+
if (healCount >= 2) {
|
|
75
|
+
console.log(` ✗ ${err.message} (atlanıyor)`);
|
|
76
|
+
continue;
|
|
155
77
|
}
|
|
78
|
+
healCount++;
|
|
79
|
+
console.log(` ✗ ${err.message}`);
|
|
80
|
+
console.log(` → Düzeltiliyor...`);
|
|
156
81
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const effort = efforts[Math.min(healAttempt - 1, efforts.length - 1)];
|
|
161
|
-
|
|
162
|
-
console.log(` 🧠 AI düzeltiyor (deneme ${healAttempt}/3)...`);
|
|
163
|
-
|
|
164
|
-
if (healAttempt > 3) {
|
|
165
|
-
console.log(` 3 denemede çözülemedi, atlanıyor.`);
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const healScreenshot = join(PATHS.screenshots, `${platform}_heal_${stepIdx}.png`);
|
|
170
|
-
await page.screenshot({ path: healScreenshot });
|
|
171
|
-
|
|
172
|
-
const ariaSnap = await page.locator('body').ariaSnapshot();
|
|
82
|
+
const ss = join(PATHS.screenshots, `${platform}_heal.png`);
|
|
83
|
+
await page.screenshot({ path: ss }).catch(() => {});
|
|
84
|
+
const aria = await page.locator("body").ariaSnapshot().catch(() => "");
|
|
173
85
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
stepIdx,
|
|
178
|
-
steps,
|
|
179
|
-
healScreenshot,
|
|
180
|
-
ariaSnap,
|
|
181
|
-
err.message,
|
|
182
|
-
post.content || step.description,
|
|
183
|
-
effort
|
|
86
|
+
const fixed = await healAndContinue(
|
|
87
|
+
platform, idx, steps, ss, aria as string,
|
|
88
|
+
err.message, post.content || step.description, "medium"
|
|
184
89
|
);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// stepIdx artırma - yeni planı deneyecek
|
|
90
|
+
steps = [...steps.slice(0, idx), ...fixed];
|
|
91
|
+
idx--;
|
|
188
92
|
}
|
|
189
93
|
}
|
|
190
94
|
|
|
191
95
|
console.log(` ✓ Tamamlandı`);
|
|
192
|
-
saveToHistory({
|
|
193
|
-
platform,
|
|
194
|
-
content: post.content || step.description,
|
|
195
|
-
action: mapAction.includes("post") ? "post" : mapAction,
|
|
196
|
-
success: true,
|
|
197
|
-
});
|
|
96
|
+
saveToHistory({ platform, content: post.content || step.description, action: mapAction.includes("post") ? "post" : mapAction, success: true });
|
|
198
97
|
} catch (err: any) {
|
|
199
|
-
console.log(` ✗
|
|
200
|
-
saveToHistory({
|
|
201
|
-
platform,
|
|
202
|
-
content: post.content || step.description,
|
|
203
|
-
action: mapAction.includes("post") ? "post" : mapAction,
|
|
204
|
-
success: false,
|
|
205
|
-
error: err.message,
|
|
206
|
-
});
|
|
98
|
+
console.log(` ✗ ${err.message}`);
|
|
99
|
+
saveToHistory({ platform, content: post.content || step.description, action: mapAction, success: false, error: err.message });
|
|
207
100
|
} finally {
|
|
208
101
|
await driver.close();
|
|
209
102
|
}
|
|
210
103
|
|
|
211
|
-
// Tekrarlar arası rastgele bekleme
|
|
212
104
|
if (r < repeatCount - 1) {
|
|
213
105
|
const delay = 2000 + Math.random() * 3000;
|
|
214
|
-
|
|
215
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
106
|
+
await new Promise((res) => setTimeout(res, delay));
|
|
216
107
|
}
|
|
217
108
|
}
|
|
218
|
-
|
|
219
109
|
console.log("");
|
|
220
110
|
}
|
|
221
111
|
|
|
222
|
-
console.log("[runner]
|
|
112
|
+
console.log("[runner] Tamamlandı.");
|
|
223
113
|
process.exit(0);
|
|
224
114
|
}
|
|
225
115
|
|
|
226
|
-
/**
|
|
227
|
-
* Parametreleri çözümle - boş olanları repeat index'e göre doldur
|
|
228
|
-
*/
|
|
229
|
-
function resolveParams(params: Record<string, string>, repeatIndex: number): Record<string, string> {
|
|
230
|
-
const resolved: Record<string, string> = {};
|
|
231
|
-
for (const [key, val] of Object.entries(params)) {
|
|
232
|
-
resolved[key] = val || "";
|
|
233
|
-
}
|
|
234
|
-
// Repeat index'i ekle (nth-child selector'ları için)
|
|
235
|
-
resolved["{{INDEX}}"] = String(repeatIndex + 1);
|
|
236
|
-
resolved["INDEX"] = String(repeatIndex + 1);
|
|
237
|
-
return resolved;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* Placeholder'ları değiştir
|
|
242
|
-
*/
|
|
243
116
|
function replacePlaceholders(text: string, post: Post, params: Record<string, string>): string {
|
|
244
117
|
if (!text) return text;
|
|
245
|
-
let
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const placeholder = key.startsWith("{{") ? key : `{{${key}}}`;
|
|
249
|
-
result = result.replace(placeholder, val);
|
|
118
|
+
let r = text.replace("{{CONTENT}}", post.content || "");
|
|
119
|
+
for (const [k, v] of Object.entries(params)) {
|
|
120
|
+
r = r.replace(k.startsWith("{{") ? k : `{{${k}}}`, v);
|
|
250
121
|
}
|
|
251
|
-
return
|
|
122
|
+
return r;
|
|
252
123
|
}
|
|
253
124
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
async function executeMapStep(
|
|
258
|
-
page: any,
|
|
259
|
-
step: any,
|
|
260
|
-
post: Post,
|
|
261
|
-
params: Record<string, string>
|
|
262
|
-
): Promise<void> {
|
|
263
|
-
function rp(text: string): string {
|
|
264
|
-
return replacePlaceholders(text, post, params);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const selectors = [step.selector, ...(step.fallbackSelectors || [])]
|
|
268
|
-
.filter(Boolean)
|
|
269
|
-
.map((s: string) => rp(s));
|
|
125
|
+
async function executeStep(page: any, step: any, post: Post, params: Record<string, string>): Promise<void> {
|
|
126
|
+
const rp = (t: string) => replacePlaceholders(t, post, params);
|
|
127
|
+
const selectors = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean).map((s: string) => rp(s));
|
|
270
128
|
|
|
271
129
|
switch (step.action) {
|
|
272
|
-
case "goto":
|
|
273
|
-
await page.goto(rp(step.url), { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
274
|
-
if (step.waitMs) await page.waitForTimeout(step.waitMs);
|
|
275
|
-
break;
|
|
276
|
-
|
|
277
130
|
case "wait":
|
|
278
131
|
await page.waitForTimeout(step.waitMs || 1000);
|
|
279
132
|
break;
|
|
280
133
|
|
|
281
134
|
case "click": {
|
|
282
|
-
let clicked = false;
|
|
283
|
-
|
|
284
|
-
// Önce normal selector'ları dene
|
|
285
135
|
for (const sel of selectors) {
|
|
286
|
-
// Hâlâ çözümlenmemiş placeholder varsa atla
|
|
287
136
|
if (sel.includes("{{")) continue;
|
|
288
137
|
try {
|
|
289
138
|
await page.waitForSelector(sel, { timeout: 5000 });
|
|
290
139
|
await page.click(sel);
|
|
291
|
-
|
|
292
|
-
break;
|
|
140
|
+
return;
|
|
293
141
|
} catch {}
|
|
294
142
|
}
|
|
295
|
-
|
|
296
|
-
// Hiçbiri çalışmadıysa hata fırlat → heal mekanizması devreye girecek
|
|
297
|
-
// AI ekranı görüp doğru butonu bulacak
|
|
298
|
-
if (!clicked) throw new Error(`Click failed: ${selectors.join(", ")}`);
|
|
299
|
-
if (step.waitMs) await page.waitForTimeout(step.waitMs);
|
|
300
|
-
break;
|
|
143
|
+
throw new Error(`Click: ${selectors[0]}`);
|
|
301
144
|
}
|
|
302
145
|
|
|
303
146
|
case "type": {
|
|
304
147
|
const text = rp(step.value || "");
|
|
305
|
-
let typed = false;
|
|
306
148
|
for (const sel of selectors) {
|
|
307
149
|
if (sel.includes("{{")) continue;
|
|
308
150
|
try {
|
|
309
151
|
await page.waitForSelector(sel, { timeout: 5000 });
|
|
310
|
-
const
|
|
311
|
-
el.getAttribute("contenteditable") === "true" ||
|
|
312
|
-
el.getAttribute("role") === "textbox"
|
|
152
|
+
const ce = await page.$eval(sel, (el: Element) =>
|
|
153
|
+
el.getAttribute("contenteditable") === "true" || el.getAttribute("role") === "textbox"
|
|
313
154
|
).catch(() => false);
|
|
314
|
-
|
|
315
|
-
if (isContentEditable) {
|
|
155
|
+
if (ce) {
|
|
316
156
|
await page.click(sel);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (el) {
|
|
321
|
-
el.focus();
|
|
322
|
-
document.execCommand("insertText", false, content);
|
|
323
|
-
}
|
|
157
|
+
await page.evaluate(([s, t]: string[]) => {
|
|
158
|
+
const el = document.querySelector(s);
|
|
159
|
+
if (el) { (el as HTMLElement).focus(); document.execCommand("insertText", false, t); }
|
|
324
160
|
}, [sel, text]);
|
|
325
161
|
} else {
|
|
326
162
|
await page.fill(sel, text);
|
|
327
163
|
}
|
|
328
|
-
|
|
329
|
-
break;
|
|
164
|
+
return;
|
|
330
165
|
} catch {}
|
|
331
166
|
}
|
|
332
|
-
|
|
333
|
-
if (step.waitMs) await page.waitForTimeout(step.waitMs);
|
|
334
|
-
break;
|
|
167
|
+
throw new Error(`Type: ${selectors[0]}`);
|
|
335
168
|
}
|
|
336
169
|
|
|
337
170
|
case "keypress":
|
|
338
171
|
await page.keyboard.press(step.key || "Enter");
|
|
339
|
-
if (step.waitMs) await page.waitForTimeout(step.waitMs);
|
|
340
172
|
break;
|
|
341
173
|
|
|
342
174
|
case "upload": {
|
|
343
175
|
const filePath = rp(step.value || "{{IMAGE}}");
|
|
344
|
-
if (!filePath || filePath.includes("{{")) break;
|
|
345
|
-
|
|
346
|
-
let uploaded = false;
|
|
176
|
+
if (!filePath || filePath.includes("{{")) break;
|
|
347
177
|
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
for (const
|
|
352
|
-
try {
|
|
353
|
-
const fileChooserPromise = page.waitForEvent("filechooser", { timeout: 5000 });
|
|
354
|
-
await page.click(sel).catch(() => {});
|
|
355
|
-
const fileChooser = await fileChooserPromise;
|
|
356
|
-
await fileChooser.setFiles(filePath);
|
|
357
|
-
uploaded = true;
|
|
358
|
-
break;
|
|
359
|
-
} catch {
|
|
360
|
-
// fileChooser timeout veya click hatası - sonraki selector'ı dene
|
|
361
|
-
}
|
|
178
|
+
// 1. input[type=file] direkt (file picker açmaz)
|
|
179
|
+
try {
|
|
180
|
+
const inputs = await page.locator('input[type="file"]').all();
|
|
181
|
+
for (const input of inputs) {
|
|
182
|
+
try { await input.setInputFiles(filePath); return; } catch {}
|
|
362
183
|
}
|
|
363
|
-
}
|
|
184
|
+
} catch {}
|
|
364
185
|
|
|
365
|
-
//
|
|
366
|
-
|
|
186
|
+
// 2. fileChooser ile butona tıkla
|
|
187
|
+
for (const sel of selectors) {
|
|
367
188
|
try {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
189
|
+
const fc = await Promise.race([
|
|
190
|
+
(async () => {
|
|
191
|
+
const fcp = page.waitForEvent("filechooser", { timeout: 4000 });
|
|
192
|
+
await page.click(sel);
|
|
193
|
+
return await fcp;
|
|
194
|
+
})(),
|
|
195
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 5000)),
|
|
196
|
+
]) as any;
|
|
197
|
+
await fc.setFiles(filePath);
|
|
198
|
+
return;
|
|
372
199
|
} catch {}
|
|
373
200
|
}
|
|
374
|
-
|
|
375
|
-
if (!uploaded) throw new Error(`Upload failed: ${filePath}`);
|
|
376
|
-
if (step.waitMs) await page.waitForTimeout(step.waitMs);
|
|
377
|
-
break;
|
|
201
|
+
throw new Error(`Upload: ${filePath}`);
|
|
378
202
|
}
|
|
379
203
|
}
|
|
380
204
|
}
|
package/package.json
CHANGED
package/core/page-watcher.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Page Watcher - İnsan beyni modeliyle sayfa izleme
|
|
3
|
-
*
|
|
4
|
-
* Beyin modeli:
|
|
5
|
-
* 1. Periferik görüş → MutationObserver ile DOM değişikliklerini sürekli izle
|
|
6
|
-
* 2. Değişiklik algılama → Yeni dialog/modal/overlay çıktığında algıla
|
|
7
|
-
* 3. Dikkat yönlendirme → Beklenmeyen element → otomatik odaklan
|
|
8
|
-
* 4. Aksiyon-algı döngüsü → Her step sonrası durumu doğrula
|
|
9
|
-
* 5. Tahmin → Beklenen sonuçla gerçeği karşılaştır
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type { Page } from "playwright";
|
|
13
|
-
|
|
14
|
-
export interface PageState {
|
|
15
|
-
hasNewDialog: boolean;
|
|
16
|
-
hasNewOverlay: boolean;
|
|
17
|
-
hasError: boolean;
|
|
18
|
-
newElements: string[]; // yeni eklenen önemli elementlerin açıklamaları
|
|
19
|
-
ariaSnapshot: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Sayfaya MutationObserver enjekte et - periferik görüş
|
|
24
|
-
* DOM'a eklenen dialog, modal, overlay, alert gibi elementleri yakalar
|
|
25
|
-
*/
|
|
26
|
-
export async function injectWatcher(page: Page): Promise<void> {
|
|
27
|
-
await page.evaluate(`(() => {
|
|
28
|
-
if (window.__socialAgentWatcher) return;
|
|
29
|
-
window.__socialAgentWatcher = {
|
|
30
|
-
changes: [],
|
|
31
|
-
newDialogs: [],
|
|
32
|
-
newOverlays: [],
|
|
33
|
-
errors: [],
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const watcher = window.__socialAgentWatcher;
|
|
37
|
-
|
|
38
|
-
const observer = new MutationObserver((mutations) => {
|
|
39
|
-
for (const mutation of mutations) {
|
|
40
|
-
for (const node of mutation.addedNodes) {
|
|
41
|
-
if (!(node instanceof HTMLElement)) continue;
|
|
42
|
-
|
|
43
|
-
const role = node.getAttribute('role');
|
|
44
|
-
const ariaModal = node.getAttribute('aria-modal');
|
|
45
|
-
const tag = node.tagName.toLowerCase();
|
|
46
|
-
|
|
47
|
-
// Dialog/Modal algılama - standart + heuristik
|
|
48
|
-
const isDialog = role === 'dialog' || role === 'alertdialog' || ariaModal === 'true' || tag === 'dialog';
|
|
49
|
-
const style = window.getComputedStyle(node);
|
|
50
|
-
const isFloating = (style.position === 'fixed' || style.position === 'absolute') && style.zIndex && parseInt(style.zIndex) > 50;
|
|
51
|
-
const cls = (node.className?.toString?.() || '').toLowerCase();
|
|
52
|
-
const looksLikeModal = cls.match(/modal|dialog|popup|overlay|drawer|sheet|toast|snackbar|dropdown|popover/);
|
|
53
|
-
|
|
54
|
-
if (isDialog || (isFloating && looksLikeModal)) {
|
|
55
|
-
watcher.newDialogs.push({
|
|
56
|
-
role: role || tag,
|
|
57
|
-
ariaLabel: node.getAttribute('aria-label') || '',
|
|
58
|
-
text: node.textContent?.substring(0, 200) || '',
|
|
59
|
-
className: cls.substring(0, 100),
|
|
60
|
-
time: Date.now()
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Overlay/backdrop algılama
|
|
65
|
-
if (isFloating && !isDialog && !looksLikeModal) {
|
|
66
|
-
const area = node.offsetWidth * node.offsetHeight;
|
|
67
|
-
const screenArea = window.innerWidth * window.innerHeight;
|
|
68
|
-
// Ekranın %30'undan büyük fixed element = muhtemelen overlay
|
|
69
|
-
if (area > screenArea * 0.3) {
|
|
70
|
-
watcher.newOverlays.push({
|
|
71
|
-
tag: tag,
|
|
72
|
-
class: cls.substring(0, 100),
|
|
73
|
-
time: Date.now()
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Error mesajı algılama - standart + heuristik
|
|
79
|
-
const isError = role === 'alert' || node.getAttribute('aria-live') === 'assertive';
|
|
80
|
-
const looksLikeError = cls.match(/error|warning|alert|danger|notification|banner/);
|
|
81
|
-
if (isError || looksLikeError) {
|
|
82
|
-
watcher.errors.push({
|
|
83
|
-
text: node.textContent?.substring(0, 300) || '',
|
|
84
|
-
time: Date.now()
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
observer.observe(document.body, {
|
|
92
|
-
childList: true,
|
|
93
|
-
subtree: true,
|
|
94
|
-
});
|
|
95
|
-
})()`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Sayfanın mevcut durumunu oku - değişiklik algılama
|
|
100
|
-
*/
|
|
101
|
-
export async function getPageState(page: Page): Promise<PageState> {
|
|
102
|
-
const watcherData = await page.evaluate(`(() => {
|
|
103
|
-
const w = window.__socialAgentWatcher;
|
|
104
|
-
if (!w) return { newDialogs: [], newOverlays: [], errors: [] };
|
|
105
|
-
const data = {
|
|
106
|
-
newDialogs: [...w.newDialogs],
|
|
107
|
-
newOverlays: [...w.newOverlays],
|
|
108
|
-
errors: [...w.errors],
|
|
109
|
-
};
|
|
110
|
-
// Reset - bir kere okuduktan sonra temizle
|
|
111
|
-
w.newDialogs = [];
|
|
112
|
-
w.newOverlays = [];
|
|
113
|
-
w.errors = [];
|
|
114
|
-
return data;
|
|
115
|
-
})()`).catch(() => ({ newDialogs: [], newOverlays: [], errors: [] })) as any;
|
|
116
|
-
|
|
117
|
-
const newElements: string[] = [];
|
|
118
|
-
|
|
119
|
-
for (const d of watcherData.newDialogs) {
|
|
120
|
-
newElements.push(`[dialog] ${d.ariaLabel || d.text.substring(0, 50)}`);
|
|
121
|
-
}
|
|
122
|
-
for (const o of watcherData.newOverlays) {
|
|
123
|
-
newElements.push(`[overlay] ${o.tag}.${o.class.substring(0, 30)}`);
|
|
124
|
-
}
|
|
125
|
-
for (const e of watcherData.errors) {
|
|
126
|
-
newElements.push(`[error] ${e.text.substring(0, 50)}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let ariaSnapshot = "";
|
|
130
|
-
try {
|
|
131
|
-
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
132
|
-
} catch {}
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
hasNewDialog: watcherData.newDialogs.length > 0,
|
|
136
|
-
hasNewOverlay: watcherData.newOverlays.length > 0,
|
|
137
|
-
hasError: watcherData.errors.length > 0,
|
|
138
|
-
newElements,
|
|
139
|
-
ariaSnapshot,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Step sonrası durumu doğrula - aksiyon-algı döngüsü
|
|
145
|
-
* Beklenmeyen element varsa true döner
|
|
146
|
-
*/
|
|
147
|
-
export async function hasUnexpectedChange(page: Page): Promise<{ changed: boolean; description: string }> {
|
|
148
|
-
const state = await getPageState(page);
|
|
149
|
-
|
|
150
|
-
if (state.hasNewDialog) {
|
|
151
|
-
return {
|
|
152
|
-
changed: true,
|
|
153
|
-
description: `Yeni dialog açıldı: ${state.newElements.filter(e => e.startsWith('[dialog]')).join(', ')}`,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (state.hasError) {
|
|
158
|
-
return {
|
|
159
|
-
changed: true,
|
|
160
|
-
description: `Hata mesajı: ${state.newElements.filter(e => e.startsWith('[error]')).join(', ')}`,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (state.hasNewOverlay) {
|
|
165
|
-
return {
|
|
166
|
-
changed: true,
|
|
167
|
-
description: `Yeni overlay: ${state.newElements.filter(e => e.startsWith('[overlay]')).join(', ')}`,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return { changed: false, description: "" };
|
|
172
|
-
}
|
package/core/reflexes.ts
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reflexes - Sistem 1 (Hızlı, Otomatik)
|
|
3
|
-
*
|
|
4
|
-
* Kahneman'ın "Thinking, Fast and Slow" modeli:
|
|
5
|
-
* - Sistem 1: Hızlı, otomatik, refleks tepkiler (bu dosya)
|
|
6
|
-
* - Sistem 2: Yavaş, bilinçli, AI destekli (heal mekanizması)
|
|
7
|
-
*
|
|
8
|
-
* Bilinen UI pattern'larını AI çağırmadan milisaniyede çözer.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { Page } from "playwright";
|
|
12
|
-
|
|
13
|
-
interface ReflexResult {
|
|
14
|
-
handled: boolean;
|
|
15
|
-
action: string; // ne yapıldı
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Sayfadaki beklenmeyen durumu refleksle çözmeye çalış.
|
|
20
|
-
* Çözemezse handled: false döner → Sistem 2 (AI) devreye girer.
|
|
21
|
-
*/
|
|
22
|
-
export async function tryReflex(page: Page, changeDescription: string): Promise<ReflexResult> {
|
|
23
|
-
// 1. Modal/Dialog kapatma refleksi
|
|
24
|
-
const dismissed = await tryDismissDialog(page);
|
|
25
|
-
if (dismissed.handled) return dismissed;
|
|
26
|
-
|
|
27
|
-
// 2. Cookie/consent banner kapatma
|
|
28
|
-
const cookie = await tryDismissCookieBanner(page);
|
|
29
|
-
if (cookie.handled) return cookie;
|
|
30
|
-
|
|
31
|
-
// 3. File input algılama - dosya bekliyor mu?
|
|
32
|
-
const fileInput = await tryDetectFileInput(page);
|
|
33
|
-
if (fileInput.handled) return fileInput;
|
|
34
|
-
|
|
35
|
-
// 4. Error/alert kapatma
|
|
36
|
-
const error = await tryDismissError(page);
|
|
37
|
-
if (error.handled) return error;
|
|
38
|
-
|
|
39
|
-
// 5. Overlay/backdrop tıklama
|
|
40
|
-
const overlay = await tryDismissOverlay(page);
|
|
41
|
-
if (overlay.handled) return overlay;
|
|
42
|
-
|
|
43
|
-
return { handled: false, action: "reflex bulunamadı" };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Dialog/Modal kapatma - "close", "cancel", "dismiss", "x" butonlarını ara
|
|
48
|
-
*/
|
|
49
|
-
async function tryDismissDialog(page: Page): Promise<ReflexResult> {
|
|
50
|
-
// Önce dialog'un post oluşturma dialog'u olup olmadığını kontrol et - onu kapatma!
|
|
51
|
-
const isComposeDialog = await page.evaluate(`(() => {
|
|
52
|
-
const dialogs = document.querySelectorAll('[role="dialog"], [aria-modal="true"], dialog');
|
|
53
|
-
for (const d of dialogs) {
|
|
54
|
-
const text = (d.textContent || '').toLowerCase();
|
|
55
|
-
if (text.includes('post') || text.includes('gönderi') || text.includes('tweet') ||
|
|
56
|
-
text.includes('paylaş') || text.includes('share') || text.includes('compose') ||
|
|
57
|
-
text.includes('yaz') || text.includes('düzenle') || text.includes('edit')) {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return false;
|
|
62
|
-
})()`).catch(() => false);
|
|
63
|
-
|
|
64
|
-
// Compose dialog'u kapatma!
|
|
65
|
-
if (isComposeDialog) return { handled: false, action: "compose dialog - kapatılmadı" };
|
|
66
|
-
|
|
67
|
-
// Gereksiz dialog'ları kapat
|
|
68
|
-
const closeSelectors = [
|
|
69
|
-
'button[aria-label="Close"]',
|
|
70
|
-
'button[aria-label="Kapat"]',
|
|
71
|
-
'button[aria-label="Dismiss"]',
|
|
72
|
-
'button[aria-label="İptal"]',
|
|
73
|
-
'button[aria-label="Cancel"]',
|
|
74
|
-
'[role="dialog"] button[aria-label*="close" i]',
|
|
75
|
-
'[role="dialog"] button[aria-label*="kapat" i]',
|
|
76
|
-
'[aria-modal="true"] button[aria-label*="close" i]',
|
|
77
|
-
'[aria-modal="true"] button[aria-label*="kapat" i]',
|
|
78
|
-
'dialog button[aria-label*="close" i]',
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
for (const sel of closeSelectors) {
|
|
82
|
-
try {
|
|
83
|
-
const btn = page.locator(sel).first();
|
|
84
|
-
if (await btn.isVisible({ timeout: 500 })) {
|
|
85
|
-
await btn.click();
|
|
86
|
-
await page.waitForTimeout(300);
|
|
87
|
-
return { handled: true, action: `dialog kapatıldı: ${sel}` };
|
|
88
|
-
}
|
|
89
|
-
} catch {}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return { handled: false, action: "" };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Cookie/consent banner kapatma
|
|
97
|
-
*/
|
|
98
|
-
async function tryDismissCookieBanner(page: Page): Promise<ReflexResult> {
|
|
99
|
-
const cookieSelectors = [
|
|
100
|
-
'button[aria-label*="cookie" i]',
|
|
101
|
-
'button[aria-label*="çerez" i]',
|
|
102
|
-
'button[aria-label*="Accept" i]',
|
|
103
|
-
'button[aria-label*="Kabul" i]',
|
|
104
|
-
'button[aria-label*="consent" i]',
|
|
105
|
-
'[id*="cookie"] button',
|
|
106
|
-
'[class*="cookie"] button',
|
|
107
|
-
'[class*="consent"] button',
|
|
108
|
-
];
|
|
109
|
-
|
|
110
|
-
for (const sel of cookieSelectors) {
|
|
111
|
-
try {
|
|
112
|
-
const btn = page.locator(sel).first();
|
|
113
|
-
if (await btn.isVisible({ timeout: 300 })) {
|
|
114
|
-
await btn.click();
|
|
115
|
-
await page.waitForTimeout(300);
|
|
116
|
-
return { handled: true, action: `cookie banner kapatıldı: ${sel}` };
|
|
117
|
-
}
|
|
118
|
-
} catch {}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return { handled: false, action: "" };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Yeni file input algılama
|
|
126
|
-
*/
|
|
127
|
-
async function tryDetectFileInput(page: Page): Promise<ReflexResult> {
|
|
128
|
-
try {
|
|
129
|
-
const hasNewFileInput = await page.evaluate(`(() => {
|
|
130
|
-
const inputs = document.querySelectorAll('input[type="file"]');
|
|
131
|
-
return inputs.length > 0;
|
|
132
|
-
})()`);
|
|
133
|
-
|
|
134
|
-
if (hasNewFileInput) {
|
|
135
|
-
return { handled: true, action: "file input algılandı - upload step'i çalışabilir" };
|
|
136
|
-
}
|
|
137
|
-
} catch {}
|
|
138
|
-
|
|
139
|
-
return { handled: false, action: "" };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Error/alert kapatma
|
|
144
|
-
*/
|
|
145
|
-
async function tryDismissError(page: Page): Promise<ReflexResult> {
|
|
146
|
-
const errorSelectors = [
|
|
147
|
-
'[role="alert"] button',
|
|
148
|
-
'[role="alert"] [aria-label*="close" i]',
|
|
149
|
-
'[role="alert"] [aria-label*="kapat" i]',
|
|
150
|
-
'[aria-live="assertive"] button',
|
|
151
|
-
];
|
|
152
|
-
|
|
153
|
-
for (const sel of errorSelectors) {
|
|
154
|
-
try {
|
|
155
|
-
const btn = page.locator(sel).first();
|
|
156
|
-
if (await btn.isVisible({ timeout: 300 })) {
|
|
157
|
-
await btn.click();
|
|
158
|
-
await page.waitForTimeout(200);
|
|
159
|
-
return { handled: true, action: `error kapatıldı: ${sel}` };
|
|
160
|
-
}
|
|
161
|
-
} catch {}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return { handled: false, action: "" };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Overlay/backdrop tıklama ile kapatma
|
|
169
|
-
*/
|
|
170
|
-
async function tryDismissOverlay(page: Page): Promise<ReflexResult> {
|
|
171
|
-
try {
|
|
172
|
-
// ESC tuşu ile kapatmayı dene
|
|
173
|
-
await page.keyboard.press("Escape");
|
|
174
|
-
await page.waitForTimeout(300);
|
|
175
|
-
|
|
176
|
-
// Dialog hâlâ var mı kontrol et
|
|
177
|
-
const stillHasDialog = await page.evaluate(`(() => {
|
|
178
|
-
return document.querySelectorAll('[role="dialog"], [aria-modal="true"]').length > 0;
|
|
179
|
-
})()`);
|
|
180
|
-
|
|
181
|
-
if (!stillHasDialog) {
|
|
182
|
-
return { handled: true, action: "overlay ESC ile kapatıldı" };
|
|
183
|
-
}
|
|
184
|
-
} catch {}
|
|
185
|
-
|
|
186
|
-
return { handled: false, action: "" };
|
|
187
|
-
}
|