"""Базовые константы: палитры, стадии, промпты, валюты, провайдеры 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