social-agent-cli 3.2.0 → 4.1.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/mapper.ts +8 -340
- package/ai/planner.ts +1 -1
- package/ai/runner.ts +69 -71
- package/cli.ts +5 -8
- package/core/upload-helper.ts +34 -0
- package/maps/linkedin.json +11 -44
- package/package.json +2 -1
- package/platforms/browser/driver.ts +57 -19
package/ai/mapper.ts
CHANGED
|
@@ -1,173 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, mkdtempSync, rmSync, readdirSync } from "node:fs";
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
|
|
3
2
|
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
3
|
import { PATHS } from "../core/config.js";
|
|
6
4
|
import type { SelectorMap } from "../core/types.js";
|
|
7
5
|
|
|
8
|
-
// ── JSON Schemas ─────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
const SELECTOR_MAP_SCHEMA = JSON.stringify({
|
|
11
|
-
type: "object",
|
|
12
|
-
properties: {
|
|
13
|
-
platform: { type: "string" },
|
|
14
|
-
action: { type: "string" },
|
|
15
|
-
description: { type: "string" },
|
|
16
|
-
version: { type: "number" },
|
|
17
|
-
lastUpdated: { type: "string" },
|
|
18
|
-
parameters: { type: "array", items: { type: "string" } },
|
|
19
|
-
steps: {
|
|
20
|
-
type: "array",
|
|
21
|
-
items: {
|
|
22
|
-
type: "object",
|
|
23
|
-
properties: {
|
|
24
|
-
action: { type: "string", enum: ["click", "type", "wait", "keypress", "goto", "upload"] },
|
|
25
|
-
description: { type: "string" },
|
|
26
|
-
selector: { type: "string" },
|
|
27
|
-
value: { type: "string" },
|
|
28
|
-
key: { type: "string" },
|
|
29
|
-
url: { type: "string" },
|
|
30
|
-
waitMs: { type: "number" },
|
|
31
|
-
fallbackSelectors: { type: "array", items: { type: "string" } },
|
|
32
|
-
},
|
|
33
|
-
required: ["action", "description"],
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
required: ["platform", "version", "steps"],
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const HEAL_SCHEMA = JSON.stringify({
|
|
41
|
-
type: "object",
|
|
42
|
-
properties: {
|
|
43
|
-
selector: { type: "string" },
|
|
44
|
-
fallbackSelectors: { type: "array", items: { type: "string" } },
|
|
45
|
-
explanation: { type: "string" },
|
|
46
|
-
},
|
|
47
|
-
required: ["selector", "fallbackSelectors", "explanation"],
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// ── Claude -p Wrapper ────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
interface ClaudeOptions {
|
|
53
|
-
prompt: string;
|
|
54
|
-
jsonSchema?: string;
|
|
55
|
-
imagePath?: string;
|
|
56
|
-
model?: string;
|
|
57
|
-
effort?: string;
|
|
58
|
-
stream?: boolean;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function claude(opts: ClaudeOptions): Promise<any> {
|
|
62
|
-
const { prompt, jsonSchema, imagePath, model = "opus", effort = "low", stream = true } = opts;
|
|
63
|
-
|
|
64
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "social-agent-"));
|
|
65
|
-
const promptFile = join(tmpDir, "prompt.txt");
|
|
66
|
-
writeFileSync(promptFile, prompt);
|
|
67
|
-
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
let cmd: string;
|
|
70
|
-
const base = `claude -p --model ${model} --effort ${effort} --max-turns 3 --tools ""`;
|
|
71
|
-
|
|
72
|
-
// Screenshot varsa prompt'a dosya yolunu ekle, Claude Code dosyayı okuyabilir
|
|
73
|
-
if (imagePath && existsSync(imagePath)) {
|
|
74
|
-
writeFileSync(promptFile, `Screenshot: ${imagePath}\n\n${prompt}`);
|
|
75
|
-
}
|
|
76
|
-
cmd = `cat '${promptFile}' | ${base}`;
|
|
77
|
-
if (jsonSchema) cmd += ` --output-format json --json-schema '${jsonSchema}'`;
|
|
78
|
-
|
|
79
|
-
const proc = spawn("bash", ["-c", cmd], {
|
|
80
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
let stdout = "";
|
|
84
|
-
let stderr = "";
|
|
85
|
-
|
|
86
|
-
proc.stdout.on("data", (chunk: Buffer) => {
|
|
87
|
-
const text = chunk.toString();
|
|
88
|
-
stdout += text;
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
proc.stderr.on("data", (chunk: Buffer) => {
|
|
92
|
-
stderr += chunk.toString();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
proc.on("close", (code) => {
|
|
96
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
97
|
-
if (stream) process.stdout.write("\n");
|
|
98
|
-
|
|
99
|
-
const output = stdout.trim();
|
|
100
|
-
|
|
101
|
-
if (jsonSchema) {
|
|
102
|
-
try {
|
|
103
|
-
const parsed = JSON.parse(output);
|
|
104
|
-
|
|
105
|
-
// structured_output alanı varsa ve dolu ise
|
|
106
|
-
if (parsed.structured_output && parsed.structured_output.steps) {
|
|
107
|
-
resolve(parsed.structured_output);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// result alanında JSON string olabilir
|
|
112
|
-
if (parsed.result && typeof parsed.result === "string" && parsed.result.length > 2) {
|
|
113
|
-
try { resolve(JSON.parse(parsed.result)); return; } catch {}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// error_max_turns veya boş result durumunda - result'tan text çıkar
|
|
117
|
-
if (parsed.subtype === "error_max_turns" || !parsed.structured_output) {
|
|
118
|
-
// Claude tool kullanıp bitirmeden durmuş, sonucu elle çıkarmayı dene
|
|
119
|
-
reject(new Error(`Claude tamamlayamadı (${parsed.subtype || "unknown"}). max-turns artırılabilir.`));
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Mesaj dizisi formatı
|
|
124
|
-
if (Array.isArray(parsed)) {
|
|
125
|
-
const last = parsed[parsed.length - 1];
|
|
126
|
-
const text = typeof last.content === "string"
|
|
127
|
-
? last.content
|
|
128
|
-
: last.content?.map((c: any) => c.text || "").join("") || "";
|
|
129
|
-
resolve(JSON.parse(text));
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Direkt obje
|
|
134
|
-
if (parsed.steps || parsed.selector) {
|
|
135
|
-
resolve(parsed);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
resolve(parsed);
|
|
140
|
-
} catch (parseErr: any) {
|
|
141
|
-
// Regex fallback
|
|
142
|
-
const match = output.match(/\{[\s\S]*\}/);
|
|
143
|
-
if (match) {
|
|
144
|
-
try { resolve(JSON.parse(match[0])); return; } catch {}
|
|
145
|
-
}
|
|
146
|
-
reject(new Error("Claude'dan geçerli JSON alınamadı"));
|
|
147
|
-
}
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (code !== 0 && !output) {
|
|
152
|
-
reject(new Error(`Claude hata (code ${code}): ${stderr}`));
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
resolve(output);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
proc.on("error", (err) => {
|
|
160
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
161
|
-
reject(err);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
setTimeout(() => {
|
|
165
|
-
proc.kill();
|
|
166
|
-
reject(new Error("Claude timeout (180s)"));
|
|
167
|
-
}, 180000);
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
6
|
// ── Map CRUD (platform başına tek dosya) ─────────────────────
|
|
172
7
|
|
|
173
8
|
interface PlatformMaps {
|
|
@@ -207,182 +42,15 @@ export function listMaps(platform?: string): SelectorMap[] {
|
|
|
207
42
|
for (const file of readdirSync(PATHS.maps)) {
|
|
208
43
|
if (!file.endsWith(".json")) continue;
|
|
209
44
|
if (platform && file !== `${platform}.json`) continue;
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
results.push(
|
|
45
|
+
const content = JSON.parse(readFileSync(join(PATHS.maps, file), "utf-8"));
|
|
46
|
+
if (content.steps) {
|
|
47
|
+
results.push(content);
|
|
48
|
+
} else {
|
|
49
|
+
for (const map of Object.values(content) as SelectorMap[]) {
|
|
50
|
+
if (map.steps) results.push(map);
|
|
51
|
+
}
|
|
213
52
|
}
|
|
214
53
|
}
|
|
215
54
|
} catch {}
|
|
216
55
|
return results;
|
|
217
56
|
}
|
|
218
|
-
|
|
219
|
-
// ── Map Oluştur ──────────────────────────────────────────────
|
|
220
|
-
|
|
221
|
-
export async function generateMap(
|
|
222
|
-
platform: string,
|
|
223
|
-
screenshotPath: string,
|
|
224
|
-
domSnapshot: string,
|
|
225
|
-
taskDescription = "yeni bir metin postu at",
|
|
226
|
-
actionName = "post"
|
|
227
|
-
): Promise<SelectorMap> {
|
|
228
|
-
const prompt = `Sen bir browser otomasyon uzmanısın. ${platform} platformunda şu görevi yerine getirmek için adımları belirle:
|
|
229
|
-
|
|
230
|
-
GÖREV: "${taskDescription}"
|
|
231
|
-
|
|
232
|
-
Sayfanın Accessibility Tree'si (ARIA Snapshot):
|
|
233
|
-
${domSnapshot}
|
|
234
|
-
|
|
235
|
-
Kurallar:
|
|
236
|
-
- ARIA snapshot'taki role ve name bilgilerini kullanarak selector oluştur
|
|
237
|
-
- aria-label, role, data-testid, id tercih et
|
|
238
|
-
- Dosya/görsel/video yükleme için "upload" action kullan. Upload action şöyle çalışır:
|
|
239
|
-
- selector'da medya/fotoğraf ekleme BUTONUNU ver (ör: button[aria-label*='Fotoğraf'], button[aria-label*='Add media'])
|
|
240
|
-
- Sistem butona tıklayıp fileChooser'ı yakalayacak ve dosyayı programatik olarak set edecek, file picker açılmayacak
|
|
241
|
-
- Eğer selector verilmezse sayfadaki input[type=file]'a direkt set eder
|
|
242
|
-
- value alanına {{IMAGE}} yaz
|
|
243
|
-
- Her adım için 2+ fallback selector
|
|
244
|
-
- Parametreler için placeholder kullan: {{CONTENT}} = metin, {{USERNAME}} = kullanıcı adı, {{URL}} = link, vs.
|
|
245
|
-
- Hangi parametreleri kullandığını "parameters" alanında listele
|
|
246
|
-
- action: "${actionName}"
|
|
247
|
-
- platform: "${platform}", version: 1, lastUpdated: "${new Date().toISOString()}"
|
|
248
|
-
- Gerekiyorsa "goto" action'ı ile belirli bir URL'e git (url alanını kullan)`;
|
|
249
|
-
|
|
250
|
-
console.log(`[claude] "${taskDescription}" analiz ediliyor...\n`);
|
|
251
|
-
|
|
252
|
-
let map: SelectorMap;
|
|
253
|
-
try {
|
|
254
|
-
map = await claude({
|
|
255
|
-
prompt,
|
|
256
|
-
jsonSchema: SELECTOR_MAP_SCHEMA,
|
|
257
|
-
imagePath: screenshotPath,
|
|
258
|
-
stream: false,
|
|
259
|
-
effort: "low",
|
|
260
|
-
});
|
|
261
|
-
} catch (err: any) {
|
|
262
|
-
console.error(`\n[claude] Hata: ${err.message}`);
|
|
263
|
-
throw err;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (!map || !map.steps) {
|
|
267
|
-
throw new Error("Claude geçerli bir map döndürmedi");
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
map.platform = platform;
|
|
271
|
-
map.action = actionName;
|
|
272
|
-
map.description = taskDescription;
|
|
273
|
-
map.lastUpdated = new Date().toISOString();
|
|
274
|
-
saveMap(map);
|
|
275
|
-
return map;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ── Self-Heal: Kaldığın yerden devam et ──────────────────────
|
|
279
|
-
|
|
280
|
-
const REMAINING_STEPS_SCHEMA = JSON.stringify({
|
|
281
|
-
type: "object",
|
|
282
|
-
properties: {
|
|
283
|
-
analysis: { type: "string" },
|
|
284
|
-
remainingSteps: {
|
|
285
|
-
type: "array",
|
|
286
|
-
items: {
|
|
287
|
-
type: "object",
|
|
288
|
-
properties: {
|
|
289
|
-
action: { type: "string", enum: ["click", "type", "wait", "keypress", "goto", "upload"] },
|
|
290
|
-
description: { type: "string" },
|
|
291
|
-
selector: { type: "string" },
|
|
292
|
-
value: { type: "string" },
|
|
293
|
-
key: { type: "string" },
|
|
294
|
-
url: { type: "string" },
|
|
295
|
-
waitMs: { type: "number" },
|
|
296
|
-
fallbackSelectors: { type: "array", items: { type: "string" } },
|
|
297
|
-
},
|
|
298
|
-
required: ["action", "description"],
|
|
299
|
-
},
|
|
300
|
-
},
|
|
301
|
-
},
|
|
302
|
-
required: ["analysis", "remainingSteps"],
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Bir adım başarısız olduğunda AI ekranı görür,
|
|
307
|
-
* durumu analiz eder ve kalan adımları yeniden planlar.
|
|
308
|
-
* Sadece selector düzeltmez - tamamen yeni akış çizebilir.
|
|
309
|
-
*/
|
|
310
|
-
export async function healAndContinue(
|
|
311
|
-
platform: string,
|
|
312
|
-
failedStepIndex: number,
|
|
313
|
-
originalSteps: SelectorStep[],
|
|
314
|
-
screenshotPath: string,
|
|
315
|
-
domSnapshot: string,
|
|
316
|
-
errorMessage: string,
|
|
317
|
-
postContent: string,
|
|
318
|
-
effort = "low"
|
|
319
|
-
): Promise<SelectorStep[]> {
|
|
320
|
-
const completedSteps = originalSteps.slice(0, failedStepIndex);
|
|
321
|
-
const failedStep = originalSteps[failedStepIndex];
|
|
322
|
-
const remainingOriginal = originalSteps.slice(failedStepIndex);
|
|
323
|
-
|
|
324
|
-
const prompt = `Bir browser otomasyon script'i çalışırken hata oluştu. Ekranın screenshot'ını ve DOM'unu görüyorsun.
|
|
325
|
-
|
|
326
|
-
Platform: ${platform}
|
|
327
|
-
Amaç: "${postContent}" metnini post olarak paylaşmak
|
|
328
|
-
|
|
329
|
-
Tamamlanan adımlar:
|
|
330
|
-
${completedSteps.map((s, i) => ` ${i + 1}. [${s.action}] ${s.description} ✓`).join("\n")}
|
|
331
|
-
|
|
332
|
-
Başarısız olan adım (#${failedStepIndex + 1}):
|
|
333
|
-
[${failedStep.action}] ${failedStep.description}
|
|
334
|
-
Selector: ${failedStep.selector}
|
|
335
|
-
Hata: ${errorMessage}
|
|
336
|
-
|
|
337
|
-
Kalan orijinal adımlar:
|
|
338
|
-
${remainingOriginal.map((s, i) => ` ${failedStepIndex + i + 1}. [${s.action}] ${s.description} → ${s.selector}`).join("\n")}
|
|
339
|
-
|
|
340
|
-
Sayfanın Accessibility Tree'si:
|
|
341
|
-
${domSnapshot}
|
|
342
|
-
|
|
343
|
-
GÖREV: Ekranın mevcut durumunu analiz et. Amaca ulaşmak için kalan adımları yeniden planla.
|
|
344
|
-
- Belki selector değişmiş → yeni selector bul
|
|
345
|
-
- Belki ekstra bir adım gerekiyor (popup kapatma, scroll, vb.)
|
|
346
|
-
- Belki farklı bir yol izlenmeli
|
|
347
|
-
- {{CONTENT}} = post metni placeholder'ı
|
|
348
|
-
- data-testid, aria-label, role tercih et, her adıma 2+ fallback selector ver
|
|
349
|
-
- Dosya yükleme için "upload" action kullan. selector'a medya ekleme butonunu ver, value: {{IMAGE}}`;
|
|
350
|
-
|
|
351
|
-
console.log(`\n[heal] AI ekranı analiz ediyor (effort: ${effort})...\n`);
|
|
352
|
-
|
|
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
|
-
}
|
|
366
|
-
|
|
367
|
-
// result çeşitli formatlarda gelebilir
|
|
368
|
-
if (!result) {
|
|
369
|
-
return originalSteps.slice(failedStepIndex + 1);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// structured_output içinde olabilir
|
|
373
|
-
if (result.structured_output?.remainingSteps) {
|
|
374
|
-
result = result.structured_output;
|
|
375
|
-
}
|
|
376
|
-
|
|
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);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
console.log(`\n[heal] ${steps.length} yeni adım`);
|
|
386
|
-
|
|
387
|
-
return steps;
|
|
388
|
-
}
|
package/ai/planner.ts
CHANGED
|
@@ -151,7 +151,7 @@ GÖREV: Bu komutu yerine getirmek için bir çalıştırma planı oluştur.
|
|
|
151
151
|
Kurallar:
|
|
152
152
|
- Mevcut map varsa "use_map" kullan, mapAction'a action adını yaz
|
|
153
153
|
- Mevcut map yoksa "learn_new" kullan, AI otomatik öğrenecek
|
|
154
|
-
-
|
|
154
|
+
- Komutta fotoğraf/görsel varsa mevcut "post" map'ini kullan, IMAGE parametresini ekle. Sistem upload'ı otomatik handle eder - ayrı map öğretmeye gerek YOK
|
|
155
155
|
- Parametreleri doldur ({{TWEET_URL}}, {{USERNAME}}, {{CONTENT}}, {{IMAGE}} vb.)
|
|
156
156
|
- Belirli kişi/URL verilmemişse platformun keşif/öneri sayfalarını kullan
|
|
157
157
|
- Tekrar eden işlemler için "repeat" kullan
|
package/ai/runner.ts
CHANGED
|
@@ -1,101 +1,99 @@
|
|
|
1
1
|
import { BrowserDriver } from "../platforms/browser/driver.js";
|
|
2
|
-
import { loadMap
|
|
3
|
-
import { createPlan } from "./planner.js";
|
|
2
|
+
import { loadMap } from "./mapper.js";
|
|
4
3
|
import { saveToHistory } from "../core/history.js";
|
|
5
4
|
import type { Post } from "../core/types.js";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { PATHS } from "../core/config.js";
|
|
8
5
|
|
|
9
6
|
const BROWSER_PLATFORMS: Record<string, string> = {
|
|
10
7
|
x: "https://x.com",
|
|
11
8
|
linkedin: "https://www.linkedin.com",
|
|
12
9
|
};
|
|
13
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Post at - map'i direkt çalıştır, AI çağırmadan
|
|
13
|
+
*/
|
|
14
14
|
export async function runCommand(platform: string, command: string): Promise<void> {
|
|
15
15
|
const url = BROWSER_PLATFORMS[platform];
|
|
16
|
-
if (!url) { console.log(`"${platform}" desteklenmiyor.`);
|
|
16
|
+
if (!url) { console.log(`"${platform}" desteklenmiyor.`); process.exit(1); }
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// --content "..." ve --image ... parse et
|
|
19
|
+
let content = "";
|
|
20
|
+
let image = "";
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
const contentMatch = command.match(/--content\s+([\s\S]+?)(?=\s+--|\s*$)/);
|
|
23
|
+
const imageMatch = command.match(/--image\s+(\S+)/);
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
if (contentMatch) {
|
|
26
|
+
content = contentMatch[1].replace(/^"|"$/g, "").trim();
|
|
27
|
+
} else {
|
|
28
|
+
// Flag yoksa tüm komutu content olarak al
|
|
29
|
+
content = command.replace(/--image\s+\S+/g, "").trim();
|
|
30
|
+
}
|
|
31
|
+
if (imageMatch) {
|
|
32
|
+
image = imageMatch[1];
|
|
33
|
+
}
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
// Map seç
|
|
36
|
+
const map = loadMap(platform, "post");
|
|
37
|
+
if (!map) {
|
|
38
|
+
console.log(`[${platform}] post map'i bulunamadı. "social-agent learn ${platform}" çalıştır.`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
console.log(`[${platform}] Post: "${content.substring(0, 50)}${content.length > 50 ? "..." : ""}"`);
|
|
43
|
+
if (image) console.log(`[${platform}] Görsel: ${image}`);
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
const driver = new BrowserDriver(platform, url);
|
|
46
|
+
await driver.launch();
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
try {
|
|
49
|
+
// Ana sayfaya git
|
|
50
|
+
if (map.steps[0]?.action !== "goto") {
|
|
51
|
+
driver.navigate(url);
|
|
52
|
+
}
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
let steps = [...map.steps];
|
|
55
|
+
|
|
56
|
+
// Image varsa: gönder butonundan önce upload step ekle
|
|
57
|
+
if (image && !steps.some(s => s.action === "upload")) {
|
|
58
|
+
const sendIdx = steps.findLastIndex(s => s.action === "click");
|
|
59
|
+
if (sendIdx > -1) {
|
|
60
|
+
steps.splice(sendIdx, 0, {
|
|
61
|
+
action: "upload" as any,
|
|
62
|
+
description: "Fotoğraf yükle",
|
|
63
|
+
selector: "input[type='file']",
|
|
64
|
+
value: "{{IMAGE}}",
|
|
65
|
+
waitMs: 3000,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
45
69
|
|
|
70
|
+
// Step'leri çalıştır
|
|
71
|
+
const errors: string[] = [];
|
|
72
|
+
for (let idx = 0; idx < steps.length; idx++) {
|
|
73
|
+
const s = steps[idx];
|
|
46
74
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
driver.navigate(url);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
let steps = [...map.steps];
|
|
53
|
-
let healCount = 0;
|
|
54
|
-
|
|
55
|
-
for (let idx = 0; idx < steps.length; idx++) {
|
|
56
|
-
const s = steps[idx];
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
console.log(` ${idx + 1}. ${s.description}`);
|
|
60
|
-
driver.executeStep(s, content, image);
|
|
61
|
-
} catch (err: any) {
|
|
62
|
-
if (healCount >= 2) {
|
|
63
|
-
console.log(` ✗ ${err.message} (atlanıyor)`);
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
healCount++;
|
|
67
|
-
console.log(` ✗ ${err.message}`);
|
|
68
|
-
console.log(` → Düzeltiliyor...`);
|
|
69
|
-
|
|
70
|
-
const ss = join(PATHS.screenshots, `${platform}_heal.png`);
|
|
71
|
-
driver.takeScreenshot(ss);
|
|
72
|
-
const snap = driver.getSnapshot();
|
|
73
|
-
|
|
74
|
-
const fixed = await healAndContinue(
|
|
75
|
-
platform, idx, steps, ss, snap,
|
|
76
|
-
err.message, content || step.description, "medium"
|
|
77
|
-
);
|
|
78
|
-
steps = [...steps.slice(0, idx), ...fixed];
|
|
79
|
-
idx--;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
console.log(` ✓ Tamamlandı`);
|
|
84
|
-
saveToHistory({ platform, content: content || step.description, action: mapAction.includes("post") ? "post" : mapAction, success: true });
|
|
75
|
+
console.log(` ${idx + 1}. ${s.description}`);
|
|
76
|
+
driver.executeStep(s, content, image);
|
|
85
77
|
} catch (err: any) {
|
|
78
|
+
errors.push(`Step ${idx + 1} (${s.description}): ${err.message}`);
|
|
86
79
|
console.log(` ✗ ${err.message}`);
|
|
87
|
-
saveToHistory({ platform, content: content || step.description, action: mapAction, success: false, error: err.message });
|
|
88
|
-
} finally {
|
|
89
|
-
await driver.close();
|
|
90
80
|
}
|
|
81
|
+
}
|
|
91
82
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
83
|
+
if (errors.length === 0) {
|
|
84
|
+
console.log(`\n✓ ${platform} post gönderildi`);
|
|
85
|
+
saveToHistory({ platform, content, action: "post", success: true, image: image || undefined });
|
|
86
|
+
} else {
|
|
87
|
+
console.log(`\n⚠ ${errors.length} hata oluştu:`);
|
|
88
|
+
errors.forEach(e => console.log(` - ${e}`));
|
|
89
|
+
saveToHistory({ platform, content, action: "post", success: false, error: errors.join("; ") });
|
|
95
90
|
}
|
|
96
|
-
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
console.log(`\n✗ ${err.message}`);
|
|
93
|
+
saveToHistory({ platform, content, action: "post", success: false, error: err.message });
|
|
94
|
+
} finally {
|
|
95
|
+
await driver.close();
|
|
97
96
|
}
|
|
98
97
|
|
|
99
|
-
console.log("[runner] Tamamlandı.");
|
|
100
98
|
process.exit(0);
|
|
101
99
|
}
|
package/cli.ts
CHANGED
|
@@ -189,17 +189,14 @@ switch (command) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
case "run": {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const runCmd = BROWSER_PLATFORMS[filteredArgs[0]]
|
|
196
|
-
? filteredArgs.slice(1).join(" ")
|
|
197
|
-
: filteredArgs.join(" ");
|
|
192
|
+
const runPlatform = BROWSER_PLATFORMS[args[0]] ? args[0] : "x";
|
|
193
|
+
const runArgs = BROWSER_PLATFORMS[args[0]] ? args.slice(1) : args;
|
|
194
|
+
const runCmd = runArgs.join(" ");
|
|
198
195
|
if (!runCmd) {
|
|
199
|
-
console.log('Kullanım: social-agent run <platform>
|
|
196
|
+
console.log('Kullanım: social-agent run <platform> --content "mesaj" [--image /path/to/img]');
|
|
200
197
|
break;
|
|
201
198
|
}
|
|
202
|
-
await runCommand(runPlatform, runCmd
|
|
199
|
+
await runCommand(runPlatform, runCmd);
|
|
203
200
|
break;
|
|
204
201
|
}
|
|
205
202
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LinkedIn upload helper - Playwright fileChooser ile dosya yükler
|
|
3
|
+
* agent-browser CDP ile çalışmıyor çünkü LinkedIn native file dialog kullanıyor
|
|
4
|
+
*
|
|
5
|
+
* Kullanım: npx tsx core/upload-helper.ts <cdp-port> <button-selector> <file-path>
|
|
6
|
+
*/
|
|
7
|
+
import { chromium } from "playwright";
|
|
8
|
+
|
|
9
|
+
const [, , port, selector, filePath] = process.argv;
|
|
10
|
+
|
|
11
|
+
if (!port || !selector || !filePath) {
|
|
12
|
+
console.error("Kullanım: npx tsx core/upload-helper.ts <port> <selector> <file-path>");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function upload() {
|
|
17
|
+
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
18
|
+
const context = browser.contexts()[0];
|
|
19
|
+
const pages = context.pages();
|
|
20
|
+
const page = pages[pages.length - 1]; // son aktif sayfa
|
|
21
|
+
|
|
22
|
+
const [fileChooser] = await Promise.all([
|
|
23
|
+
page.waitForEvent("filechooser", { timeout: 10000 }),
|
|
24
|
+
page.click(selector),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
await fileChooser.setFiles(filePath);
|
|
28
|
+
console.log("OK");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
upload().catch((e) => {
|
|
32
|
+
console.error(e.message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
package/maps/linkedin.json
CHANGED
|
@@ -2,68 +2,35 @@
|
|
|
2
2
|
"post": {
|
|
3
3
|
"platform": "linkedin",
|
|
4
4
|
"action": "post",
|
|
5
|
-
"description": "LinkedIn
|
|
6
|
-
"version":
|
|
7
|
-
"lastUpdated": "2026-03-14T12:00:00.000Z",
|
|
5
|
+
"description": "LinkedIn post paylaşma",
|
|
6
|
+
"version": 3,
|
|
8
7
|
"parameters": [
|
|
9
8
|
"{{CONTENT}}"
|
|
10
9
|
],
|
|
11
10
|
"steps": [
|
|
12
11
|
{
|
|
13
12
|
"action": "click",
|
|
14
|
-
"description": "Gönderi
|
|
13
|
+
"description": "\"Gönderi başlatın\" butonuna tıkla",
|
|
15
14
|
"selector": "button.share-box-feed-entry__trigger",
|
|
16
|
-
"fallbackSelectors": [
|
|
17
|
-
|
|
18
|
-
"[aria-label*='Gönderi başlat']",
|
|
19
|
-
"button[aria-label*='post']"
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"action": "wait",
|
|
24
|
-
"description": "Post oluşturma modalının açılmasını bekle",
|
|
25
|
-
"waitMs": 1500
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"action": "click",
|
|
29
|
-
"description": "Metin alanına tıkla",
|
|
30
|
-
"selector": "[role='textbox'][aria-label*='Text editor']",
|
|
31
|
-
"fallbackSelectors": [
|
|
32
|
-
"[role='textbox'][contenteditable='true']",
|
|
33
|
-
".ql-editor[contenteditable='true']",
|
|
34
|
-
"[aria-label*='Metin düzenleyici']"
|
|
35
|
-
]
|
|
15
|
+
"fallbackSelectors": [],
|
|
16
|
+
"waitMs": 2000
|
|
36
17
|
},
|
|
37
18
|
{
|
|
38
19
|
"action": "type",
|
|
39
20
|
"description": "Post metnini yaz",
|
|
40
|
-
"selector": "[role
|
|
21
|
+
"selector": "[role=\"textbox\"]",
|
|
41
22
|
"value": "{{CONTENT}}",
|
|
42
23
|
"fallbackSelectors": [
|
|
43
|
-
"[
|
|
44
|
-
|
|
45
|
-
]
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"action": "wait",
|
|
49
|
-
"description": "Metin girişi sonrası bekleme",
|
|
24
|
+
"[contenteditable=\"true\"]"
|
|
25
|
+
],
|
|
50
26
|
"waitMs": 1000
|
|
51
27
|
},
|
|
52
28
|
{
|
|
53
29
|
"action": "click",
|
|
54
|
-
"description": "
|
|
30
|
+
"description": "\"Gönderi\" gönder butonuna tıkla",
|
|
55
31
|
"selector": "button.share-actions__primary-action",
|
|
56
|
-
"fallbackSelectors": [
|
|
57
|
-
|
|
58
|
-
"[aria-label='Gönder']",
|
|
59
|
-
"button[aria-label*='Post']",
|
|
60
|
-
"button[aria-label*='Paylaş']"
|
|
61
|
-
]
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
"action": "wait",
|
|
65
|
-
"description": "Post gönderimini bekle",
|
|
66
|
-
"waitMs": 2000
|
|
32
|
+
"fallbackSelectors": [],
|
|
33
|
+
"waitMs": 3000
|
|
67
34
|
}
|
|
68
35
|
]
|
|
69
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "social-agent-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "AI-powered social media agent - free APIs + browser automation with self-healing selectors",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"tsconfig.json"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
+
"playwright": "^1.58.2",
|
|
27
28
|
"tsx": "^4.19.0"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
@@ -3,7 +3,7 @@ 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
|
|
6
|
+
import { loadMap } from "../../ai/mapper.js";
|
|
7
7
|
import { pickChromeProfile, getSavedProfile } from "../../core/profiles.js";
|
|
8
8
|
import type { SelectorMap } from "../../core/types.js";
|
|
9
9
|
|
|
@@ -105,27 +105,26 @@ export class BrowserDriver {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* Learn - ARIA snapshot
|
|
108
|
+
* Learn - ARIA snapshot al ve dosyaya yaz (Claude Code okuyup map oluşturur)
|
|
109
109
|
*/
|
|
110
110
|
async learn(taskDescription = "yeni bir metin postu at", actionName = "post"): Promise<SelectorMap> {
|
|
111
111
|
await this.launch();
|
|
112
112
|
this.cmd(`open "${this.homeUrl}"`, 30000);
|
|
113
113
|
this.cmd("wait 3000");
|
|
114
114
|
|
|
115
|
-
const screenshotPath = join(PATHS.screenshots, `${this.platform}
|
|
115
|
+
const screenshotPath = join(PATHS.screenshots, `${this.platform}_learn.png`);
|
|
116
116
|
this.cmd(`screenshot "${screenshotPath}"`);
|
|
117
117
|
|
|
118
|
-
// Compact interactive snapshot
|
|
119
118
|
const snapshot = this.cmd("snapshot -i -c", 10000);
|
|
119
|
+
const snapshotPath = join(PATHS.screenshots, `${this.platform}_snapshot.txt`);
|
|
120
|
+
const { writeFileSync: writeFile } = await import("node:fs");
|
|
121
|
+
writeFile(snapshotPath, snapshot);
|
|
120
122
|
|
|
121
|
-
console.log(`
|
|
122
|
-
console.log(`
|
|
123
|
-
console.log(
|
|
124
|
-
|
|
125
|
-
const map = await generateMap(this.platform, screenshotPath, snapshot, taskDescription, actionName);
|
|
126
|
-
console.log(`[${this.platform}] "${actionName}" map oluşturuldu (${map.steps.length} adım)`);
|
|
123
|
+
console.log(`Screenshot: ${screenshotPath}`);
|
|
124
|
+
console.log(`Snapshot: ${snapshotPath}`);
|
|
125
|
+
console.log(`\nBu dosyaları Claude Code'a ver, map oluştursun.`);
|
|
127
126
|
process.exit(0);
|
|
128
|
-
return
|
|
127
|
+
return {} as SelectorMap;
|
|
129
128
|
}
|
|
130
129
|
|
|
131
130
|
navigate(url: string): void {
|
|
@@ -147,16 +146,31 @@ export class BrowserDriver {
|
|
|
147
146
|
|
|
148
147
|
case "click": {
|
|
149
148
|
const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
|
|
149
|
+
// 1. CSS selector ile dene
|
|
150
150
|
for (const sel of sels) {
|
|
151
151
|
if (sel.includes("{{")) continue;
|
|
152
152
|
try { this.cmd(`click "${sel}"`); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
// 2. JS eval fallback - description'dan buton text'i çıkar
|
|
155
|
+
const desc = step.description || "";
|
|
156
|
+
// Tırnak içindeki text'leri bul: "Gönderi", 'Post', vb.
|
|
157
|
+
const textMatches = desc.match(/["'""\u201C\u201D]([^"'""\u201C\u201D]+)["'""\u201C\u201D]/g) || [];
|
|
158
|
+
const texts = textMatches.map(m => m.replace(/["'""\u201C\u201D]/g, "").trim()).filter(Boolean);
|
|
159
|
+
|
|
160
|
+
for (const text of texts) {
|
|
161
|
+
try {
|
|
162
|
+
this.cmd(`eval "document.querySelectorAll('button,a,[role=button]').forEach(b => { if(b.textContent.trim()==='${text}' || b.textContent.trim().startsWith('${text}')) b.click() })"`);
|
|
163
|
+
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
164
|
+
return;
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`Click: ${sels[0] || desc}`);
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
case "type": {
|
|
158
171
|
const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
|
|
159
|
-
const escaped = val.replace(/"/g, '\\"');
|
|
172
|
+
const escaped = val.replace(/"/g, '\\"').replace(/'/g, "\\'");
|
|
173
|
+
// 1. CSS selector ile dene
|
|
160
174
|
for (const sel of sels) {
|
|
161
175
|
if (sel.includes("{{")) continue;
|
|
162
176
|
try {
|
|
@@ -165,6 +179,12 @@ export class BrowserDriver {
|
|
|
165
179
|
return;
|
|
166
180
|
} catch {}
|
|
167
181
|
}
|
|
182
|
+
// 2. Fallback: sayfadaki ilk textbox/contenteditable'a yaz
|
|
183
|
+
try {
|
|
184
|
+
this.cmd(`eval "var el = document.querySelector('[role=textbox],[contenteditable=true]'); if(el) { el.focus(); document.execCommand('insertText', false, '${escaped}'); }"`);
|
|
185
|
+
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
186
|
+
return;
|
|
187
|
+
} catch {}
|
|
168
188
|
throw new Error(`Type: ${sels[0]}`);
|
|
169
189
|
}
|
|
170
190
|
|
|
@@ -176,17 +196,35 @@ export class BrowserDriver {
|
|
|
176
196
|
case "upload": {
|
|
177
197
|
const filePath = val || imagePath || "";
|
|
178
198
|
if (!filePath || filePath.includes("{{")) break;
|
|
179
|
-
|
|
199
|
+
|
|
200
|
+
// 1. agent-browser upload ile dene (X'te çalışır)
|
|
180
201
|
try {
|
|
181
|
-
this.cmd(`upload "input[type='file']" "${filePath}"`,
|
|
202
|
+
this.cmd(`upload "input[type='file']" "${filePath}"`, 5000);
|
|
182
203
|
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
183
204
|
return;
|
|
184
205
|
} catch {}
|
|
185
|
-
|
|
206
|
+
|
|
207
|
+
// 2. Playwright fileChooser ile dene (LinkedIn gibi native dialog kullananlar için)
|
|
186
208
|
const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
209
|
+
const btnSel = sels.find(s => !s.includes("input[type")) || "button";
|
|
210
|
+
try {
|
|
211
|
+
const { dirname: dn } = await import("node:path");
|
|
212
|
+
const { fileURLToPath: fu } = await import("node:url");
|
|
213
|
+
const pkgDir = process.env.SOCIAL_AGENT_PKG || join(dn(fu(import.meta.url)), "../..");
|
|
214
|
+
execSync(
|
|
215
|
+
`npx tsx "${join(pkgDir, 'core/upload-helper.ts')}" ${CDP_PORT} "${btnSel}" "${filePath}"`,
|
|
216
|
+
{ stdio: "pipe", timeout: 15000 }
|
|
217
|
+
);
|
|
218
|
+
this.cmd("wait 3000");
|
|
219
|
+
// Editör modal'ı varsa kapat
|
|
220
|
+
try {
|
|
221
|
+
this.cmd(`eval "document.querySelectorAll('button').forEach(b => { var t=b.textContent.trim().toLowerCase(); if(t==='ileri' || t==='bitti' || t==='done' || t==='next' || t==='tamam') b.click() })"`);
|
|
222
|
+
this.cmd("wait 1000");
|
|
223
|
+
} catch {}
|
|
224
|
+
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
225
|
+
return;
|
|
226
|
+
} catch {}
|
|
227
|
+
|
|
190
228
|
throw new Error(`Upload: ${filePath}`);
|
|
191
229
|
}
|
|
192
230
|
}
|