ALTER TABLE raw_posts ADD COLUMN IF NOT EXISTS qualification_status TEXT, ADD COLUMN IF NOT EXISTS qualification_score INTEGER, ADD COLUMN IF NOT EXISTS qualification_decision TEXT, ADD COLUMN IF NOT EXISTS qualification_reason TEXT, ADD COLUMN IF NOT EXISTS qualification_model TEXT, ADD COLUMN IF NOT EXISTS qualification_prompt_hash TEXT, ADD COLUMN IF NOT EXISTS qualification_batch_id BIGINT, ADD COLUMN IF NOT EXISTS qualified_at TIMESTAMPTZ; CREATE INDEX IF NOT EXISTS idx_raw_posts_qualification_claim ON raw_posts(status, qualification_status, created_at) WHERE status='storage_ready'; CREATE TABLE IF NOT EXISTS ai_qualification_batches ( id BIGSERIAL PRIMARY KEY, status TEXT NOT NULL DEFAULT 'processing', provider TEXT NOT NULL, model TEXT NOT NULL, prompt_hash TEXT NOT NULL, posts_count INTEGER NOT NULL DEFAULT 0, accepted_count INTEGER NOT NULL DEFAULT 0, rejected_count INTEGER NOT NULL DEFAULT 0, maybe_count INTEGER NOT NULL DEFAULT 0, request_json JSONB NOT NULL DEFAULT '{}'::jsonb, response_json JSONB, error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ ); INSERT INTO worker_controls(name, enabled, settings_json) VALUES ('ai-qualifier', FALSE, '{}'::jsonb) ON CONFLICT (name) DO NOTHING; INSERT INTO app_settings(key, value_json, value_type, title, description, category) VALUES ('ai_qualifier_enabled', 'false'::jsonb, 'bool', 'AI qualifier enabled', 'Enable qualification requests to AI API.', 'AI Qualifier'), ('ai_qualifier_provider', '"openrouter"'::jsonb, 'str', 'Provider', 'LiteLLM provider: openrouter, openai, anthropic, gemini, openai_compatible.', 'AI Qualifier'), ('ai_qualifier_model', '"openrouter/openai/gpt-4.1-mini"'::jsonb, 'str', 'Model', 'Model name in LiteLLM format.', 'AI Qualifier'), ('ai_qualifier_api_key', '""'::jsonb, 'secret', 'API key', 'AI provider API key.', 'AI Qualifier'), ('ai_qualifier_api_base', '""'::jsonb, 'str', 'API base', 'Optional base URL for OpenAI-compatible providers.', 'AI Qualifier'), ('ai_qualifier_batch_size', '30'::jsonb, 'int', 'Batch size', 'How many raw posts to classify in one request.', 'AI Qualifier'), ('ai_qualifier_min_score', '7'::jsonb, 'int', 'Minimum accepted score', 'Posts with score >= this value are accepted.', 'AI Qualifier'), ('ai_qualifier_interval_sec', '60'::jsonb, 'int', 'Idle interval, sec', 'Pause between qualifier checks.', 'AI Qualifier'), ('ai_qualifier_max_text_chars', '2000'::jsonb, 'int', 'Max text chars per post', 'Trim raw text before sending it to AI.', 'AI Qualifier'), ('ai_qualifier_temperature', '0'::jsonb, 'float', 'Temperature', 'AI sampling temperature for stable classification.', 'AI Qualifier'), ('ai_qualifier_timeout_sec', '120'::jsonb, 'int', 'Timeout, sec', 'AI request timeout.', 'AI Qualifier'), ('ai_qualifier_prompt', '"Ты квалификатор постов для проекта, который собирает посты производителей и магазинов милитари, тактической и airsoft-снаряги.\\n\\nОцени каждый raw-пост по пригодности для дальнейшего рерайта и публикации.\\n\\nПодходит: товарные посты, новинки, обзоры, производство, экипировка, тактическая одежда, броня, подсумки, рюкзаки, разгрузки, ремни, патчи, аксессуары, airsoft/страйкбольная снаряга, полезные анонсы производителя.\\n\\nМусор: мемы, поздравления, вакансии, политика, бытовуха, пустые посты, посты без конкретного товара/снаряжения, розыгрыши без предметного контента, чистая реклама скидки без понятного товара, нерелевантные новости.\\n\\nВерни только JSON-объект по схеме: {\\\"results\\\":[{\\\"id\\\":123,\\\"score\\\":8,\\\"decision\\\":\\\"accepted\\\",\\\"reason\\\":\\\"короткая причина\\\"}]}. score — целое число от 1 до 10. decision: accepted, rejected или maybe. Не добавляй markdown и пояснения вне JSON."'::jsonb, 'text', 'Prompt', 'Main qualification prompt.', 'AI Qualifier') ON CONFLICT (key) DO NOTHING;