Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF, CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения), .editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize. Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
254 lines
20 KiB
Python
254 lines
20 KiB
Python
"""Базовые константы: палитры, стадии, промпты, валюты, провайдеры 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
|