#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ ELENA CONSULTING — Backend API @wasrusgen1 | КОНСАЛТИНГ Реальный чат-интервью + построение бизнес-модели через Opus 4.8. Крутится на Finnish VPS (RU IP блокируется Anthropic). """ import os, re, json, sqlite3, secrets, time, base64 from datetime import datetime, timezone from flask import Flask, request, jsonify, g from flask_cors import CORS import anthropic # ── Config ─────────────────────────────────────────── BASE = "/opt/elena-consulting" DB_PATH = os.path.join(BASE, "elena.db") PROMPT_PATH = os.path.join(BASE, "elena_system_prompt.md") UPLOAD_DIR = os.path.join(BASE, "uploads") MODEL = "claude-opus-4-8" def extract_text(path, fname): ext = fname.lower().rsplit(".", 1)[-1] if "." in fname else "" try: if ext == "pdf": import pdfplumber with pdfplumber.open(path) as pdf: return "\n".join(p.extract_text() or "" for p in pdf.pages) elif ext == "docx": import docx return "\n".join(p.text for p in docx.Document(path).paragraphs) elif ext in ("xlsx", "xlsm"): import openpyxl wb = openpyxl.load_workbook(path, read_only=True, data_only=True) out = [] for ws in wb.worksheets: out.append(f"# Лист: {ws.title}") for row in ws.iter_rows(values_only=True): cells = [str(c) for c in row if c is not None] if cells: out.append(" | ".join(cells)) return "\n".join(out) elif ext in ("txt", "csv", "md"): return open(path, encoding="utf-8", errors="replace").read() except Exception as e: return f"[не удалось извлечь текст из {fname}: {e}]" return f"[{fname}: формат .{ext} не поддержан для извлечения текста]" def _key(): env = open("/opt/zashita-api/.env").read() return re.search(r'ANTHROPIC_API_KEY=(\S+)', env).group(1) def _yookassa_creds(): """shopId и secret из .env. Если нет — None (демо-режим).""" try: env = open(os.path.join(BASE, ".env")).read() sid = re.search(r'YOOKASSA_SHOP_ID=(\S+)', env) sec = re.search(r'YOOKASSA_SECRET=(\S+)', env) if sid and sec: return sid.group(1), sec.group(1) except Exception: pass return None, None client = anthropic.Anthropic(api_key=_key()) SYSTEM_PROMPT = open(PROMPT_PATH, encoding="utf-8").read() app = Flask(__name__) CORS(app) def now(): return datetime.now(timezone.utc).isoformat() # ── DB ─────────────────────────────────────────────── def db(): if "db" not in g: g.db = sqlite3.connect(DB_PATH) g.db.row_factory = sqlite3.Row return g.db @app.teardown_appcontext def close_db(exc): d = g.pop("db", None) if d: d.close() def init_db(): con = sqlite3.connect(DB_PATH) con.executescript(""" CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY AUTOINCREMENT, token TEXT UNIQUE NOT NULL, client_name TEXT, niche TEXT, description TEXT, status TEXT DEFAULT 'interview', created_at TEXT, tg_chat_id TEXT ); CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, role TEXT NOT NULL, -- 'user' (клиент) | 'assistant' (Елена) content TEXT NOT NULL, created_at TEXT, FOREIGN KEY(project_id) REFERENCES projects(id) ); CREATE TABLE IF NOT EXISTS models ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, blocks_json TEXT NOT NULL, created_at TEXT, FOREIGN KEY(project_id) REFERENCES projects(id) ); CREATE TABLE IF NOT EXISTS artifacts ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, kind TEXT NOT NULL, -- 'selection' | 'canvas' | 'vsm' | 'spec' data_json TEXT NOT NULL, created_at TEXT, FOREIGN KEY(project_id) REFERENCES projects(id) ); CREATE TABLE IF NOT EXISTS acceptances ( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id INTEGER NOT NULL, doc TEXT NOT NULL, -- 'offer' | 'pep' | 'pdn' doc_version TEXT NOT NULL, doc_hash TEXT NOT NULL, -- SHA-256 текста документа (что подписано) identifier TEXT, -- телефон/email подписанта code TEXT, -- код подтверждения ip TEXT, user_agent TEXT, payment_id TEXT, -- акцепт оплатой accepted_at TEXT NOT NULL -- append-only, не редактируется ); """) # Миграции — добавляем колонки если нет (идемпотентно) for sql in [ "ALTER TABLE projects ADD COLUMN tg_chat_id TEXT", ]: try: con.execute(sql) except Exception: pass con.commit() con.close() # ── Helpers ────────────────────────────────────────── def get_project(token): return db().execute("SELECT * FROM projects WHERE token=?", (token,)).fetchone() def history(project_id): rows = db().execute( "SELECT role, content FROM messages WHERE project_id=? ORDER BY id", (project_id,) ).fetchall() return [{"role": r["role"], "content": r["content"]} for r in rows] def save_artifact(project_id, kind, data): con = db() con.execute( "INSERT INTO artifacts (project_id, kind, data_json, created_at) VALUES (?,?,?,?)", (project_id, kind, json.dumps(data, ensure_ascii=False), now()) ) con.commit() def latest_artifact(project_id, kind): row = db().execute( "SELECT data_json FROM artifacts WHERE project_id=? AND kind=? ORDER BY id DESC LIMIT 1", (project_id, kind) ).fetchone() return json.loads(row["data_json"]) if row else None def run_tool(project_id, tool, tool_name, instruction, extra_context=None, max_tokens=4096): """Универсальный вызов forced-tool Opus на основе истории интервью.""" msgs = history(project_id) if not msgs: return None, "no interview data" ctx = instruction if extra_context: ctx = extra_context + "\n\n" + instruction msgs = msgs + [{"role": "user", "content": ctx}] try: resp = client.messages.create( model=MODEL, max_tokens=max_tokens, system=[{"type": "text", "text": SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"}}], tools=[tool], tool_choice={"type": "tool", "name": tool_name}, messages=msgs ) block = next(b for b in resp.content if b.type == "tool_use") return block.input, {"in": resp.usage.input_tokens, "out": resp.usage.output_tokens} except Exception as e: return None, str(e) # ── Routes ─────────────────────────────────────────── @app.route("/api/health") def health(): return jsonify({"ok": True, "model": MODEL, "time": now()}) @app.route("/api/project/new", methods=["POST"]) def new_project(): data = request.get_json(force=True) or {} token = secrets.token_urlsafe(8) con = db() cur = con.execute( "INSERT INTO projects (token, client_name, niche, status, created_at) VALUES (?,?,?,?,?)", (token, data.get("client_name", ""), data.get("niche", ""), "interview", now()) ) con.commit() pid = cur.lastrowid # Первое приветствие Елены — открывает интервью greeting = _elena_first_message(data.get("client_name", ""), data.get("niche", "")) con.execute( "INSERT INTO messages (project_id, role, content, created_at) VALUES (?,?,?,?)", (pid, "assistant", greeting, now()) ) con.commit() return jsonify({"token": token, "greeting": greeting}) def _elena_first_message(name, niche): nm = f", {name}" if name else "" return (f"Здравствуйте{nm}! Я Елена, ваш консультант. " f"Моя задача — разобрать ваш бизнес по полочкам и показать где можно ускориться и заработать больше.\n\n" f"Расскажите своими словами: чем вы занимаетесь и что сейчас беспокоит больше всего? " f"Можно текстом или голосом — как удобнее.") @app.route("/api/project/profile", methods=["POST"]) def save_profile(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 name = (data.get("client_name") or "").strip() niche = (data.get("niche") or "").strip() desc = (data.get("description") or "").strip() con = db() con.execute("UPDATE projects SET client_name=?, niche=?, description=? WHERE id=?", (name, niche, desc, proj["id"])) con.commit() # Профиль становится первым реальным сообщением клиента — Елена сразу в контексте. # Удаляем старое дефолтное приветствие и формируем диалог заново. con.execute("DELETE FROM messages WHERE project_id=?", (proj["id"],)) profile_msg = f"Меня зовут {name}." if name else "" if niche: profile_msg += f" Сфера деятельности: {niche}." if desc: profile_msg += f"\n\nО моей деятельности: {desc}" profile_msg = profile_msg.strip() or "Хочу разобрать свой бизнес." con.execute("INSERT INTO messages (project_id, role, content, created_at) VALUES (?,?,?,?)", (proj["id"], "user", profile_msg, now())) con.commit() # Елена отвечает на профиль try: resp = client.messages.create( model=MODEL, max_tokens=900, system=[{"type": "text", "text": SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"}}], messages=[{"role": "user", "content": profile_msg}] ) reply = resp.content[0].text except Exception as e: return jsonify({"error": str(e)}), 500 con.execute("INSERT INTO messages (project_id, role, content, created_at) VALUES (?,?,?,?)", (proj["id"], "assistant", reply, now())) con.commit() return jsonify({"ok": True, "profile_msg": profile_msg, "reply": reply}) @app.route("/api/chat", methods=["POST"]) def chat(): data = request.get_json(force=True) or {} token = data.get("token") msg = (data.get("message") or "").strip() if not token or not msg: return jsonify({"error": "token and message required"}), 400 proj = get_project(token) if not proj: return jsonify({"error": "project not found"}), 404 con = db() con.execute( "INSERT INTO messages (project_id, role, content, created_at) VALUES (?,?,?,?)", (proj["id"], "user", msg, now()) ) con.commit() msgs = history(proj["id"]) try: resp = client.messages.create( model=MODEL, max_tokens=1024, system=[{ "type": "text", "text": SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"} }], messages=msgs ) reply = resp.content[0].text usage = {"in": resp.usage.input_tokens, "out": resp.usage.output_tokens} except Exception as e: return jsonify({"error": str(e)}), 500 con.execute( "INSERT INTO messages (project_id, role, content, created_at) VALUES (?,?,?,?)", (proj["id"], "assistant", reply, now()) ) con.commit() return jsonify({"reply": reply, "usage": usage}) # ── Tool schema: строгий IDEF0 (ICOM + декомпозиция) ── ARROW = { "type": "object", "properties": { "name": {"type": "string", "description": "Что за поток (название стрелки)"}, "source": {"type": "string", "description": "Откуда: внешний источник или node_id функции"}, "exists": {"type": "boolean", "description": "true если реально существует в бизнесе, false если ДОЛЖЕН быть но отсутствует"} }, "required": ["name", "source", "exists"] } OUT_ARROW = { "type": "object", "properties": { "name": {"type": "string"}, "target": {"type": "string", "description": "Куда идёт: внешний потребитель или node_id функции; 'НИКУДА' если выход никто не использует"}, "exists": {"type": "boolean"} }, "required": ["name", "target", "exists"] } MECH = { "type": "object", "properties": { "name": {"type": "string", "description": "Исполнитель или инструмент"}, "type": {"type": "string", "enum": ["human", "equipment", "software", "none"]}, "load": {"type": "string", "description": "Нагрузка/объём потребления, если известно"} }, "required": ["name", "type"] } def _activity_schema(): return { "type": "object", "properties": { "node_id": {"type": "string", "description": "Идентификатор по IDEF0: A0, A1, A2, A1.1 ..."}, "parent": {"type": "string", "description": "node_id родителя или 'A0' для верхнего уровня"}, "function": {"type": "string", "description": "Функция ГЛАГОЛОМ + объект, напр. 'Раскроить ткань', 'Принять заказ'"}, "inputs": {"type": "array", "items": ARROW, "description": "I — что ПРЕОБРАЗУЕТСЯ (сырьё, данные на обработку)"}, "controls": {"type": "array", "items": ARROW, "description": "C — что УПРАВЛЯЕТ но не расходуется (нормы, регламенты, ТЗ, лекала). Если нет — пустой массив = функция без контроля"}, "outputs": {"type": "array", "items": OUT_ARROW, "description": "O — результат функции"}, "mechanisms":{"type": "array", "items": MECH, "description": "M — кто/чем выполняется (люди, оборудование, ПО)"}, "completeness": {"type": "integer", "description": "0-100"}, "issues": {"type": "array", "items": {"type": "string"}} }, "required": ["node_id","parent","function","inputs","controls","outputs","mechanisms","completeness","issues"] } MODEL_TOOL = { "name": "build_idef0_model", "description": "Строит функциональную модель бизнеса клиента по стандарту IDEF0: контекстная диаграмма A-0, декомпозиция A0, анализ стрелок ICOM.", "input_schema": { "type": "object", "properties": { "client_summary": {"type": "string", "description": "1-2 предложения: кто клиент и чем занимается"}, "business_pattern": {"type": "string", "description": "Тип бизнеса и модель монетизации"}, "context": { "type": "object", "description": "Контекстная диаграмма A-0: весь бизнес как ОДНА функция", "properties": { "function": {"type": "string", "description": "Главная функция бизнеса глаголом, напр. 'Производить и продавать швейные изделия'"}, "inputs": {"type": "array", "items": ARROW}, "controls": {"type": "array", "items": ARROW}, "outputs": {"type": "array", "items": OUT_ARROW}, "mechanisms":{"type": "array", "items": MECH} }, "required": ["function","inputs","controls","outputs","mechanisms"] }, "activities": { "type": "array", "description": "Декомпозиция A0 на функции (3-8 для МСБ), при необходимости под-функции A1.1 и т.д.", "items": _activity_schema() }, "arrow_issues": { "type": "array", "description": "Анализ стрелок — разрывы модели", "items": { "type": "object", "properties": { "type": {"type": "string", "enum": ["missing_control","dangling_output","dangling_input","overloaded_mechanism","manual_bridge","broken_flow"]}, "title": {"type": "string"}, "description": {"type": "string"}, "node_id": {"type": "string", "description": "К какой функции относится"}, "severity": {"type": "string", "enum": ["critical","high","medium"]} }, "required": ["type","title","description","node_id","severity"] } }, "missing_info": {"type": "array", "items": {"type": "string"}} }, "required": ["client_summary","business_pattern","context","activities","arrow_issues","missing_info"] } } BUILD_INSTRUCTION = """Построй функциональную модель бизнеса клиента по строгому стандарту IDEF0. ПРАВИЛА ICOM (различай 4 типа стрелок): - INPUT (I, слева): то что ПРЕОБРАЗУЕТСЯ функцией в выход (сырьё, заявка, данные на обработку). - CONTROL (C, сверху): то что УПРАВЛЯЕТ функцией но НЕ расходуется (нормы, регламенты, ТЗ, лекала, прайс, ГОСТ). КРИТИЧНО: если у функции нет контроля — оставь controls пустым, это сигнал проблемы 'работают как привыкли'. - OUTPUT (O, справа): результат функции. Если выход никто не использует — target='НИКУДА'. - MECHANISM (M, снизу): кто/чем выполняется (человек, оборудование, ПО). type='none' если механизма по сути нет. ШАГИ: 1. context (A-0): опиши весь бизнес как ОДНУ функцию глаголом, с её внешними ICOM. 2. activities (A0): декомпозируй на 3-8 главных функций. Каждая — ГЛАГОЛ+объект ('Принять заказ', 'Раскроить ткань'). Присвой node_id A1, A2... Свяжи стрелки: выход одной функции = вход или контроль другой (source/target = node_id). 3. Где функция сложная — декомпозируй глубже (A1.1, A1.2) с parent. 4. arrow_issues: проанализируй стрелки: - missing_control: функция без управляющих норм - dangling_output: выход никто не потребляет - dangling_input: вход ниоткуда не приходит - overloaded_mechanism: один механизм (человек) на много функций - manual_bridge: человек вручную переносит данные между функциями - broken_flow: выход одной функции не доходит до входа следующей 5. Помечай exists=false для стрелок которые ДОЛЖНЫ быть, но в бизнесе отсутствуют. Функция = глагол. Не выдумывай то чего нет в интервью. Вызови build_idef0_model.""" @app.route("/api/build-model", methods=["POST"]) def build_model(): data = request.get_json(force=True) or {} token = data.get("token") proj = get_project(token) if not proj: return jsonify({"error": "project not found"}), 404 msgs = history(proj["id"]) if not msgs: return jsonify({"error": "no interview data"}), 400 docs = documents_context(proj["id"]) build_text = (docs + "\n\n" + BUILD_INSTRUCTION) if docs else BUILD_INSTRUCTION msgs = msgs + [{"role": "user", "content": build_text}] try: resp = client.messages.create( model=MODEL, max_tokens=4096, system=[{"type": "text", "text": SYSTEM_PROMPT, "cache_control": {"type": "ephemeral"}}], tools=[MODEL_TOOL], tool_choice={"type": "tool", "name": "build_idef0_model"}, messages=msgs ) block = next(b for b in resp.content if b.type == "tool_use") model_data = block.input usage = {"in": resp.usage.input_tokens, "out": resp.usage.output_tokens} except Exception as e: return jsonify({"error": str(e)}), 500 con = db() con.execute( "INSERT INTO models (project_id, blocks_json, created_at) VALUES (?,?,?)", (proj["id"], json.dumps(model_data, ensure_ascii=False), now()) ) con.execute("UPDATE projects SET status='model_ready' WHERE id=?", (proj["id"],)) con.commit() return jsonify({"model": model_data, "usage": usage}) # ══ СЕЛЕКТОР МЕТОДОЛОГИЙ ═════════════════════════════ SELECT_TOOL = { "name": "recommend_methodologies", "description": "Анализирует бизнес клиента и рекомендует набор методологий моделирования.", "input_schema": { "type": "object", "properties": { "business_type": {"type": "string", "description": "Распознанный тип: эксперт / услуги / производство / торговля / иное"}, "recommended": { "type": "array", "items": { "type": "object", "properties": { "method": {"type": "string", "enum": ["canvas", "idef0", "vsm", "dfd", "erd"]}, "use": {"type": "boolean", "description": "Применять ли"}, "depth": {"type": "string", "enum": ["full", "light", "skip"]}, "reason": {"type": "string", "description": "Почему именно так для этого клиента"} }, "required": ["method", "use", "depth", "reason"] } }, "rationale": {"type": "string", "description": "Общее обоснование набора"} }, "required": ["business_type", "recommended", "rationale"] } } SELECT_INSTRUCTION = """Проанализируй бизнес клиента и реши какие методологии моделирования применить. Доступны: canvas (Business Model Canvas — стратегия), idef0 (функциональная модель), vsm (поток ценности, потери), dfd (потоки данных), erd (модель данных). Правила: canvas нужен почти всем. idef0 — где есть процессы. vsm — где есть поток материала/товара (производство, торговля). Для простого эксперта без производства vsm пропусти. Для каждой метод. укажи use, depth (full/light/skip) и reason под ЭТОГО клиента. Дай rationale. Вызови recommend_methodologies.""" @app.route("/api/select-methodologies", methods=["POST"]) def select_methodologies(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 result, usage = run_tool(proj["id"], SELECT_TOOL, "recommend_methodologies", SELECT_INSTRUCTION, max_tokens=1500) if result is None: return jsonify({"error": usage}), 500 save_artifact(proj["id"], "selection", result) return jsonify({"selection": result, "usage": usage}) # ══ CANVAS — Business Model Canvas (9 блоков) ════════ CANVAS_BLOCK = { "type": "object", "properties": { "items": {"type": "array", "items": {"type": "string"}, "description": "Пункты блока"}, "completeness": {"type": "integer"}, "note": {"type": "string", "description": "Комментарий / выявленная проблема по блоку"} }, "required": ["items", "completeness", "note"] } CANVAS_TOOL = { "name": "build_canvas", "description": "Строит Business Model Canvas клиента (9 блоков) из интервью.", "input_schema": { "type": "object", "properties": { "value_propositions": CANVAS_BLOCK, "customer_segments": CANVAS_BLOCK, "channels": CANVAS_BLOCK, "customer_relationships": CANVAS_BLOCK, "revenue_streams": CANVAS_BLOCK, "key_resources": CANVAS_BLOCK, "key_activities": CANVAS_BLOCK, "key_partners": CANVAS_BLOCK, "cost_structure": CANVAS_BLOCK, "insight": {"type": "string", "description": "Главный стратегический вывод по бизнес-модели"} }, "required": ["value_propositions","customer_segments","channels","customer_relationships","revenue_streams","key_resources","key_activities","key_partners","cost_structure","insight"] } } CANVAS_INSTRUCTION = """Построй Business Model Canvas клиента из интервью. 9 блоков: - value_propositions: ценность, которую получает клиент - customer_segments: кто платит, сегменты - channels: как доходит ценность до клиента - customer_relationships: тип отношений с клиентами - revenue_streams: откуда деньги, модель монетизации - key_resources: ключевые ресурсы - key_activities: ключевые действия - key_partners: партнёры, поставщики - cost_structure: основные издержки Каждый блок: items (пункты), completeness (0-100), note (проблема/комментарий). Дай insight — главный стратегический вывод. Не выдумывай. Где данных нет — низкий completeness. Вызови build_canvas.""" @app.route("/api/build-canvas", methods=["POST"]) def build_canvas(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 result, usage = run_tool(proj["id"], CANVAS_TOOL, "build_canvas", CANVAS_INSTRUCTION, extra_context=documents_context(proj["id"]), max_tokens=3000) if result is None: return jsonify({"error": usage}), 500 save_artifact(proj["id"], "canvas", result) return jsonify({"canvas": result, "usage": usage}) # ══ ГЕНЕРАТОР ТЗ (части A-C из IDEF0) ════════════════ SPEC_TOOL = { "name": "build_tech_spec", "description": "Собирает техническое задание на ПО из модели бизнеса: контекст, модули, модель данных.", "input_schema": { "type": "object", "properties": { "overview": {"type": "string", "description": "A1-A2: что за система, для кого, какую проблему решает, границы"}, "roles": { "type": "array", "description": "A3: роли пользователей (из mechanisms IDEF0)", "items": {"type": "object", "properties": { "name": {"type": "string"}, "does": {"type": "string"}, "access": {"type": "string"} }, "required": ["name","does","access"]} }, "modules": { "type": "array", "description": "B: модули системы (из функций IDEF0)", "items": {"type": "object", "properties": { "name": {"type": "string", "description": "Название модуля"}, "source_node": {"type": "string", "description": "node_id функции IDEF0"}, "purpose": {"type": "string"}, "screens": {"type": "array", "items": {"type": "string"}, "description": "Экраны модуля"}, "inputs_data": {"type": "string", "description": "Какие данные вводятся"}, "outputs_data": {"type": "string", "description": "Что система создаёт/показывает"}, "rules": {"type": "array", "items": {"type": "string"}, "description": "Бизнес-правила (из controls)"}, "roles": {"type": "array", "items": {"type": "string"}, "description": "Кто пользуется"} }, "required": ["name","source_node","purpose","screens","inputs_data","outputs_data","rules","roles"]} }, "entities": { "type": "array", "description": "C: модель данных (таблицы)", "items": {"type": "object", "properties": { "name": {"type": "string", "description": "Сущность/таблица"}, "fields": {"type": "array", "items": {"type": "object", "properties": { "field": {"type": "string"}, "type": {"type": "string"}, "note": {"type": "string"} }, "required": ["field","type"]}}, "relations": {"type": "array", "items": {"type": "string"}, "description": "Связи с другими таблицами"}, "example": {"type": "string", "description": "Пример строки данных"} }, "required": ["name","fields","relations","example"]} }, "open_questions": {"type": "array", "items": {"type": "string"}, "description": "Что уточнить перед разработкой"} }, "required": ["overview","roles","modules","entities","open_questions"] } } @app.route("/api/build-spec", methods=["POST"]) def build_spec(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 # Подгружаем готовую IDEF0 модель как контекст model_row = db().execute("SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],)).fetchone() if not model_row: return jsonify({"error": "сначала постройте IDEF0 модель"}), 400 idef0 = model_row["blocks_json"] instruction = ("На основе интервью и построенной IDEF0-модели собери ТЗ на программу для бизнеса клиента.\n" "МАППИНГ: функция IDEF0 → модуль; Input → вводимые данные; Output → что показывает; " "Control → бизнес-правила; Mechanism → роли; хранилища → таблицы данных.\n" "overview (A1-A2): что за система, для кого, проблема, границы.\n" "roles (A3): из mechanisms.\n" "modules (B): каждая функция = модуль с экранами, данными, правилами.\n" "entities (C): модель данных — таблицы с полями, связями, примером строки.\n" "Думай как проектировщик ПО. Вызови build_tech_spec.\n\n" f"IDEF0-МОДЕЛЬ:\n{idef0}") result, usage = run_tool(proj["id"], SPEC_TOOL, "build_tech_spec", instruction, max_tokens=8192) if result is None: return jsonify({"error": usage}), 500 save_artifact(proj["id"], "spec", result) con = db(); con.execute("UPDATE projects SET status='spec_ready' WHERE id=?", (proj["id"],)); con.commit() return jsonify({"spec": result, "usage": usage}) @app.route("/api/project/crm", methods=["POST"]) def update_crm(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 crm = latest_artifact(proj["id"], "crm") or { "pipeline": "lead", "deal_amount": 0, "paid_amount": 0, "contact": "", "source": "", "note": "", "payments": [], "billing_type": "paid" } for k in ("pipeline", "deal_amount", "paid_amount", "contact", "source", "note", "payments", "billing_type", "payment_schedule", "stage_payments", "stage_prices", "complexity"): if k in data: crm[k] = data[k] # paid_amount = сумма платежей (если есть реестр) if "payments" in crm and isinstance(crm["payments"], list): crm["paid_amount"] = sum(p.get("amount", 0) for p in crm["payments"]) save_artifact(proj["id"], "crm", crm) return jsonify({"ok": True, "crm": crm}) import hashlib # Версии и тексты юридических документов (хеш фиксируется при акцепте) LEGAL_DOCS = {"offer": "1.0", "pep": "1.0", "pdn": "1.0"} def _doc_hash(doc): """SHA-256 текста документа — для доказательства что подписано.""" path = os.path.join(BASE, "legal", {"offer": "dogovor_oferta.md", "pep": "soglashenie_pep.md", "pdn": "politika_pdn.md"}.get(doc, "")) try: return hashlib.sha256(open(path, "rb").read()).hexdigest() except Exception: return "" @app.route("/api/legal/") def legal_text(doc): """Отдаёт текст юридического документа для ознакомления.""" fname = {"offer": "dogovor_oferta.md", "pep": "soglashenie_pep.md", "pdn": "politika_pdn.md"}.get(doc) if not fname: return jsonify({"error": "unknown doc"}), 404 try: text = open(os.path.join(BASE, "legal", fname), encoding="utf-8").read() return jsonify({"doc": doc, "version": LEGAL_DOCS.get(doc, "1.0"), "text": text}) except Exception as e: return jsonify({"error": str(e)}), 404 @app.route("/api/sign/request", methods=["POST"]) def sign_request(): """Генерирует код подтверждения (ПЭП). В проде — отправка по SMS/email.""" data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 identifier = (data.get("identifier") or "").strip() if not identifier: return jsonify({"error": "укажите телефон или email"}), 400 code = "".join(secrets.choice("0123456789") for _ in range(4)) # храним код в crm artifact (TTL ~10 мин через timestamp) crm = latest_artifact(proj["id"], "crm") or {} crm["_sign_code"] = code crm["_sign_identifier"] = identifier crm["_sign_at"] = now() save_artifact(proj["id"], "crm", crm) # ДЕМО: возвращаем код (в проде — SMS/email, код не возвращается) return jsonify({"ok": True, "demo_code": code, "note": "ДЕМО: в проде код придёт по SMS/email"}) @app.route("/api/sign/confirm", methods=["POST"]) def sign_confirm(): """Проверяет код и фиксирует подписание (акцепт) в журнал.""" data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 code = (data.get("code") or "").strip() crm = latest_artifact(proj["id"], "crm") or {} if not crm.get("_sign_code") or code != crm["_sign_code"]: return jsonify({"error": "неverный код"}), 400 identifier = crm.get("_sign_identifier", "") docs = data.get("docs", ["offer", "pep"]) ip = request.headers.get("X-Real-IP") or request.headers.get("X-Forwarded-For", request.remote_addr or "") ua = request.headers.get("User-Agent", "")[:300] con = db() recorded = [] for doc in docs: if doc not in LEGAL_DOCS: continue h = _doc_hash(doc) con.execute( "INSERT INTO acceptances (project_id, doc, doc_version, doc_hash, identifier, code, ip, user_agent, payment_id, accepted_at) VALUES (?,?,?,?,?,?,?,?,?,?)", (proj["id"], doc, LEGAL_DOCS[doc], h, identifier, code, ip, ua, "", now()) ) recorded.append(doc) # очищаем временный код crm.pop("_sign_code", None); crm.pop("_sign_at", None) save_artifact(proj["id"], "crm", crm) con.commit() return jsonify({"ok": True, "signed": recorded, "identifier": identifier, "at": now()}) @app.route("/api/accept", methods=["POST"]) def accept_documents(): """Фиксация акцепта (ПЭП) в append-only журнал с хешем документов.""" data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 docs = data.get("docs", ["offer", "pep"]) # какие документы акцептованы identifier = data.get("identifier", "") # телефон/email code = data.get("code", "") payment_id = data.get("payment_id", "") ip = request.headers.get("X-Real-IP") or request.headers.get("X-Forwarded-For", request.remote_addr or "") ua = request.headers.get("User-Agent", "")[:300] con = db() recorded = [] for doc in docs: if doc not in LEGAL_DOCS: continue h = _doc_hash(doc) con.execute( "INSERT INTO acceptances (project_id, doc, doc_version, doc_hash, identifier, code, ip, user_agent, payment_id, accepted_at) VALUES (?,?,?,?,?,?,?,?,?,?)", (proj["id"], doc, LEGAL_DOCS[doc], h, identifier, code, ip, ua, payment_id, now()) ) recorded.append({"doc": doc, "version": LEGAL_DOCS[doc], "hash": h[:16] + "..."}) con.commit() return jsonify({"ok": True, "accepted": recorded, "at": now()}) @app.route("/api/acceptances/") def get_acceptances(token): """Выписка из журнала акцептов (доказательная база).""" proj = get_project(token) if not proj: return jsonify({"error": "not found"}), 404 rows = db().execute( "SELECT doc, doc_version, doc_hash, identifier, code, ip, payment_id, accepted_at FROM acceptances WHERE project_id=? ORDER BY id", (proj["id"],) ).fetchall() return jsonify({"acceptances": [dict(r) for r in rows]}) @app.route("/api/payment/create", methods=["POST"]) def payment_create(): """Создаёт платёж. method: card | sbp | cash.""" import urllib.request, urllib.parse data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 amount = float(data.get("amount", 0)) method = data.get("method", "card") # card | sbp | cash desc = data.get("description", f"Оплата консалтинга — {proj['client_name'] or 'клиент'}") if amount <= 0: return jsonify({"error": "сумма должна быть больше 0"}), 400 # Наличные — не онлайн: фиксируем намерение, Руслан подтвердит вручную if method == "cash": crm = latest_artifact(proj["id"], "crm") or {} pending = crm.get("pending_cash", []) pending.append({"amount": amount, "desc": desc, "at": now()}) crm["pending_cash"] = pending save_artifact(proj["id"], "crm", crm) return jsonify({"method": "cash", "instructions": "Оплата наличными при встрече. Консультант подтвердит получение.", "amount": amount}) sid, sec = _yookassa_creds() return_url = data.get("return_url", "https://wasrusgen1.ru/consulting/cabinet.html") # Демо-режим если ключей ЮKassa нет if not sid: return jsonify({ "method": method, "demo": True, "confirmation_url": return_url + "?demo_paid=" + str(int(amount)), "note": "ДЕМО: ключи ЮKassa не настроены. Реальная оплата заработает после добавления YOOKASSA_SHOP_ID/SECRET." }) # Реальный вызов ЮKassa API import base64 as b64m, json as jsonm payload = { "amount": {"value": f"{amount:.2f}", "currency": "RUB"}, "capture": True, "description": desc, "metadata": {"token": proj["token"]}, "confirmation": {"type": "redirect", "return_url": return_url} } if method == "sbp": payload["payment_method_data"] = {"type": "sbp"} payload["confirmation"] = {"type": "qr"} try: body = jsonm.dumps(payload).encode() req = urllib.request.Request("https://api.yookassa.ru/v3/payments", data=body, method="POST") auth = b64m.b64encode(f"{sid}:{sec}".encode()).decode() req.add_header("Authorization", "Basic " + auth) req.add_header("Content-Type", "application/json") req.add_header("Idempotence-Key", secrets.token_hex(16)) resp = urllib.request.urlopen(req, timeout=20) result = jsonm.loads(resp.read()) conf = result.get("confirmation", {}) return jsonify({ "method": method, "payment_id": result.get("id"), "confirmation_url": conf.get("confirmation_url"), "qr": conf.get("confirmation_data") # для СБП — QR-данные }) except Exception as e: return jsonify({"error": "ЮKassa: " + str(e)}), 500 @app.route("/api/payment/webhook", methods=["POST"]) def payment_webhook(): """ЮKassa шлёт уведомление о статусе. При succeeded — платёж в реестр.""" import json as jsonm data = request.get_json(force=True) or {} event = data.get("event") obj = data.get("object", {}) if event == "payment.succeeded": token = obj.get("metadata", {}).get("token") proj = get_project(token) if token else None if proj: amount = float(obj.get("amount", {}).get("value", 0)) method = obj.get("payment_method", {}).get("type", "") crm = latest_artifact(proj["id"], "crm") or {"payments": []} crm.setdefault("payments", []).append({ "date": now()[:10], "amount": amount, "note": f"ЮKassa ({method})", "auto": True }) crm["paid_amount"] = sum(p.get("amount", 0) for p in crm["payments"]) save_artifact(proj["id"], "crm", crm) return jsonify({"ok": True}) # ЮKassa требует 200 @app.route("/api/project/delete", methods=["POST"]) def delete_project(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 pid = proj["id"] con = db() con.execute("DELETE FROM messages WHERE project_id=?", (pid,)) con.execute("DELETE FROM models WHERE project_id=?", (pid,)) con.execute("DELETE FROM artifacts WHERE project_id=?", (pid,)) con.execute("DELETE FROM projects WHERE id=?", (pid,)) con.commit() # удалить загруженные документы import shutil pdir = os.path.join(UPLOAD_DIR, proj["token"]) if os.path.isdir(pdir): shutil.rmtree(pdir, ignore_errors=True) return jsonify({"ok": True}) @app.route("/api/project/tasks", methods=["POST"]) def update_tasks(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 tasks = data.get("tasks", []) # [{text, due, done}] save_artifact(proj["id"], "tasks", {"items": tasks}) return jsonify({"ok": True, "tasks": tasks}) @app.route("/api/project/approve", methods=["POST"]) def approve_stage(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 stage = data.get("stage") approved = bool(data.get("approved", True)) con = db() # approvals хранятся как один артефакт kind='approvals' — словарь stage->bool row = db().execute("SELECT id, data_json FROM artifacts WHERE project_id=? AND kind='approvals' ORDER BY id DESC LIMIT 1", (proj["id"],)).fetchone() appr = json.loads(row["data_json"]) if row else {} if approved: appr[stage] = now() else: appr.pop(stage, None) if row: con.execute("UPDATE artifacts SET data_json=? WHERE id=?", (json.dumps(appr, ensure_ascii=False), row["id"])) else: con.execute("INSERT INTO artifacts (project_id, kind, data_json, created_at) VALUES (?,?,?,?)", (proj["id"], "approvals", json.dumps(appr, ensure_ascii=False), now())) con.commit() return jsonify({"ok": True, "approvals": appr}) @app.route("/api/upload", methods=["POST"]) def upload_doc(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 fname = (data.get("filename") or "file").replace("/", "_").replace("\\", "_") b64 = data.get("content") or "" try: raw = base64.b64decode(b64.split(",")[-1]) except Exception as e: return jsonify({"error": "bad base64: " + str(e)}), 400 if len(raw) > 15 * 1024 * 1024: return jsonify({"error": "файл больше 15 МБ"}), 400 pdir = os.path.join(UPLOAD_DIR, proj["token"]) os.makedirs(pdir, exist_ok=True) path = os.path.join(pdir, fname) with open(path, "wb") as f: f.write(raw) text = extract_text(path, fname) save_artifact(proj["id"], "document", {"filename": fname, "text": text[:10000], "size": len(raw)}) return jsonify({"ok": True, "filename": fname, "chars": len(text), "size": len(raw)}) def documents_context(project_id): rows = db().execute("SELECT data_json FROM artifacts WHERE project_id=? AND kind='document' ORDER BY id", (project_id,)).fetchall() if not rows: return None parts = ["ЗАГРУЖЕННЫЕ ДОКУМЕНТЫ КЛИЕНТА:"] for r in rows: d = json.loads(r["data_json"]) parts.append(f"\n=== {d['filename']} ===\n{d.get('text','')[:6000]}") return "\n".join(parts) @app.route("/api/projects") def list_projects(): rows = db().execute( "SELECT p.token, p.client_name, p.niche, p.status, p.created_at, " "(SELECT COUNT(*) FROM messages m WHERE m.project_id=p.id) as msg_count " "FROM projects p ORDER BY p.id DESC" ).fetchall() out = [] for r in rows: pid = db().execute("SELECT id FROM projects WHERE token=?", (r["token"],)).fetchone()["id"] out.append({ "token": r["token"], "client_name": r["client_name"] or "Без имени", "niche": r["niche"] or "", "status": r["status"], "created_at": r["created_at"], "msg_count": r["msg_count"], "has_selection": latest_artifact(pid, "selection") is not None, "has_canvas": latest_artifact(pid, "canvas") is not None, "has_idef0": db().execute("SELECT 1 FROM models WHERE project_id=? LIMIT 1", (pid,)).fetchone() is not None, "has_spec": latest_artifact(pid, "spec") is not None, "approvals": latest_artifact(pid, "approvals") or {}, "crm": latest_artifact(pid, "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":""}, "tasks": (latest_artifact(pid, "tasks") or {}).get("items", []) }) return jsonify({"projects": out}) SCREEN_TOOL = { "name": "design_screen", "description": "Проектирует один экран программы: layout, поля, действия, пример данных.", "input_schema": { "type": "object", "properties": { "title": {"type": "string", "description": "Название экрана"}, "type": {"type": "string", "enum": ["list", "card", "form", "dashboard", "kanban"], "description": "Тип экрана"}, "columns": {"type": "array", "items": {"type": "string"}, "description": "Для list/kanban: колонки таблицы или стадии"}, "fields": {"type": "array", "items": {"type": "object", "properties": { "label": {"type": "string"}, "kind": {"type": "string", "enum": ["text","number","select","date","money","status","textarea"]} }, "required": ["label","kind"]}, "description": "Для form/card: поля"}, "rows": {"type": "array", "items": {"type": "array", "items": {"type": "string"}}, "description": "Для list: 2-3 примера строк (значения по колонкам)"}, "actions": {"type": "array", "items": {"type": "string"}, "description": "Кнопки/действия на экране"}, "kpis": {"type": "array", "items": {"type": "object", "properties": {"value": {"type": "string"}, "label": {"type": "string"}}, "required": ["value","label"]}, "description": "Для dashboard: показатели"} }, "required": ["title", "type", "actions"] } } @app.route("/api/design-screen", methods=["POST"]) def design_screen(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 module = data.get("module", "") instr = (f"Спроектируй ОДИН экран программы для модуля «{module}» бизнеса клиента.\n" "Используй реальные данные клиента из интервью для примеров (имена, суммы, статусы).\n" "Выбери подходящий type (list/card/form/dashboard/kanban). Заполни поля/колонки/примеры строк/KPI.\n" "actions — конкретные кнопки. Вызови design_screen.") result, usage = run_tool(proj["id"], SCREEN_TOOL, "design_screen", instr, max_tokens=1500) if result is None: return jsonify({"error": usage}), 500 return jsonify({"screen": result, "usage": usage}) # ══ ЦЕНООБРАЗОВАНИЕ под клиента ══════════════════════ PRICING_TOOL = { "name": "build_pricing", "description": "Оценивает масштаб работы по клиенту, анализирует рынок консалтинга и предлагает таблицу цен с аргументами.", "input_schema": { "type": "object", "properties": { "scale": { "type": "object", "description": "Оценка масштаба работы", "properties": { "size": {"type": "string", "enum": ["micro", "small", "medium", "large"], "description": "micro=соло, small=2-10, medium=10-50, large=50+"}, "complexity": {"type": "string", "enum": ["low", "medium", "high"]}, "scope": {"type": "string", "description": "Объём работ: сколько ролей интервьюировать, процессов, документов"}, "effort_estimate": {"type": "string", "description": "Оценка трудозатрат (часы/недели) и токенов AI"} }, "required": ["size", "complexity", "scope", "effort_estimate"] }, "market": {"type": "string", "description": "Анализ рынка: вилка цен на аналогичный консалтинг в РФ, с кем сравниваем"}, "packages": { "type": "array", "description": "2-3 пакета услуг", "items": {"type": "object", "properties": { "name": {"type": "string"}, "scope": {"type": "array", "items": {"type": "string"}, "description": "Что входит"}, "price": {"type": "integer", "description": "Цена в рублях"}, "duration": {"type": "string", "description": "Срок"}, "argument": {"type": "string", "description": "Аргумент почему такая цена"} }, "required": ["name", "scope", "price", "duration", "argument"]} }, "recommended": {"type": "string", "description": "Какой пакет рекомендуется этому клиенту и почему"}, "rationale": {"type": "string", "description": "Главный аргумент по цене для продажи (ROI, экономия клиента)"} }, "required": ["scale", "market", "packages", "recommended", "rationale"] } } @app.route("/api/build-pricing", methods=["POST"]) def build_pricing(): data = request.get_json(force=True) or {} proj = get_project(data.get("token")) if not proj: return jsonify({"error": "project not found"}), 404 # Контекст: интервью + IDEF0 модель если есть model_row = db().execute("SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],)).fetchone() extra = ("ПОСТРОЕННАЯ МОДЕЛЬ БИЗНЕСА (для оценки масштаба):\n" + model_row["blocks_json"]) if model_row else None instr = ("На основе первичного интервью (и модели бизнеса, если есть) сформируй ЦЕНОВОЕ ПРЕДЛОЖЕНИЕ для клиента.\n" "1. Оцени МАСШТАБ работы: размер бизнеса, сложность, сколько ролей интервьюировать, процессов, документов, оценка трудозатрат.\n" "2. Проанализируй РЫНОК: вилка цен на аналогичный консалтинг (разбор бизнеса + ТЗ на ПО) в РФ для такого масштаба.\n" "3. Предложи 2-3 ПАКЕТА (напр. Экспресс / Стандарт / Премиум или по этапам) с ценами в рублях, составом, сроком и аргументом цены.\n" "4. Рекомендуй пакет под этого клиента.\n" "5. Главный аргумент по цене — через ROI/экономию клиента (сколько он теряет сейчас vs стоимость).\n" "Цены реалистичные для МСБ РФ. Вызови build_pricing.") result, usage = run_tool(proj["id"], PRICING_TOOL, "build_pricing", instr, extra_context=extra, max_tokens=2500) if result is None: return jsonify({"error": usage}), 500 save_artifact(proj["id"], "pricing", result) return jsonify({"pricing": result, "usage": usage}) @app.route("/api/project/") def get_project_state(token): proj = get_project(token) if not proj: return jsonify({"error": "not found"}), 404 msgs = db().execute( "SELECT role, content, created_at FROM messages WHERE project_id=? ORDER BY id", (proj["id"],) ).fetchall() model_row = db().execute( "SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (proj["id"],) ).fetchone() crm = latest_artifact(proj["id"], "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":"","billing_type":"paid"} payments = crm.get("payments", []) if isinstance(crm.get("payments"), list) else [] paid_total = sum(p.get("amount", 0) for p in payments) if payments else (crm.get("paid_amount", 0) or 0) deal_amount = crm.get("deal_amount", 0) or 0 debt = max(0, deal_amount - paid_total) billing = crm.get("billing_type", "paid") # Разблокировка выгрузки документов/ТЗ: бесплатный клиент ИЛИ долг закрыт (платный с deal>0) unlocked = (billing == "free") or (deal_amount > 0 and debt <= 0) crm["paid_amount"] = paid_total crm["debt"] = debt return jsonify({ "token": token, "client_name": proj["client_name"], "niche": proj["niche"], "description": proj["description"] if "description" in proj.keys() else "", "status": proj["status"], "messages": [{"role": m["role"], "content": m["content"], "at": m["created_at"]} for m in msgs], "model": json.loads(model_row["blocks_json"]) if model_row else None, "selection": latest_artifact(proj["id"], "selection"), "canvas": latest_artifact(proj["id"], "canvas"), "spec": latest_artifact(proj["id"], "spec"), "approvals": latest_artifact(proj["id"], "approvals") or {}, "crm": crm, "unlocked": unlocked, "debt": debt, "paid_total": paid_total, "deal_amount": deal_amount, "billing_type": billing, "tasks": (latest_artifact(proj["id"], "tasks") or {}).get("items", []), "pricing": latest_artifact(proj["id"], "pricing"), "signed": db().execute("SELECT 1 FROM acceptances WHERE project_id=? AND doc='offer' LIMIT 1", (proj["id"],)).fetchone() is not None, "documents": [json.loads(r["data_json"]) and {"filename": json.loads(r["data_json"])["filename"], "size": json.loads(r["data_json"]).get("size",0)} for r in db().execute("SELECT data_json FROM artifacts WHERE project_id=? AND kind='document' ORDER BY id", (proj["id"],)).fetchall()] }) # ── Telegram Bot ───────────────────────────────────── TG_TOKEN = "8767209545:AAEVgfL-bAhg6j0fHUyKWUze4SLTfJbLklM" TG_API = f"https://api.telegram.org/bot{TG_TOKEN}" CABINET_URL = "https://wasrusgen1.ru/consulting/cabinet.html" def tg_send(chat_id, text, reply_markup=None): import urllib.request as ur payload = {"chat_id": chat_id, "text": text, "parse_mode": "HTML"} if reply_markup: payload["reply_markup"] = json.dumps(reply_markup) body = json.dumps(payload).encode() req = ur.Request(f"{TG_API}/sendMessage", data=body, headers={"Content-Type": "application/json"}) try: ur.urlopen(req, timeout=8) except Exception as e: app.logger.error(f"tg_send error: {e}") @app.route("/api/tg/webhook", methods=["POST"]) def tg_webhook(): data = request.get_json(silent=True) or {} msg = data.get("message") or data.get("callback_query", {}).get("message") if not msg: return jsonify({"ok": True}) chat_id = msg["chat"]["id"] text = (msg.get("text") or "").strip() cmd = text.split()[0].split("@")[0].lower() if text.startswith("/") else "" # /start [token] — открыть кабинет или прислать ссылку if cmd == "/start": parts = text.split() token = parts[1] if len(parts) > 1 else None if token: proj = get_project(token) if proj: url = f"{CABINET_URL}?t={token}" tg_send(chat_id, f"Привет! Ваш проект: {proj['client_name'] or 'без названия'}\n" f"Кабинет: {url}", reply_markup={"inline_keyboard": [[ {"text": "Открыть кабинет", "web_app": {"url": url}} ]]} ) return jsonify({"ok": True}) tg_send(chat_id, "Добро пожаловать в @wasrusgen1 | КОНСАЛТИНГ\n\n" "Для доступа к кабинету вам нужна персональная ссылка от консультанта.\n\n" "Напишите нам: @wasrusgen1", ) # /status — статус проекта по tg_id (если привязан) elif cmd == "/status": row = db().execute( "SELECT token, client_name, status FROM projects WHERE tg_chat_id=? ORDER BY id DESC LIMIT 1", (str(chat_id),) ).fetchone() if row: tg_send(chat_id, f"Проект: {row['client_name']}\n" f"Статус: {row['status']}\n" f"Кабинет: {CABINET_URL}?t={row['token']}" ) else: tg_send(chat_id, "Привязанный проект не найден. Откройте кабинет по ссылке от консультанта.") elif cmd == "/help": tg_send(chat_id, "/start — открыть кабинет\n" "/status — статус проекта\n\n" "По вопросам: @wasrusgen1" ) return jsonify({"ok": True}) if __name__ == "__main__": init_db() app.run(host="0.0.0.0", port=5002)