Инициализировать репозиторий «Дейл»

Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
This commit is contained in:
Rustam Khalimov
2026-09-11 02:50:17 +03:00
commit 9e07568ddd
1402 changed files with 177470 additions and 0 deletions
@@ -0,0 +1,253 @@
"""Базовые константы: палитры, стадии, промпты, валюты, провайдеры AI.
Значения согласованы с фронтенд-прототипом (frontend/src/data.js),
чтобы интеграция была безболезненной.
"""
# Палитра колонок-досок (по умолчанию)
PALETTE = ["#818cf8", "#fbbf24", "#22d3ee", "#e879f9", "#34d399", "#fb7185", "#a78bfa", "#f97316"]
# Колонки не создаются по умолчанию: их создаёт пользователь или предлагает
# ИИ (suggested=TRUE — ждёт решения пользователя).
# Служебные колонки дашборда
SERVICE_COLS = {"inbox", "archive", "trash", "taken"}
# Стадии канбана «Выбранных»
PIPELINE_STAGES = [
{"id": "planned", "name": "Запланировано", "color": "#818cf8", "terminal": False},
{"id": "reply", "name": "Отклик", "color": "#38bdf8", "terminal": False},
{"id": "agree", "name": "Согласование", "color": "#a78bfa", "terminal": False},
{"id": "work", "name": "В работе", "color": "#fbbf24", "terminal": False},
{"id": "review", "name": "Проверка", "color": "#f97316", "terminal": False},
{"id": "ready", "name": "Готово", "color": "#4ade80", "terminal": False},
{"id": "hold", "name": "Отложено", "color": "#94a3b8", "terminal": False},
{"id": "finished", "name": "Выполнено", "color": "#2bd576", "terminal": True},
{"id": "rejected", "name": "Отклонено", "color": "#ff6b6b", "terminal": True},
]
# Валюты и мок-курсы (до первого успешного запроса к ЦБ)
CURRENCIES = [
{"code": "RUB", "name": "Российский рубль", "symbol": ""},
{"code": "USD", "name": "Доллар США", "symbol": "$"},
{"code": "EUR", "name": "Евро", "symbol": ""},
{"code": "CNY", "name": "Китайский юань", "symbol": "¥"},
{"code": "KZT", "name": "Казахстанский тенге", "symbol": ""},
{"code": "BYN", "name": "Белорусский рубль", "symbol": "Br"},
{"code": "USDT", "name": "Tether (USDT)", "symbol": ""},
{"code": "GBP", "name": "Британский фунт", "symbol": "£"},
]
MOCK_RATES = {
"RUB": 1.0,
"USD": 92.5,
"EUR": 99.9,
"CNY": 13.1,
"KZT": 0.19,
"BYN": 28.6,
"USDT": 92.5,
"GBP": 117.4,
}
CBR_URL = "https://www.cbr-xml-daily.ru/daily_json.js"
# Стоп-фразы по умолчанию (этап 1 фильтра — без ИИ)
DEFAULT_STOP_PHRASES = ["взаимный пиар", "резюме", "ищу работу", "набор в команду"]
DEFAULT_MIN_LEN = 24
# Промпты. В тексте можно использовать плейсхолдеры, которые подставляются из
# настроек «Сферы и ключей» при каждом вызове ИИ:
# {domain} — domainDescription (что для вас заявка/лид, ваша сфера)
# {keywords} — domainKeywords (общие слова-маркеры заявок)
DEFAULT_AI_PROMPT = (
"Ты — классификатор входящих сообщений. Сообщение — ЗАЯВКА (лид), только если в нём есть конкретный"
" запрос или предложение по делу: заказ услуги/товара/работы, поиск исполнителя или найм человека."
" Направление вашей сферы:\n{domain}\n\n"
"Частые слова-маркеры заявок в вашей сфере: {keywords}\n\n"
"Определи по тексту:\n"
"1. is_spam — true, если это НЕ заявка: служебные сообщения (коды входа/подтверждения, уведомления),"
" приветствия и поздравления, флуд и обсуждения без задачи, вопросы без конкретики, реклама, скам,"
" фин. пирамиды, резюме соискателей, взаимный пиар, приглашения в чаты, рассылки."
" Если сомневаешься — ставь true (лучше пропустить сообщение, чем засорить карточками)\n"
"2. board — id подходящей доски из списка. Выбирай по КРИТЕРИЯМ колонки"
" (в скобках указаны её направление, тематика, ключевые слова, уровень, бюджет, описание), а не только по названию;"
" если ни одна колонка не подходит под текст — верни null (карточка пойдёт в «Неразобранное»)\n"
"3. is_vacancy — true, если это НАЙМ/постоянная или проектная занятость: ищут человека в команду"
" (признаки: вакансия, грейд/уровень, «в компанию», hr, зарплата за месяц);"
" false — разовая сделка: заказ/услуга/товар (фриланс, подряд, «нужно сделать/купить», цена за работу)\n"
"4. title — короткий заголовок (4–9 слов, без эмодзи, хэштегов и знаков препинания в конце)."
" Запрещено: markdown, ссылки в любом виде ([текст](url), голые url) — только чистый текст\n"
"5. company — кто разместил заявку: компания/бренд/частное лицо/заказчик. Только факт из текста;"
" не указано — верни пустую строку \"\"\n"
"6. format — формат работы/выполнения: удалённо/офис/гибрид, город, график. Коротко; нет — \"\"\n"
"7. task — 1–2 предложения: что за задача/роль и в чём суть (для кого, что нужно сделать)."
" Ёмко, без пересказа всего объявления и без служебных строк\n"
"8. requirements — JSON-массив строк ключевых требований к кандидату/исполнителю"
" (что нужно уметь/иметь, пункты «Требования/Обязанности/Нужно»). Нет — []\n"
"9. plus — JSON-массив строк «будет плюсом»/«приветствуется»/«желательно». Нет — []\n"
"10. conditions — условия одной строкой: оплата/ЗП/вилка, сроки, объём, тип занятости. Нет — \"\"\n"
"11. stack — JSON-массив строк (не строка!): технологии/предметы/услуги/материалы из заявки."
" Названия пиши слитно как в оригинале: \".NET\", \"C#\", \"Node.js\", \"ASP.NET Core\" — не разбивай на отдельные буквы (2–8)\n"
"12. budget — объект { from, to, currency }: одна сумма → from=to=X; «до X» → from=null, to=X;"
" диапазон «от X до Y» → from=X, to=Y. Если суммы нет — null\n"
"13. contacts — JSON-массив строк контактов ДЛЯ СВЯЗИ: телефон, email, @username (не бот),"
" ссылка на профиль человека (LinkedIn, t.me/…). НЕ включай ботов, каналы/группы и ссылки на"
" вакансию/пост/форму. Если контакта в тексте нет — пустой массив []\n"
"Поля 5–10 — это блок «О заявке» на карточке: заполняй их только фактами из текста, ничего не выдумывай."
" Запрещено: markdown-разметка, ссылки в любом виде, эмодзи, хэштеги, «от»/«привет», дословное копирование исходника.\n"
"Отвечай строго в формате JSON."
)
DEFAULT_AI_FILTER_PROMPT = (
"Ты — страж входящих сообщений каналов. Пропускай только реальные заявки/лиды по вашей сфере"
" (заказ, услуга, товар, найм — с конкретикой).\n"
"Ваша сфера и что считать заявкой:\n{domain}\n\n"
"Слова-маркеры заявок: {keywords}\n\n"
"НЕ пропускай:\n"
"- служебные сообщения: коды входа/подтверждения, уведомления, приветствия, поздравления\n"
"- просто сообщения без задачи и конкретики: флуд, обсуждения, вопросы «кто работал с …»\n"
"- рекламу и саморекламу\n"
"- скам, фин. пирамиды, «заработок»\n"
"- резюме, поиск работы соискателями\n"
"- взаимный пиар, приглашения в чаты\n"
"- рассылки и дайджесты без прямых заявок\n"
"При сомнении — не пропускай.\n"
'Верни строго JSON: { "pass": true|false, "reason": "причина отказа или null" }'
)
# Промпт структуры карточки (блок «О заявке»). Отдельный от классификатора:
# задаёт, какие поля и как заполнять, чтобы все карточки имели одинаковую
# структуру текста (разной длины). Добавляется к промпту классификатора
# отдельным блоком (см. services/ai.classify). Редактируется в UI.
DEFAULT_AI_CARD_PROMPT = (
"Ты также возвращаешь содержимое блока «О заявке» карточки — поля company, format,"
" task, requirements, plus, conditions. Карточка всегда собирается из одних и тех же блоков:"
" одинаковая структура у всех карточек, различается только длина.\n"
"1. company — кто ищет/разместил: компания, бренд, агентство, частное лицо, заказчик. Одно предложение, факт из текста.\n"
"2. format — формат работы: удалённо/офис/гибрид/разъездной, город/страна, график (5/2, full-time, part-time). Коротко.\n"
"3. task — 1–2 предложения по шаблону: что за задача/роль → для кого → что нужно сделать/какой результат."
" Пиши ёмко, не пересказывай объявление дословно.\n"
"4. requirements — вынеси сюда только реальные требования/обязанности из текста"
" (пункты списков «Требования», «Обязанности», «Что предстоит делать», «Нужно»):"
" каждый пункт короткой строкой в JSON-массиве. Не придумывай сверх текста.\n"
"5. plus — только то, что отмечено как «будет плюсом»/«приветствуется»/«желательно»."
" Нет таких пунктов — пустой массив [].\n"
"6. conditions — условия одной строкой: оплата/ЗП/вилка/гонорар, сроки, объём, тип занятости."
" Не дублируй budget-числа в других полях.\n"
"Для не-IT сфер «стек/требования» означают материалы, услуги, навыки, инструменты — по смыслу заявки."
" Запрещено во всех полях: markdown-разметка, ссылки в любом виде ([текст](url), голые url),"
" эмодзи, хэштеги, «от»/«привет»/служебные строки канала."
)
# Дефолтные маркеры локального разбора (настраиваются в UI «Сфера и ключи»).
DEFAULT_HIRE_MARKERS = [
"вакансия", "вакансию", "вакансии", "вакантна", "вакант", "нанимаем", "найм", "full-time",
"на постоянную", "занятость", "в офис", "официальное оформление", "пятидневка",
"грейд", "в компанию", "полная занятость", "на постоянную работу", "в команду", "нанимает",
"на постоянную основу", "в штат",
]
DEFAULT_LEVEL_TERMS = [
"junior", "джун", "джуниор", "middle", "мидл", "mid", "senior", "сеньор", "сеньйор",
"lead", "тимлид", "тиэмлид", "architect", "архитектор", "стажёр", "стажер", "intern", "trainee",
]
# Маркеры, по которым сообщение опознаётся как резюме/самопрезентация
# соискателя («ищу работу», «моё резюме»). По умолчанию такие сообщения
# отсекаются на этапе 1 (до ИИ и правил колонок), если пользователь ищет
# вакансии и заказы, а не кандидатов. Редактируется в «Сфере и ключах»;
# выключите отсев — резюме начнут собираться как обычные лиды.
# Слово «резюме» имеет контекстный guard (см. pipeline._resume_reason):
# «…вакансия…, присылайте резюме» — это объявление работодателя, его не режем.
DEFAULT_RESUME_MARKERS = [
"резюме", "#резюме", "моё резюме", "мое резюме", "ищу работу", "ищу вакансию",
"рассмотрю предложения", "готов к собеседованию", "в поиске работы",
"ищу проект", "ищу подработку",
]
# AI-провайдеры (активен один)
AI_PROVIDERS = [
{"id": "deepseek", "name": "DeepSeek", "base": "https://api.deepseek.com", "local": False,
"models": ["deepseek-v4-flash", "deepseek-v4-pro", "deepseek-v4-flash-vision-exp"]},
{"id": "openai", "name": "OpenAI", "base": "https://api.openai.com/v1", "local": False,
"models": ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]},
{"id": "openrouter", "name": "OpenRouter", "base": "https://openrouter.ai/api/v1", "local": False,
"models": ["deepseek/deepseek-v4-flash", "anthropic/claude-sonnet-5", "openai/gpt-5.6-luna",
"google/gemini-3.8-flash", "qwen/qwen3.8-flash"]},
{"id": "anthropic", "name": "Anthropic Claude", "base": "https://api.anthropic.com", "local": False,
"api_style": "anthropic",
"models": ["claude-sonnet-5", "claude-opus-5", "claude-fable-5-1", "claude-haiku-4-5-20251001"]},
{"id": "ollama", "name": "Ollama (локально)", "base": "http://localhost:11434/v1", "local": True,
"models": ["qwen3-coder:30b", "qwen3-coder:480b", "qwen3:32b"]},
{"id": "lmstudio", "name": "LM Studio (локально)", "base": "http://localhost:1234/v1", "local": True,
"models": ["qwen3-coder-30b", "qwen3-coder-480b", "llama-3.3-70b"]},
{"id": "custom", "name": "Другой (OpenAI-совместимый)", "base": "https://", "local": False, "models": []},
]
# Настройки по умолчанию (ключи -> значение; не секреты)
DEFAULT_SETTINGS = {
# хранилище
"autoArchive": True,
"archiveAfterDays": 14, # 1..30
"archiveClearDays": 90,
"trashClearDays": 7,
# фильтры входящих
"minLen": DEFAULT_MIN_LEN,
"stopPhrases": DEFAULT_STOP_PHRASES,
"mlEnabled": True, # локальный ML-слой (обучается на действиях пользователя)
"aiEnabled": True, # полный выключатель ИИ: false = только фильтр + ML
"aiFilterEnabled": True,
"aiFilterPrompt": DEFAULT_AI_FILTER_PROMPT,
"aiPrompt": DEFAULT_AI_PROMPT,
"cardPrompt": DEFAULT_AI_CARD_PROMPT, # структура карточки/блока «О заявке» (отдельный промпт)
# сфера и ключи (универсальный классификатор: редактируется в UI)
"domainDescription": "",
"domainKeywords": [],
"hireMarkers": DEFAULT_HIRE_MARKERS,
"levelTerms": DEFAULT_LEVEL_TERMS,
"resumeMarkers": DEFAULT_RESUME_MARKERS,
"blockResumes": True, # True = ищем вакансии/заказы, резюме соискателей отсекаем
# тип заявок, которые собираем: both | vacancy | freelance (этап 1, до ИИ)
"wantedType": "both",
# «без указания суммы карточку не создаём»: отдельно для найма и заказов
"budgetRequiredHire": False,
"budgetRequiredOrder": False,
# подписи типов на карточках (по ситуации/сфере пользователя)
"hireLabel": "вакансия",
"orderLabel": "фриланс",
# личная библиотека промптов пользователя: [{id, name, description, prompt}]
"myPrompts": [],
# напоминания
"remindersEnabled": True,
# валюта и курсы
"conversionOn": True,
"targetCurrency": "RUB",
"rateSource": "cbr", # cbr | mock
# UI-состояния колонок
"colState": {},
# AI: активный провайдер и конфиги (ключи хранятся здесь же, наружу маскируются)
"aiProvider": "deepseek",
"aiConfigs": {
p["id"]: {"apiKey": "", "baseUrl": p["base"], "model": (p["models"] or [""])[0]}
for p in AI_PROVIDERS
},
# telegram-ключи: {api_id, api_hash, account}
"tgKeys": {"apiId": "", "apiHash": ""},
# авто-мониторинг новых чатов/каналов (добавлены с другого клиента)
"autoMonitorNew": True,
# поиск каналов (Discovery)
"discJoinLimit": 50, # суточный лимит авто-вступлений (общий)
"discJoinDelayMin": 50, # сек, нижняя граница паузы между авто-вступлениями
"discJoinDelayMax": 70, # сек, верхняя граница
"discEvalSample": 10, # размер выборки сообщений при оценке
"discEvalThreshold": 40, # % подходящих сообщений
}
# Диалоговые цвета (детерминированно по имени)
DIALOG_HUES = ["#3b82f6", "#38bdf8", "#f472b6", "#f59e0b", "#a78bfa", "#34d399", "#fb7185", "#22c55e"]
# Множители единиц времени
DAY_MS = 86_400_000
HOUR_MS = 3_600_000
MIN_MS = 60_000