social-agent-cli 2.1.0 → 2.2.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/ai/runner.ts +33 -16
- package/core/page-watcher.ts +24 -11
- package/core/reflexes.ts +187 -0
- package/package.json +1 -1
package/ai/runner.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createPlan, type ExecutionPlan, type PlanStep } from "./planner.js";
|
|
|
4
4
|
import { healAndContinue } from "./mapper.js";
|
|
5
5
|
import { saveToHistory } from "../core/history.js";
|
|
6
6
|
import { injectWatcher, hasUnexpectedChange } from "../core/page-watcher.js";
|
|
7
|
+
import { tryReflex } from "../core/reflexes.js";
|
|
7
8
|
import type { Post, PostResult } from "../core/types.js";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import { PATHS } from "../core/config.js";
|
|
@@ -119,36 +120,52 @@ export async function runCommand(
|
|
|
119
120
|
healAttempt = 0;
|
|
120
121
|
|
|
121
122
|
// Aksiyon-algı döngüsü: step sonrası beklenmeyen değişiklik var mı?
|
|
122
|
-
await page.waitForTimeout(300);
|
|
123
|
+
await page.waitForTimeout(300);
|
|
123
124
|
const change = await hasUnexpectedChange(page);
|
|
124
125
|
if (change.changed) {
|
|
125
126
|
console.log(` ⚡ ${change.description}`);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
await page.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
`
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
137
146
|
}
|
|
138
147
|
} catch (err: any) {
|
|
148
|
+
console.log(` ✗ ${err.message}`);
|
|
149
|
+
|
|
150
|
+
// Sistem 1: Refleks - hata sonrası otomatik düzeltme
|
|
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
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Sistem 2: AI
|
|
139
158
|
healAttempt++;
|
|
140
159
|
const efforts = ["low", "medium", "medium"];
|
|
141
160
|
const effort = efforts[Math.min(healAttempt - 1, efforts.length - 1)];
|
|
142
161
|
|
|
143
|
-
console.log(`
|
|
144
|
-
console.log(` → AI düzeltiyor (deneme ${healAttempt}/3)...`);
|
|
162
|
+
console.log(` 🧠 AI düzeltiyor (deneme ${healAttempt}/3)...`);
|
|
145
163
|
|
|
146
164
|
if (healAttempt > 3) {
|
|
147
|
-
console.log(`
|
|
165
|
+
console.log(` 3 denemede çözülemedi, atlanıyor.`);
|
|
148
166
|
break;
|
|
149
167
|
}
|
|
150
168
|
|
|
151
|
-
// Screenshot + DOM al
|
|
152
169
|
const healScreenshot = join(PATHS.screenshots, `${platform}_heal_${stepIdx}.png`);
|
|
153
170
|
await page.screenshot({ path: healScreenshot });
|
|
154
171
|
|
package/core/page-watcher.ts
CHANGED
|
@@ -44,28 +44,41 @@ export async function injectWatcher(page: Page): Promise<void> {
|
|
|
44
44
|
const ariaModal = node.getAttribute('aria-modal');
|
|
45
45
|
const tag = node.tagName.toLowerCase();
|
|
46
46
|
|
|
47
|
-
// Dialog/Modal algılama
|
|
48
|
-
|
|
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)) {
|
|
49
55
|
watcher.newDialogs.push({
|
|
50
56
|
role: role || tag,
|
|
51
57
|
ariaLabel: node.getAttribute('aria-label') || '',
|
|
52
58
|
text: node.textContent?.substring(0, 200) || '',
|
|
59
|
+
className: cls.substring(0, 100),
|
|
53
60
|
time: Date.now()
|
|
54
61
|
});
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
// Overlay/backdrop algılama
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
// Error mesajı algılama
|
|
68
|
-
|
|
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) {
|
|
69
82
|
watcher.errors.push({
|
|
70
83
|
text: node.textContent?.substring(0, 300) || '',
|
|
71
84
|
time: Date.now()
|
package/core/reflexes.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
}
|