From eopowers
Сканира eop.bg за отворени обществени поръчки, категоризира и приоритизира по ROI. Използва се при търсене на нови поръчки за участие.
How this skill is triggered — by the user, by Claude, or both
Slash command
/eopowers:eop-scanThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
```
Прогрес на сканиране:
- [ ] Отваряне на eop.bg и прилагане на филтри
- [ ] Извличане на всички отворени поръчки
- [ ] Извличане на метрики от документите (Phase 2)
- [ ] Категоризация по тип
- [ ] Приоритизация по ROI
- [ ] Представяне на резултати
- [ ] Запис на scan summary файл
- [ ] Избор на поръчки за участие
Прочети ./eopowers/domain.md с Read tool. Ако файлът не съществува — съобщи: "Няма домейн конфигурация. Стартирайте /init първо." и спри.
Извлечи от файла:
Използвай тези данни навсякъде по-долу вместо хардкоднати стойности.
Отвори https://app.eop.bg/today с browser_navigate.
Страницата е Angular SPA — изчакай съдържанието да се зареди. Използвай browser_snapshot (НЕ screenshot) за да провериш дали е заредено.
ВАЖНО: eop.bg третира множество ключови думи като AND (И), не OR (ИЛИ). Търсенето "СМР" + "обновяване" връща 0 резултата. Затова всяка ключова дума се търси ОТДЕЛНО.
Събери всички ключови думи за търсене:
За ВСЯКА ключова дума изпълни:
Филтърът "отворени за участие" е приложен по подразбиране — не го променяй.
Обходи всички страници с резултати (всяка показва ~10 записа).
За навигация между страниците: натисни бутоните с номера в пагинацията (1, 2, 3... N).
ВАЖНО: Използвай exact: true при търсене на бутон по номер, за да избегнеш конфликт (напр. бутон "2" vs "20").
Пример: page.getByRole('button', { name: '2', exact: true })
Изчакай 2 секунди след всяка смяна на страница за зареждане на данни.
За всеки запис извлечи:
/today/575004 → 575004)За по-бързо извличане, използвай browser_run_code вместо отделни snapshot-и:
async (page) => {
const items = await page.$$eval('a[href*="/today/"]', links => {
return links
.filter(a => a.parentElement?.tagName?.includes('TENDER') || a.closest('li'))
.map(a => ({
id: a.getAttribute('href').split('/').pop(),
text: a.textContent.replace(/\s+/g, ' ').trim().substring(0, 300)
}))
.filter(item => item.id && item.id.match(/^\d+$/));
});
return JSON.stringify(items);
}
Дедупликация: След обхождане на всички ключови думи и всички страници, премахни дублирани записи по ID. Логирай:
Регионално групиране: Ако в domain.md има секция "Регион":
След извличане на основните данни и прилагане на keyword филтъра, за ВСЯКА поръчка, която не е изключена:
Покажи прогрес: "📐 Анализирам [N]/[Total]: [title]..."
Ако ./eopowers/offers/<offer-id>/meta.md вече съществува — пропусни (вече е анализирана).
Създай директория:
mkdir -p ./eopowers/offers/<offer-id>/attachments
Навигирай до https://app.eop.bg/today/<offer-id> с browser_navigate.
Направи browser_snapshot за да намериш секцията "Прикачени файлове".
5b. От snapshot-а извлечи прогнозната стойност — търси текст "Прогнозна стойност (без ДДС)" и стойността до него (формат: EUR X XXX XXX,XX). Парсвай: премахни интервалите между цифрите, замени запетая с точка. Пример: EUR 1 303 052,27 → 1303052.27.
Изтегляне на документи (приоритет: Export ZIP):
Метод 1 (предпочитан): Export ZIP
T<id>-Експорт-*.zip..playwright-mcp/. След сваляне, премести:
mv .playwright-mcp/T*-Експорт-*.zip "./eopowers/offers/<offer-id>/attachments/"
Метод 2 (fallback): Индивидуални файлове
mv ".playwright-mcp/<downloaded-filename>" "./eopowers/offers/<offer-id>/attachments/"
Разархивиране и нормализация на пътища:
python3 -c "
import zipfile, os, subprocess, shutil
attachments = './eopowers/offers/<offer-id>/attachments'
# Extract ZIPs
for f in [x for x in os.listdir(attachments) if x.endswith('.zip')]:
zpath = os.path.join(attachments, f)
zipfile.ZipFile(zpath).extractall(attachments)
# Extract RARs
for f in [x for x in os.listdir(attachments) if x.endswith('.rar')]:
rpath = os.path.join(attachments, f)
subprocess.run(['unrar', 'x', '-o+', rpath, attachments + '/'], capture_output=True)
# Normalize Windows backslash paths — flatten all nested files to attachments/
for root, dirs, files in os.walk(attachments):
for fname in files:
src = os.path.join(root, fname)
dst = os.path.join(attachments, fname)
if src != dst and not os.path.exists(dst):
shutil.move(src, dst)
# Remove empty subdirectories
for root, dirs, files in os.walk(attachments, topdown=False):
if root != attachments and not os.listdir(root):
os.rmdir(root)
"
Конвертирай .doc файлове в .docx за парсване (НЕ .txt — губи таблици):
for f in ./eopowers/offers/<offer-id>/attachments/*.doc; do
[ -f "$f" ] && libreoffice --headless --convert-to docx --outdir "$(dirname "$f")" "$f" 2>/dev/null
done
Парсвай документите за метрики (виж по-долу).
9b. Visual PDF fallback за сканирани документи:
Ако Python скриптът отпечата NEED_VISUAL_PDF_FALLBACK (т.е. не всички метрики са намерени и има PDF файлове), използвай Read tool за визуално четене на PDF:
Read: ./eopowers/offers/<offer-id>/attachments/<file>.pdf (pages: "1-5")
За да активираш fallback-а, добави в края на Python скрипта (преди for key in patterns:):
# Signal for visual fallback
if len(results) < len(patterns):
missing = [k for k in patterns if k not in results]
pdf_candidates = [f for f in files if f.endswith('.pdf')]
if pdf_candidates:
print(f'NEED_VISUAL_PDF_FALLBACK missing={",".join(missing)} files={",".join(os.path.basename(f) for f in pdf_candidates[:2])}')
Приоритет на файлове: спазвай реда от секция "Приоритет на файлове за метрики" в domain.md.
Генерирай Python скрипт за извличане на метрики, като адаптираш шаблона по-долу спрямо domain.md:
file_priority функцията — генерирай от секция "Приоритет на файлове за метрики" в domain.md. Всеки ред от списъка става условие с нарастващ приоритет (0, 1, 2...).patterns dict — попълни от секция "Метрики" в domain.md: ключ = кратко име (lowercase), стойност = regex от domain.md.min_values dict — попълни от "Минимална стойност" на всяка метрика в domain.md.python -c "
import re, glob, subprocess, os
offer_id = '<offer-id>'
attachments_dir = f'./eopowers/offers/{offer_id}/attachments'
all_files = glob.glob(f'{attachments_dir}/*.docx')
pdf_files = glob.glob(f'{attachments_dir}/*.pdf')
# ГЕНЕРИРАЙ тази функция от domain.md 'Приоритет на файлове за метрики'
def file_priority(f):
name = os.path.basename(f).lower()
# [ред 1 от списъка] → return 0
# [ред 2 от списъка] → return 1
# ... и т.н.
return 99 # default
files = sorted(all_files, key=file_priority)
files.extend(sorted(pdf_files, key=file_priority))
# ПОПЪЛНИ от domain.md секция 'Метрики' — за всяка метрика:
# ключ = кратко_име.lower(), стойност = regex от domain.md
patterns = {
# 'зп': r'(?:Застроена площ|ЗП)...', ← пример
}
# ПОПЪЛНИ от domain.md — за всяка метрика 'Минимална стойност'
min_values = {
# 'зп': 10, ← пример
}
results = {}
for fpath in files:
try:
if fpath.endswith('.pdf'):
try:
import subprocess as sp
r = sp.run(['pdftotext', '-l', '5', fpath, '-'], capture_output=True, text=True, timeout=15)
text = r.stdout
except FileNotFoundError:
try:
import pdfplumber
with pdfplumber.open(fpath) as pdf:
text = '\n'.join(p.extract_text() or '' for p in pdf.pages[:5])
except ImportError:
continue
elif fpath.endswith('.docx'):
from docx import Document
doc = Document(fpath)
text = '\n'.join(p.text for p in doc.paragraphs)
for table in doc.tables:
for row in table.rows:
text += '\n' + ' '.join(cell.text for cell in row.cells)
else:
with open(fpath, 'r', errors='ignore') as f:
text = f.read()
for key, pat in patterns.items():
if key not in results:
m = re.search(pat, text, re.IGNORECASE)
if m:
val = m.group(1).replace(' ', '').replace(',', '.')
num = float(val)
if num >= min_values.get(key, 0):
results[key] = num
if len(results) == len(patterns):
break
except Exception:
continue
# Print results dynamically for each metric defined in domain.md
for key in patterns:
print(f'{key.upper()}={results.get(key, \"—\")}')
"
Изчисли рентабилност по формулата от domain.md (секция "Праг за рентабилност"). Ако стойността е под прага — маркирай с предупреждението от domain.md.
Използвай таблиците от domain.md (заредени в началото).
Алгоритъм:
ROI = (V * Cm * Ct) / (D * Cp)
V = Прогнозна стойност (нормализирана 0-1 спрямо максималната намерена стойност)
Cm = Съответствие с компетенции (1.0 = пълно, 0.5 = частично, 0.2 = несъответствие)
Ct = Времеви буфер (дни до краен срок / 30, максимум 1.0)
D = Сложност (1 = проста, 2 = средна, 3 = сложна; по брой обособени позиции)
Cp = Конкуренция (1 = Договаряне без обявление, 1.5 = Публично състезание, 2 = Открита процедура)
Представи като оценка 1-10 (по-висока = по-атрактивна). Нормализирай крайния резултат: score = min(10, ROI * 10).
За стойността на Cm: прочети ./eopowers/company-profile.md и провери компетенциите. Ако файлът не съществува — използвай 0.5 за всички поръчки.
Покажи резултатите като Markdown таблица, сортирана по ROI (низходящо).
Колоните се генерират динамично от domain.md:
Фиксирани колони: #, Поръчка, Възложител, Стойност (EUR), Категория, ROI
За всяка метрика в domain.md: добави колона "[Кратко име] ([Единица])"
За всяка метрика с праг за рентабилност: добави колона "EUR/[Единица] [Кратко име]"
⚠️ — стойност под прага от domain.md
"—" — метриката не може да бъде извлечена от документите
Ако няма намерени резултати — съобщи: "Не бяха намерени отворени поръчки с текущите филтри."
След представяне на таблицата (преди въпроса за избор), автоматично запиши файл ./eopowers/offers/scan-YYYY-MM-DD.md със следното съдържание:
# Сканиране — YYYY-MM-DD
## Параметри
- Филтър: "[ключова дума от domain.md]"
- Общо резултати: [total] (извлечени: [raw], уникални: [unique])
- Сканирани страници: 1-N
- Изключени (отрицателни ключови думи): [count]
- Включени след филтриране: [count] (от уникалните)
- Обогатени с метрики: [count]
## Изключени поръчки
| ID | Наименование | Причина |
|----|-------------|---------|
| [id] | [title] | [matched negative keyword] |
## Включени по категории
| Категория | Брой |
|-----------|------|
| [category] | [count] |
## Обогатени поръчки (Phase 2 — с метрики)
[full enriched results table — колони генерирани динамично от domain.md]
[Легенда от domain.md — праг за рентабилност и символ за предупреждение]
## Всички необогатени поръчки
| ID | Обект | Възложител |
|----|-------|-----------|
[offers where metrics couldn't be extracted]
Потвърди: "💾 Резултатите са записани в ./eopowers/offers/scan-YYYY-MM-DD.md"
След представяне на таблицата, попитай:
Кои поръчки искате да разгледате? (въведете номера, разделени със запетая)
За всяка избрана поръчка:
mkdir -p ./eopowers/offers/<offer-id>
meta.md в директорията:# Поръчка <offer-id>
- Наименование: [title]
- Възложител: [buyer]
- Прогнозна стойност: [value] EUR
- Краен срок: [deadline]
- Начин на възлагане: [procedure type]
- Категория: [category]
- ROI оценка: [score]
[За всяка метрика от domain.md:]
- [Пълно име на метриката]: [value] [единица]
- EUR/[единица] ([кратко име]): [value]
- URL: https://app.eop.bg/today/<offer-id>
- Дата на сканиране: [current date]
Потвърди записа: "Записани са [N] поръчки в ./eopowers/offers/."
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub lutherwaves/eopowers --plugin eopowers