social-agent-cli 4.0.0 → 4.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 +24 -13
- package/cli.ts +4 -6
- package/core/upload-helper.ts +57 -0
- package/maps/linkedin.json +11 -44
- package/package.json +2 -1
- package/platforms/browser/driver.ts +56 -8
package/ai/runner.ts
CHANGED
|
@@ -15,16 +15,22 @@ export async function runCommand(platform: string, command: string): Promise<voi
|
|
|
15
15
|
const url = BROWSER_PLATFORMS[platform];
|
|
16
16
|
if (!url) { console.log(`"${platform}" desteklenmiyor.`); process.exit(1); }
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// --content "..." ve --image ... parse et
|
|
19
|
+
let content = "";
|
|
20
|
+
let image = "";
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
.replace(/^
|
|
27
|
-
|
|
22
|
+
const contentMatch = command.match(/--content\s+([\s\S]+?)(?=\s+--|\s*$)/);
|
|
23
|
+
const imageMatch = command.match(/--image\s+(\S+)/);
|
|
24
|
+
|
|
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
|
+
}
|
|
28
34
|
|
|
29
35
|
// Map seç
|
|
30
36
|
const map = loadMap(platform, "post");
|
|
@@ -47,11 +53,12 @@ export async function runCommand(platform: string, command: string): Promise<voi
|
|
|
47
53
|
|
|
48
54
|
let steps = [...map.steps];
|
|
49
55
|
|
|
50
|
-
// Image varsa:
|
|
56
|
+
// Image varsa: metin yazma step'inden ÖNCE upload step ekle (LinkedIn'de önce fotoğraf yüklenmeli)
|
|
51
57
|
if (image && !steps.some(s => s.action === "upload")) {
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
const typeIdx = steps.findIndex(s => s.action === "type");
|
|
59
|
+
const insertIdx = typeIdx > -1 ? typeIdx : steps.findLastIndex(s => s.action === "click");
|
|
60
|
+
if (insertIdx > -1) {
|
|
61
|
+
steps.splice(insertIdx, 0, {
|
|
55
62
|
action: "upload" as any,
|
|
56
63
|
description: "Fotoğraf yükle",
|
|
57
64
|
selector: "input[type='file']",
|
|
@@ -63,7 +70,9 @@ export async function runCommand(platform: string, command: string): Promise<voi
|
|
|
63
70
|
|
|
64
71
|
// Step'leri çalıştır
|
|
65
72
|
const errors: string[] = [];
|
|
73
|
+
let aborted = false;
|
|
66
74
|
for (let idx = 0; idx < steps.length; idx++) {
|
|
75
|
+
if (aborted) break;
|
|
67
76
|
const s = steps[idx];
|
|
68
77
|
try {
|
|
69
78
|
console.log(` ${idx + 1}. ${s.description}`);
|
|
@@ -71,6 +80,8 @@ export async function runCommand(platform: string, command: string): Promise<voi
|
|
|
71
80
|
} catch (err: any) {
|
|
72
81
|
errors.push(`Step ${idx + 1} (${s.description}): ${err.message}`);
|
|
73
82
|
console.log(` ✗ ${err.message}`);
|
|
83
|
+
// Upload hatası → geri kalanı çalıştırma (fotoğrafsız post göndermesin)
|
|
84
|
+
if (s.action === "upload") { aborted = true; }
|
|
74
85
|
}
|
|
75
86
|
}
|
|
76
87
|
|
package/cli.ts
CHANGED
|
@@ -189,13 +189,11 @@ switch (command) {
|
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
case "run": {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const runCmd =
|
|
195
|
-
? filteredArgs.slice(1).join(" ")
|
|
196
|
-
: 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(" ");
|
|
197
195
|
if (!runCmd) {
|
|
198
|
-
console.log('Kullanım: social-agent run <platform> "
|
|
196
|
+
console.log('Kullanım: social-agent run <platform> --content "mesaj" [--image /path/to/img]');
|
|
199
197
|
break;
|
|
200
198
|
}
|
|
201
199
|
await runCommand(runPlatform, runCmd);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload helper - CDP fileChooser intercept ile dosya yükler
|
|
3
|
+
* LinkedIn gibi native file dialog kullanan platformlar için
|
|
4
|
+
*
|
|
5
|
+
* Kullanım: npx tsx core/upload-helper.ts <cdp-port> <click-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 pages = browser.contexts()[0].pages();
|
|
19
|
+
const page = pages[pages.length - 1];
|
|
20
|
+
|
|
21
|
+
// fileChooser event'ini dinlemeye başla
|
|
22
|
+
let resolved = false;
|
|
23
|
+
const fcPromise = page.waitForEvent("filechooser", { timeout: 10000 });
|
|
24
|
+
|
|
25
|
+
// Butona tıkla - fileChooser tetiklenecek
|
|
26
|
+
// Birden fazla selector dene
|
|
27
|
+
const selectors = selector.split("|");
|
|
28
|
+
for (const sel of selectors) {
|
|
29
|
+
try {
|
|
30
|
+
// Playwright text selector
|
|
31
|
+
if (sel.startsWith("text=")) {
|
|
32
|
+
await page.click(sel);
|
|
33
|
+
} else {
|
|
34
|
+
// CSS selector veya role selector
|
|
35
|
+
const el = page.locator(sel).first();
|
|
36
|
+
if (await el.isVisible({ timeout: 2000 })) {
|
|
37
|
+
await el.click();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const fc = await fcPromise;
|
|
45
|
+
await fc.setFiles(filePath);
|
|
46
|
+
resolved = true;
|
|
47
|
+
console.log("OK");
|
|
48
|
+
} catch (e: any) {
|
|
49
|
+
console.error("FAIL:", e.message.substring(0, 100));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
upload().catch((e) => {
|
|
55
|
+
console.error(e.message);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
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": "4.
|
|
3
|
+
"version": "4.2.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": {
|
|
@@ -146,16 +146,31 @@ export class BrowserDriver {
|
|
|
146
146
|
|
|
147
147
|
case "click": {
|
|
148
148
|
const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
|
|
149
|
+
// 1. CSS selector ile dene
|
|
149
150
|
for (const sel of sels) {
|
|
150
151
|
if (sel.includes("{{")) continue;
|
|
151
152
|
try { this.cmd(`click "${sel}"`); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
|
|
152
153
|
}
|
|
153
|
-
|
|
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}`);
|
|
154
168
|
}
|
|
155
169
|
|
|
156
170
|
case "type": {
|
|
157
171
|
const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
|
|
158
|
-
const escaped = val.replace(/"/g, '\\"');
|
|
172
|
+
const escaped = val.replace(/"/g, '\\"').replace(/'/g, "\\'");
|
|
173
|
+
// 1. CSS selector ile dene
|
|
159
174
|
for (const sel of sels) {
|
|
160
175
|
if (sel.includes("{{")) continue;
|
|
161
176
|
try {
|
|
@@ -164,6 +179,12 @@ export class BrowserDriver {
|
|
|
164
179
|
return;
|
|
165
180
|
} catch {}
|
|
166
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 {}
|
|
167
188
|
throw new Error(`Type: ${sels[0]}`);
|
|
168
189
|
}
|
|
169
190
|
|
|
@@ -175,17 +196,44 @@ export class BrowserDriver {
|
|
|
175
196
|
case "upload": {
|
|
176
197
|
const filePath = val || imagePath || "";
|
|
177
198
|
if (!filePath || filePath.includes("{{")) break;
|
|
178
|
-
|
|
199
|
+
|
|
200
|
+
// 1. agent-browser upload ile dene (X'te çalışır)
|
|
179
201
|
try {
|
|
180
|
-
this.cmd(`upload "input[type='file']" "${filePath}"`,
|
|
202
|
+
this.cmd(`upload "input[type='file']" "${filePath}"`, 5000);
|
|
181
203
|
if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
|
|
182
204
|
return;
|
|
183
205
|
} catch {}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
206
|
+
|
|
207
|
+
// 2. Playwright fileChooser ile dene (LinkedIn gibi native dialog kullananlar için)
|
|
208
|
+
// Snapshot'tan medya butonunun aria-label'ını bul
|
|
209
|
+
let btnSel = "button[aria-label*='Medya']";
|
|
210
|
+
try {
|
|
211
|
+
const snap = this.cmd("snapshot -i -c", 5000);
|
|
212
|
+
const mediaMatch = snap.match(/button "([^"]*(?:Medya|media|Fotoğraf|photo|Dosya|file|video)[^"]*)"/i);
|
|
213
|
+
if (mediaMatch) btnSel = `button[aria-label='${mediaMatch[1]}']`;
|
|
214
|
+
} catch {}
|
|
215
|
+
try {
|
|
216
|
+
const thisDir = new URL(".", import.meta.url).pathname;
|
|
217
|
+
const pkgDir = process.env.SOCIAL_AGENT_PKG || join(thisDir, "../..");
|
|
218
|
+
console.log(` [upload] helper: ${btnSel}`);
|
|
219
|
+
const helperResult = execSync(
|
|
220
|
+
`npx tsx "${join(pkgDir, 'core/upload-helper.ts')}" ${CDP_PORT} "${btnSel}" "${filePath}"`,
|
|
221
|
+
{ encoding: "utf-8", timeout: 60000 }
|
|
222
|
+
).trim();
|
|
223
|
+
if (helperResult === "OK") {
|
|
224
|
+
// Editör modal kapatma - "İleri" butonuna tıkla (3 saniye bekle, modal yüklensin)
|
|
225
|
+
setTimeout(() => {}, 3000);
|
|
226
|
+
try {
|
|
227
|
+
execSync(`agent-browser --cdp ${CDP_PORT} wait 3000`, { stdio: "pipe", timeout: 10000 });
|
|
228
|
+
execSync(`agent-browser --cdp ${CDP_PORT} eval "document.querySelectorAll('button').forEach(b => { var t=b.textContent.trim().toLowerCase(); if(t==='ileri' || t==='bitti' || t==='done' || t==='next') b.click() })"`, { stdio: "pipe", timeout: 5000 });
|
|
229
|
+
execSync(`agent-browser --cdp ${CDP_PORT} wait 2000`, { stdio: "pipe", timeout: 5000 });
|
|
230
|
+
} catch {}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
} catch (e: any) {
|
|
234
|
+
console.log(` [upload] helper hata: ${e.message?.substring(0, 80)}`);
|
|
188
235
|
}
|
|
236
|
+
|
|
189
237
|
throw new Error(`Upload: ${filePath}`);
|
|
190
238
|
}
|
|
191
239
|
}
|