version: "0.7.0" kind: app app: name: Site Factory mode: workflow icon: "🌐" icon_background: "#E8F3FF" icon_type: emoji description: "Генерирует одностраничный HTML по брифу и публикует на https://{slug}.apps.butrix.dev" use_icon_as_answer_icon: false dependencies: [] workflow: conversation_variables: [] environment_variables: - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" name: SITE_FACTORY_TOKEN value: "" value_type: secret description: "Bearer-токен Site Factory (вставьте значение из credentials/site-factory.env)" rag_pipeline_variables: [] features: file_upload: enabled: false opening_statement: "" retriever_resource: enabled: false sensitive_word_avoidance: enabled: false speech_to_text: enabled: false suggested_questions: [] suggested_questions_after_answer: enabled: false text_to_speech: enabled: false graph: viewport: x: 0 y: 0 zoom: 0.7 edges: - id: e-start-llm source: "1726147200001" sourceHandle: source target: "1726147200002" targetHandle: target type: custom zIndex: 0 data: sourceType: start targetType: llm isInIteration: false isInLoop: false - id: e-llm-clean source: "1726147200002" sourceHandle: source target: "1726147200003" targetHandle: target type: custom zIndex: 0 data: sourceType: llm targetType: code isInIteration: false isInLoop: false - id: e-clean-http source: "1726147200003" sourceHandle: source target: "1726147200004" targetHandle: target type: custom zIndex: 0 data: sourceType: code targetType: http-request isInIteration: false isInLoop: false - id: e-http-parse source: "1726147200004" sourceHandle: source target: "1726147200005" targetHandle: target type: custom zIndex: 0 data: sourceType: http-request targetType: code isInIteration: false isInLoop: false - id: e-parse-end source: "1726147200005" sourceHandle: source target: "1726147200006" targetHandle: target type: custom zIndex: 0 data: sourceType: code targetType: end isInIteration: false isInLoop: false nodes: - id: "1726147200001" type: custom position: {x: 30, y: 220} positionAbsolute: {x: 30, y: 220} selected: false sourcePosition: right targetPosition: left width: 244 height: 118 data: type: start title: Start desc: "brief + slug" selected: false variables: - variable: brief label: brief type: paragraph required: true max_length: 4000 options: [] - variable: slug label: slug type: text-input required: true max_length: 48 options: [] - id: "1726147200002" type: custom position: {x: 330, y: 220} positionAbsolute: {x: 330, y: 220} selected: false sourcePosition: right targetPosition: left width: 244 height: 90 data: type: llm title: Generate HTML desc: "gemini-3.6-flash via LiteLLM / OpenAI-compatible" model: provider: langgenius/openai_api_compatible/openai_api_compatible name: gemini-3.6-flash mode: chat completion_params: temperature: 0.7 prompt_template: - role: system text: | You are a senior web designer and frontend engineer. Generate a complete, beautiful, responsive single-file HTML document. Rules: - Output RAW HTML only. No markdown, no explanations, no code fences. - Start with . End with . - Inline CSS and JS only (no external stylesheets/scripts except common CDNs if truly needed; prefer zero external deps). - Language of the page = language of the user's brief (Russian if the brief is Russian). - Mobile-first, accessible, distinctive visual design (not a generic purple gradient template). - Semantic HTML5. Meta viewport + title + description. - If the brief is thin, invent tasteful content that still matches the request. - role: user text: | Subdomain / slug: {{#1726147200001.slug#}} Brief: {{#1726147200001.brief#}} context: enabled: false variable_selector: [] vision: enabled: false configs: variable_selector: [] memory: enabled: false window: enabled: false size: 10 structured_output: enabled: false retry_config: retry_enabled: true max_retries: 2 retry_interval: 1000 - id: "1726147200003" type: custom position: {x: 630, y: 220} positionAbsolute: {x: 630, y: 220} selected: false sourcePosition: right targetPosition: left width: 244 height: 90 data: type: code title: Clean HTML + JSON desc: "Strip markdown fences and JSON-encode payload" code_language: python3 code: | import json import re def main(html_raw: str, slug: str) -> dict: text = (html_raw or "").strip() m = re.search(r"```(?:html|HTML|xml)?\s*\n([\s\S]*?)\n```", text) if m: text = m.group(1).strip() elif text.startswith("```"): text = re.sub(r"^```[A-Za-z0-9_-]*\s*", "", text) text = re.sub(r"\s*```$", "", text).strip() if "" f"{slug}{text}" ) payload = json.dumps( {"slug": slug, "html": text, "title": slug}, ensure_ascii=False, ) return {"html": text, "payload": payload} variables: - variable: html_raw value_selector: - "1726147200002" - text - variable: slug value_selector: - "1726147200001" - slug outputs: html: type: string children: null payload: type: string children: null - id: "1726147200004" type: custom position: {x: 930, y: 220} positionAbsolute: {x: 930, y: 220} selected: false sourcePosition: right targetPosition: left width: 244 height: 90 data: type: http-request title: POST /deploy desc: "Internal Site Factory API (ai-stack). Token = env SITE_FACTORY_TOKEN" method: POST url: http://site-factory-api:8080/deploy authorization: type: api-key config: type: bearer api_key: "{{#env.SITE_FACTORY_TOKEN#}}" headers: "Content-Type: application/json" params: "" body: type: json data: - key: "" type: text value: "{{#1726147200003.payload#}}" timeout: connect: 10 read: 30 write: 30 ssl_verify: false retry_config: retry_enabled: true max_retries: 2 retry_interval: 1000 - id: "1726147200005" type: custom position: {x: 1230, y: 220} positionAbsolute: {x: 1230, y: 220} selected: false sourcePosition: right targetPosition: left width: 244 height: 90 data: type: code title: Parse deploy result desc: "" code_language: python3 code: | import json def main(body: str, status_code) -> dict: raw = body or "" try: data = json.loads(raw) except Exception: data = {} url = data.get("url") or "" slug = data.get("slug") or "" err = data.get("detail") or "" try: code = int(status_code) except Exception: code = 0 if code >= 400 and not err: err = raw[:500] return { "url": url, "slug": slug, "status_code": code, "error": str(err), } variables: - variable: body value_selector: - "1726147200004" - body - variable: status_code value_selector: - "1726147200004" - status_code outputs: url: type: string children: null slug: type: string children: null status_code: type: number children: null error: type: string children: null - id: "1726147200006" type: custom position: {x: 1530, y: 220} positionAbsolute: {x: 1530, y: 220} selected: false sourcePosition: right targetPosition: left width: 244 height: 90 data: type: end title: End desc: "" selected: false outputs: - variable: url value_selector: - "1726147200005" - url value_type: string - variable: slug value_selector: - "1726147200005" - slug value_type: string - variable: status_code value_selector: - "1726147200005" - status_code value_type: number - variable: error value_selector: - "1726147200005" - error value_type: string