| name | kariyer-outreach |
| description | Массовая рассылка откликов на турецкие вакансии Kariyer.net для лидгена. Триггеры: /kariyer-outreach, 'отклики kariyer', 'отклики турция', 'турецкие вакансии' |
/kariyer-outreach — Автоматические отклики на Kariyer.net
Полная спецификация: ~/kariyer-outreach/SPEC.md. Шаблон: ~/kariyer-outreach/templates/cover-letter-tr.md.
Вызов
/kariyer-outreach <количество> [запрос] [--флаги]
Аргументы
| Аргумент | Тип | Дефолт | Описание |
|---|
| количество | число | обязательный | Сколько откликов отправить |
| запрос | строка | "dijital pazarlama OR yapay zeka OR growth marketing" | Поисковый запрос |
| --query | строка | - | Альтернатива позиционному запросу |
| --min-salary | число | - | Минимальная TL |
| --dry-run | флаг | - | Поиск + генерация БЕЗ отправки |
| --preview | флаг | - | Показать текст перед каждой отправкой |
| --any-format | флаг | - | Не фильтровать по Uzaktan |
| --no-filter | флаг | - | Отключить exclusion filter |
| --check-responses | флаг | - | Только опрос inbox + basvurularim |
Инструменты
EVAL=~/kariyer-outreach/scripts/safari-eval.sh
CAPTCHA=~/kariyer-outreach/scripts/cliclick-captcha.sh
SNIFFER=~/kariyer-outreach/scripts/sniffer-install.js
DATA=~/kariyer-outreach-data
DB=$DATA/outreach.db
TEMPLATE=~/kariyer-outreach/templates/cover-letter-tr.md
Процесс
Шаг 0: INIT
- Прочитать
$DATA/sent.json
- Если corrupted: восстановить из
.bak или создать {"vacancies": []}
- Валидация: JSON парсится,
vacancies массив, каждый элемент имеет kariyer_id и status
- Дедупликация
- Backup:
cp $DATA/sent.json $DATA/sent.json.bak
- Проверить лимит: если
vacancies.length > 100 — предупреждение, если > 150 — подтверждение
- Парсить аргументы
- Safari bridge healthcheck:
$EVAL '"alive"'
- Если
-1728 — STOP и сообщить: "Safari → Develop → Allow JavaScript from Apple Events должно быть включено"
- Если OK — найти активный таб Kariyer или открыть:
$EVAL --nav "https://www.kariyer.net/profil"
Шаг 1: SEARCH
- Построить URL:
https://www.kariyer.net/is-ilanlari?kw={QUERY}&cl=remote (точный parameter name уточнить в Sprint 5 recon)
$EVAL --nav <url>
- Дождаться загрузки через poll:
(async () => {
for (var i=0; i<20; i++) {
if (document.querySelectorAll('[data-test="job-card"], article').length > 0) return "loaded";
await new Promise(r => setTimeout(r, 500));
}
return "timeout";
})()
- Infinite scroll если нужно (пока длина DOM растёт):
window.scrollTo(0, document.body.scrollHeight)
- Извлечь вакансии через
$EVAL:
Array.from(document.querySelectorAll('[data-test="job-card"], article[id^="job_"]')).map(c => ({
id: c.getAttribute('data-job-id') || c.id,
url: c.querySelector('a')?.href,
title: c.querySelector('[data-test="job-title"], h2, h3')?.textContent?.trim(),
company: c.querySelector('[data-test="company"]')?.textContent?.trim(),
city: c.querySelector('[data-test="city"]')?.textContent?.trim(),
remote: /uzaktan|remote/i.test(c.textContent),
salary: c.querySelector('[data-test="salary"]')?.textContent?.trim() || null
})).filter(v => v.id && v.title)
- Фильтрация (если НЕ --no-filter):
Исключить если title содержит:
stajyer, junior, jr., uzman yardımcısı, satış, çağrı merkezi, insan kaynakları, muhasebe, yazılım geliştirici, developer, mühendis, tasarımcı, designer, kurye, depo
- Дедупликация: исключить kariyer_id уже в sent.json
- Если набрано мало — scroll/pagination
Шаг 2: READ (для каждой вакансии)
$EVAL --nav <vacancy_url>
- Polling до загрузки
- Извлечь через
$EVAL:
- Название, компания, maaş (raw), açıklama (первые 1000 символов), location
- Проверить "Bu ilana zaten başvurdun" — если да: SKIP_ALREADY_APPLIED
- Парсинг зарплаты:
30.000 TL → 30000
30.000 - 50.000 TL → 40000 (среднее)
$2000 или 2000 USD → × 34 = 68000
€2000 или 2000 EUR → × 37 = 74000
brüt → × 0.72
Maaş bilgisi belirtilmemiş → null
Шаг 3: GENERATE
Сгенерировать кастом cover letter на турецком по шаблону $TEMPLATE:
OPENING (random из 5):
{ŞİRKET} ekibi, merhaba!
Merhaba {ŞİRKET}!
{ŞİRKET}, selam!
Selam! {POZİSYON} ilanınız için yazıyorum.
Merhaba ekip!
VACANCY_REACTION:
Anladığım kadarıyla, {1-2 предложения пересказа задач на турецком}.
OFFER (с зарплатой):
Size daha iyi bir teklifim var. Kadroya almak yerine, kendi şirketim üzerinden proje bazlı çalışmayı öneriyorum - {SALARY*0.8} TL, kadrolu maliyetinizin %20 altında.
OFFER (без):
Size daha iyi bir teklifim var. Kadroya almak yerine, kendi şirketim üzerinden proje bazlı çalışmayı öneriyorum - kadrolu bir uzmanın vergi ve sigorta dahil toplam maliyetinin %20 altında.
WHAT_I_DO (match по ключевым словам ilan, см. полный список в cover-letter-tr.md):
| Ключи TR | Модуль |
|---|
| dijital pazarlama, sosyal medya, içerik | AI içerik fabrikam otomatik post, görsel, video üretip 13 platforma yayınlıyor |
| lead, B2B, büyüme, satış geliştirme | Lead makinem şirketleri bulup zenginleştirip kişisel teklifler üretiyor |
| funnel, e-posta, webinar | Webinar funnel'ları otomatik ısıtma ve lead qualification |
| SEO, içerik pazarlaması | AI semantic core altında uzman metinler üretip yayınlıyor |
| CRM, segmentasyon | AI segmentasyon, kişisel e-postalar, trigger zincirleri |
| video, reels, YouTube | HeyGen + ElevenLabs AI video pipeline |
| influencer, blogger | AI influencer taraması, brief üretimi |
| yapay zeka, AI, otomasyon | Claude + GPT + n8n + MCP stack, 50+ proje |
SLOTS: Uzaktan, proje bazlı çalışıyorum. Aynı anda 5 müşteri alıyorum, şu an 2 slot boşta.
CTA:
Projeler: timzinin.com
Konuşmak için: calendly.com/timzinin
ENGLISH_FOLLOWUP (фиксированный в конце каждого письма):
---
(English: I'm an AI-automation marketer building content factories and lead machines for tech companies. Happy to chat in English — calendly.com/timzinin)
Anti-AI:
- Без тире (—), только дефис с пробелами ( - )
- Без клише:
inovatif çözümler, 360 derece hizmet, kapsamlı strateji, değer katmak, sürdürülebilir büyüme
- Без hedge:
belki, muhtemelen, sanırım, olabilir
- Активный залог:
kuruyorum, yönetiyorum
Шаг 4: SEND
- Если --dry-run: показать текст, записать SKIP_DRY_RUN, продолжить
- Если --preview: показать текст, спросить OK/Skip
- Click "Başvur" через
$EVAL:
(function(){
var btns = document.querySelectorAll("a, button, [role=button]");
for (var i=0; i<btns.length; i++) {
var t = (btns[i].textContent || "").trim();
if (t === "Başvur" || t === "Başvur Yap" || t === "Hemen Başvur") {
btns[i].click();
return "clicked:" + i;
}
}
return "not_found";
})()
- CAPTCHA check — если появилась press-and-hold:
document.body.textContent.indexOf("Basılı Tut") >= 0 || document.querySelector("[class*=captcha], [class*=px-]")
- Если да:
bash $CAPTCHA → exit 0 success, exit 2 retry (max 3), exit 1 — SKIP_NO_TEXTAREA+notify
- Wait for Ön Yazı modal (если есть):
(async () => {
for (var i=0; i<10; i++) {
if (document.querySelector("textarea, [contenteditable=true]")) return "found";
await new Promise(r => setTimeout(r, 500));
}
return "no_ta";
})()
- Если textarea нет: SKIP_NO_ON_YAZI или SKIP_NO_TEXTAREA
- Установить фокус на textarea + заполнить через pbcopy + cmd-v (для турецких символов):
echo -n "$COVER_LETTER" | pbcopy
$EVAL '(function(){
var ta = document.querySelector("textarea");
if (ta) { ta.focus(); return "focused"; }
return "no_ta";
})()'
osascript -e 'tell application "System Events" to keystroke "v" using command down'
- Триггер input event:
(function(){
var ta = document.querySelector("textarea");
if (ta) ta.dispatchEvent(new Event("input", {bubbles:true}));
return "dispatched";
})()
- Click "Gönder" / "Başvuruyu Gönder":
(function(){
var btns = document.querySelectorAll("button");
for (var i=0; i<btns.length; i++) {
var t = (btns[i].textContent || "").trim();
if (t.indexOf("Gönder") >= 0 || t.indexOf("Başvur") >= 0) {
var r = btns[i].getBoundingClientRect();
if (r.y > 200) { btns[i].click(); return "clicked:" + i; }
}
}
return "not_found";
})()
- Poll success "Başvurun alındı" / "Başvurun iletildi":
(async () => {
for (var i=0; i<10; i++) {
var t = document.body.textContent;
if (t.indexOf("alındı") >= 0 || t.indexOf("iletildi") >= 0) return "ok";
await new Promise(r => setTimeout(r, 500));
}
return "timeout";
})()
- Screenshot failure →
$DATA/artifacts/fail-$(date +%s).png
Шаг 5: LOG
Atomic write в $DATA/sent.json:
{
"id": "job_1234567",
"kariyer_id": "job_1234567",
"company": "ABC Teknoloji",
"title": "Dijital Pazarlama Uzmanı",
"salary": 40000,
"salary_currency": "TRY",
"salary_raw": "30.000 - 50.000 TL",
"status": "SENT",
"reason": null,
"date": "2026-04-05",
"message_preview": "ABC Teknoloji ekibi, merhaba!...",
"captcha_triggered": false,
"captcha_solve_ms": null
}
INSERT в SQLite:
INSERT INTO vacancies (kariyer_id, company, title, salary_min, salary_currency, city, remote, description, url)
VALUES (?, ?, ?, ?, 'TRY', ?, ?, ?, ?);
INSERT INTO outreach (vacancy_id, message_text, message_template, status)
VALUES (last_insert_rowid(), ?, 'cover-letter-tr', ?);
Reason codes: SENT, SKIP_ALREADY_APPLIED, SKIP_NO_TEXTAREA, SKIP_NO_ON_YAZI, SKIP_DRY_RUN, SKIP_USER, FAILED_NO_CONFIRMATION, FAILED_ANTI_BOT, FAILED_SAFARI_ERROR, FAILED_CAPTCHA_SOLVE
Дневной лог $DATA/log-YYYY-MM-DD.md:
| # | kariyer_id | Şirket | Pozisyon | Maaş | Статус | Reason | Captcha |
Шаг 6: REPEAT
Throttle: 5-10 сек random.
После каждых 5 отправок: pause 60s + navigate на https://www.kariyer.net/ (остыть PerimeterX sensor).
Лимит: 15 за сессию (осторожнее чем HH).
Hard stop: 3 подряд FAILED_ANTI_BOT или FAILED_CAPTCHA_SOLVE.
Шаг 7: REPORT
Kariyer Outreach: завершено
Отправлено: X/N
Пропущено: Y (breakdown по reason codes)
CAPTCHA triggered: Z (solved: A, failed: B, avg Cms)
Топ вакансии: ...
Всего за всё время: N
Check responses режим (--check-responses)
- Gmail MCP:
mcp__claude_ai_Gmail__gmail_search_messages(q='from:@kariyer.net newer_than:1d')
- Safari eval
$EVAL --nav https://www.kariyer.net/basvurularim → парсинг статусов
- Safari eval
$EVAL --nav https://www.kariyer.net/mesajlar → парсинг inbox
- UPSERT в responses table
- Только новые
INTERVIEW / MESSAGE → tg-communicator notify_user