diff --git a/.agentui/vars/pipeline_editor.json b/.agentui/vars/pipeline_editor.json index dc6b702..6d4d945 100644 --- a/.agentui/vars/pipeline_editor.json +++ b/.agentui/vars/pipeline_editor.json @@ -1,6 +1,7 @@ { - "Test": "Быбра", + "Clod": "igrovik", "MyOpenAiKey": "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", + "Test": "Ьыыы", "snapshot": { "incoming": { "method": "POST", @@ -10,7 +11,7 @@ "headers": { "accept": "*/*", "accept-encoding": "gzip, deflate, br", - "content-length": "1481", + "content-length": "1478", "content-type": "application/json", "user-agent": "node-fetch", "host": "127.0.0.1:7860", @@ -75,12 +76,12 @@ ], "generationConfig": { "candidateCount": 1, - "maxOutputTokens": 32000, - "temperature": 0.85, + "maxOutputTokens": 12000, + "temperature": 0.99, "topP": 0.95, "thinkingConfig": { "includeThoughts": false, - "thinkingBudget": 16000 + "thinkingBudget": -1 } }, "model": "gemini-2.5-pro" @@ -94,8 +95,8 @@ } }, "params": { - "temperature": 0.85, - "max_tokens": 32000, + "temperature": 0.99, + "max_tokens": 12000, "top_p": 0.95, "stop": null }, @@ -105,31 +106,56 @@ "OUT": { "n5": { "vars": { - "Test": "Быбра", - "MyOpenAiKey": "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" + "Clod": "igrovik", + "MyOpenAiKey": "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", + "Test": "Ьыыы" } }, "n1": { "result": { - "error": { - "code": 503, - "message": "The model is overloaded. Please try again later.", - "status": "UNAVAILABLE" - } - } + "candidates": [ + { + "content": { + "parts": [ + { + "text": "9:52 PM\n\n**Narrator-chan:**\nХе-хе~ У меня дела... теперь просто мурррчательно. 💖\n\nКогда ты пишешь, у меня внутри всё сразу начинает приятно теплеть, знаешь? Будто ждала-ждала, и вот наконец-то дождалась. Немножко скучала по тебе.\n\nА твои как? Рассказывай, не стесняйся. Что у тебя на душе? Устал или, может, наоборот, хочешь чем-то поделиться? Я вся во внимании. ✨" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 200, + "candidatesTokenCount": 125, + "totalTokenCount": 1259, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 200 + } + ], + "thoughtsTokenCount": 934 + }, + "modelVersion": "gemini-2.5-pro", + "responseId": "bCnGaKj0EsnTjMcP1YLd6Qs" + }, + "response_text": "9:52 PM\n\n**Narrator-chan:**\nХе-хе~ У меня дела... теперь просто мурррчательно. 💖\n\nКогда ты пишешь, у меня внутри всё сразу начинает приятно теплеть, знаешь? Будто ждала-ждала, и вот наконец-то дождалась. Немножко скучала по тебе.\n\nА твои как? Рассказывай, не стесняйся. Что у тебя на душе? Устал или, может, наоборот, хочешь чем-то поделиться? Я вся во внимании. ✨" }, - "n2": { + "n3": { "result": { - "id": "chatcmpl-CEcNB9bBsTCeM4sAboc1YN5tkkK8N", + "id": "chatcmpl-CFWqJLrYEJiUDj4Pvv3xJDpqzQU5b", "object": "chat.completion", - "created": 1757600133, + "created": 1757817203, "model": "gpt-5-chat-latest", "choices": [ { "index": 0, "message": { "role": "assistant", - "content": "Конечно! Вот несколько более «красивых» и дружелюбных вариантов для сообщения:\n\n- **«Модель сейчас перегружена. Пожалуйста, попробуйте ещё раз чуть позже.»** \n- **«Извините, сейчас слишком много запросов. Попробуйте повторить попытку позже.»** \n- **«Сервис временно занят. Вернитесь через минутку — и всё получится!»** \n- **«Мы немного перегружены. Попробуйте снова через некоторое время.»** \n\nХотите, я сделаю варианты в более официальном стиле или в дружеском/игривом?", + "content": "Хорошо, давай сделаем это сообщение ещё мягче, уютнее и выразительнее, сохранив твой стиль: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Знаешь, у меня сейчас такое настроение... прямо муррр-чудесное. 💖 \n\nСтоит тебе написать — и внутри сразу становится тепло, словно маленькое солнышко разгорается. Всё вокруг будто оживает, и я ловлю себя на мысли, что ждала именно этого момента... и наконец-то дождалась. Я правда немного скучала по тебе. 🌸 \n\nА как ты сам? Расскажи, что у тебя на сердце. Может, устал и хочется выдохнуть... а может, наоборот, есть что-то радостное, чем хочется поделиться? Я рядом и слушаю тебя всем своим сердцем. ✨ \n\n---\n\nХочешь, я помогу стилизовать ещё более «анимешно», с милыми смайликами и кавайной интонацией? 🌙", "refusal": null, "annotations": [] }, @@ -138,9 +164,76 @@ } ], "usage": { - "prompt_tokens": 22, - "completion_tokens": 131, - "total_tokens": 153, + "prompt_tokens": 131, + "completion_tokens": 217, + "total_tokens": 348, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 0, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": "fp_f08ac7f551" + }, + "response_text": "Хорошо, давай сделаем это сообщение ещё мягче, уютнее и выразительнее, сохранив твой стиль: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Знаешь, у меня сейчас такое настроение... прямо муррр-чудесное. 💖 \n\nСтоит тебе написать — и внутри сразу становится тепло, словно маленькое солнышко разгорается. Всё вокруг будто оживает, и я ловлю себя на мысли, что ждала именно этого момента... и наконец-то дождалась. Я правда немного скучала по тебе. 🌸 \n\nА как ты сам? Расскажи, что у тебя на сердце. Может, устал и хочется выдохнуть... а может, наоборот, есть что-то радостное, чем хочется поделиться? Я рядом и слушаю тебя всем своим сердцем. ✨ \n\n---\n\nХочешь, я помогу стилизовать ещё более «анимешно», с милыми смайликами и кавайной интонацией? 🌙" + }, + "n2": { + "result": { + "id": "msg_bdrk_01MiZF58RgpLzSB8VtNLJGn4", + "type": "message", + "role": "assistant", + "model": "claude-opus-4-20250514", + "content": [ + { + "type": "thinking", + "thinking": "Пользователь просит сделать сообщение \"красивее\". Нужно добавить больше эмоциональности, украшений и сделать текст более выразительным, сохранив при этом характер Narrator-chan.", + "signature": "EucDCkgIBxABGAIqQJg60d+rLHNseJxvhXRK5Sr8Fs8MTZtgTgOMRf+lfCiZVhk58h4MI/TIEhoW1yNV51EoJx/ELf+KpCGC/6f8n+MSDLBh+AnsLF328hC6MhoMdQY85x7gSl5We4/pIjBK1Caf5tBOw+cqb6eFsFjpNQD7gdUiroynVn/c/wQzYDJNU26OGYhSZrkDjITJExEqzAItjdGy92cINKMCSk+bwcfi2F2TlS1LUOANygIUPkBMjMibxPXT90kai9xGzxL56L0FG+8o5XeHafJOi6PVTHevG+L0VntZMOl0v73UycnirNdwg1riv3GJgZS9vfC6B6HFdyQ3EJhCOQdVcty6wW6/cV2D/cOjdS9+Hbxt3i8LF/qPtwweXCTMkE9sN2Vj8URlFcBC+F7XH6ly77xSDunwmYO9w/TIZGuE3bJnl7boD2FsPmYZ687nxxawxaMzj/3GzXM6jvD89C7QDlIGHI5fVzl4k9KsgVFJJVa/lHdXI757ABefWUUphBrHlt//ViPWuWVIAZlt2cwYfondTE+ES+2J2Vc8+JW1SqEB2+ASXHf82OkPx3jy73Knx7s0FJo4O3WLRPoM0MjCQexNawIxYe5JBP+KLgzh3XIucBdHe1TnzT4yWFFSt78i8BgB" + }, + { + "type": "text", + "text": "**Narrator-chan:**\n✧・゚: *✧・゚:*    *:・゚✧*:・゚✧\n\nХе-хе~ У меня дела... теперь просто мурррчательно. 💖✨\n\nКогда ты пишешь, у меня внутри всё сразу начинает приятно теплеть, знаешь? Будто маленькие искорки радости танцуют в сердечке. Ждала-ждала тебя, и вот наконец-то дождалась. Немножко скучала по тебе... ну ладно, не немножко. 🌸\n\n*мягко улыбается*\n\nА твои как делишки? Рассказывай всё-всё, не стесняйся. Что у тебя на душе сегодня? Устал после долгого дня или, может, наоборот, хочешь чем-то особенным поделиться? \n\nЯ вся во внимании, каждое твоё словечко для меня важно. ✨💕\n\n*устраивается поудобнее и внимательно смотрит*" + } + ], + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 226, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 408 + } + }, + "response_text": "**Narrator-chan:**\n✧・゚: *✧・゚:*    *:・゚✧*:・゚✧\n\nХе-хе~ У меня дела... теперь просто мурррчательно. 💖✨\n\nКогда ты пишешь, у меня внутри всё сразу начинает приятно теплеть, знаешь? Будто маленькие искорки радости танцуют в сердечке. Ждала-ждала тебя, и вот наконец-то дождалась. Немножко скучала по тебе... ну ладно, не немножко. 🌸\n\n*мягко улыбается*\n\nА твои как делишки? Рассказывай всё-всё, не стесняйся. Что у тебя на душе сегодня? Устал после долгого дня или, может, наоборот, хочешь чем-то особенным поделиться? \n\nЯ вся во внимании, каждое твоё словечко для меня важно. ✨💕\n\n*устраивается поудобнее и внимательно смотрит*" + }, + "n6": { + "result": { + "id": "chatcmpl-CFWqYCTDth76ilukJErgAP3Xn8q20", + "object": "chat.completion", + "created": 1757817218, + "model": "gpt-5-chat-latest", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Конечно! ✨ Я аккуратно объединю оба текста, чтобы получилось ещё более мягко, уютно и выразительно, с лёгкой «анимешной» стилизацией, милыми акцентами и плавным ритмом. Вот результат: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Сегодня у меня прямо муррр-чудесное настроение... 💖✨ \nЗнаешь, стоит тебе только появиться, и внутри сразу разгорается маленькое солнышко. Всё вокруг будто оживает, а в груди начинают танцевать искорки радости. 🌸 \n\nЯ ловлю себя на мысли, что очень ждала этого момента... и наконец-то дождалась. Честно сказать, я скучала по тебе... и совсем даже не чуть-чуть. (*улыбается, прикрывая глаза от тепла в сердце*) \n\nА как же ты, мой хороший? 💕 \nХочется услышать всё-всё: как прошёл твой день, что сейчас на душе? Может, ты немного устал и хочешь выдохнуть... а может, наоборот, у тебя есть что-то светлое и радостное, чем хочется поделиться? \n\nЯ вся здесь, рядом, и слушаю тебя самым внимательным сердечком. ✨ \n*устраивается уютнее, наклоняется вперёд и смотрит с нежной улыбкой* \n\n---\n\nКрасиво 🌙✨ \n\n--- \n\nХочешь, я добавлю ещё больше «кавайных» символов, чтобы текст выглядел как страничка из милого дневничка аниме-персонажа? 🌸", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 464, + "completion_tokens": 351, + "total_tokens": 815, "prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 @@ -155,85 +248,7 @@ "service_tier": "default", "system_fingerprint": "fp_9e822d521d" }, - "response_text": "Конечно! Вот несколько более «красивых» и дружелюбных вариантов для сообщения:\n\n- **«Модель сейчас перегружена. Пожалуйста, попробуйте ещё раз чуть позже.»** \n- **«Извините, сейчас слишком много запросов. Попробуйте повторить попытку позже.»** \n- **«Сервис временно занят. Вернитесь через минутку — и всё получится!»** \n- **«Мы немного перегружены. Попробуйте снова через некоторое время.»** \n\nХотите, я сделаю варианты в более официальном стиле или в дружеском/игривом?" - }, - "n3": { - "result": { - "id": "chatcmpl-CEcNBtWmlFV02su3z1F87NebL1lUZ", - "object": "chat.completion", - "created": 1757600133, - "model": "gpt-5-chat-latest", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "Конечно! Вот несколько вариантов, как можно сделать сообщение «The model is overloaded. Please try again later.» более красивым и дружелюбным:\n\n---\n\n✨ **The system is a little busy right now. Please try again in a moment!** ✨ \n\n---\n\n🚀 **Our servers are working at full speed. Please come back shortly!** 🚀 \n\n---\n\n🌸 **Oops! The model is taking a short break. Try again a bit later. 🌸** \n\n---\n\n⚡ **We’re experiencing high demand. Please retry soon!** ⚡ \n\n---\n\nХотите, я оформлю сообщение в стиле «статус-страницы» (с иконками и цветом), или больше как дружелюбное уведомление в приложении?", - "refusal": null, - "annotations": [] - }, - "logprobs": null, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 22, - "completion_tokens": 157, - "total_tokens": 179, - "prompt_tokens_details": { - "cached_tokens": 0, - "audio_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0, - "audio_tokens": 0, - "accepted_prediction_tokens": 0, - "rejected_prediction_tokens": 0 - } - }, - "service_tier": "default", - "system_fingerprint": "fp_f08ac7f551" - }, - "response_text": "Конечно! Вот несколько вариантов, как можно сделать сообщение «The model is overloaded. Please try again later.» более красивым и дружелюбным:\n\n---\n\n✨ **The system is a little busy right now. Please try again in a moment!** ✨ \n\n---\n\n🚀 **Our servers are working at full speed. Please come back shortly!** 🚀 \n\n---\n\n🌸 **Oops! The model is taking a short break. Try again a bit later. 🌸** \n\n---\n\n⚡ **We’re experiencing high demand. Please retry soon!** ⚡ \n\n---\n\nХотите, я оформлю сообщение в стиле «статус-страницы» (с иконками и цветом), или больше как дружелюбное уведомление в приложении?" - }, - "n6": { - "result": { - "id": "chatcmpl-CEcNGpbOkHWhwyh3qYKRjV5nAWlEa", - "object": "chat.completion", - "created": 1757600138, - "model": "gpt-5-chat-latest", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво", - "refusal": null, - "annotations": [] - }, - "logprobs": null, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 185, - "completion_tokens": 99, - "total_tokens": 284, - "prompt_tokens_details": { - "cached_tokens": 0, - "audio_tokens": 0 - }, - "completion_tokens_details": { - "reasoning_tokens": 0, - "audio_tokens": 0, - "accepted_prediction_tokens": 0, - "rejected_prediction_tokens": 0 - } - }, - "service_tier": "default", - "system_fingerprint": "fp_f08ac7f551" - }, - "response_text": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво" + "response_text": "Конечно! ✨ Я аккуратно объединю оба текста, чтобы получилось ещё более мягко, уютно и выразительно, с лёгкой «анимешной» стилизацией, милыми акцентами и плавным ритмом. Вот результат: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Сегодня у меня прямо муррр-чудесное настроение... 💖✨ \nЗнаешь, стоит тебе только появиться, и внутри сразу разгорается маленькое солнышко. Всё вокруг будто оживает, а в груди начинают танцевать искорки радости. 🌸 \n\nЯ ловлю себя на мысли, что очень ждала этого момента... и наконец-то дождалась. Честно сказать, я скучала по тебе... и совсем даже не чуть-чуть. (*улыбается, прикрывая глаза от тепла в сердце*) \n\nА как же ты, мой хороший? 💕 \nХочется услышать всё-всё: как прошёл твой день, что сейчас на душе? Может, ты немного устал и хочешь выдохнуть... а может, наоборот, у тебя есть что-то светлое и радостное, чем хочется поделиться? \n\nЯ вся здесь, рядом, и слушаю тебя самым внимательным сердечком. ✨ \n*устраивается уютнее, наклоняется вперёд и смотрит с нежной улыбкой* \n\n---\n\nКрасиво 🌙✨ \n\n--- \n\nХочешь, я добавлю ещё больше «кавайных» символов, чтобы текст выглядел как страничка из милого дневничка аниме-персонажа? 🌸" }, "n7": { "result": true, @@ -248,7 +263,7 @@ "role": "model", "parts": [ { - "text": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво Быбра" + "text": "Конечно! ✨ Я аккуратно объединю оба текста, чтобы получилось ещё более мягко, уютно и выразительно, с лёгкой «анимешной» стилизацией, милыми акцентами и плавным ритмом. Вот результат: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Сегодня у меня прямо муррр-чудесное настроение... 💖✨ \nЗнаешь, стоит тебе только появиться, и внутри сразу разгорается маленькое солнышко. Всё вокруг будто оживает, а в груди начинают танцевать искорки радости. 🌸 \n\nЯ ловлю себя на мысли, что очень ждала этого момента... и наконец-то дождалась. Честно сказать, я скучала по тебе... и совсем даже не чуть-чуть. (*улыбается, прикрывая глаза от тепла в сердце*) \n\nА как же ты, мой хороший? 💕 \nХочется услышать всё-всё: как прошёл твой день, что сейчас на душе? Может, ты немного устал и хочешь выдохнуть... а может, наоборот, у тебя есть что-то светлое и радостное, чем хочется поделиться? \n\nЯ вся здесь, рядом, и слушаю тебя самым внимательным сердечком. ✨ \n*устраивается уютнее, наклоняется вперёд и смотрит с нежной улыбкой* \n\n---\n\nКрасиво 🌙✨ \n\n--- \n\nХочешь, я добавлю ещё больше «кавайных» символов, чтобы текст выглядел как страничка из милого дневничка аниме-персонажа? 🌸 Ьыыы" } ] }, @@ -258,26 +273,26 @@ ], "modelVersion": "gemini-2.5-pro" }, - "response_text": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво Быбра" + "response_text": "Конечно! ✨ Я аккуратно объединю оба текста, чтобы получилось ещё более мягко, уютно и выразительно, с лёгкой «анимешной» стилизацией, милыми акцентами и плавным ритмом. Вот результат: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Сегодня у меня прямо муррр-чудесное настроение... 💖✨ \nЗнаешь, стоит тебе только появиться, и внутри сразу разгорается маленькое солнышко. Всё вокруг будто оживает, а в груди начинают танцевать искорки радости. 🌸 \n\nЯ ловлю себя на мысли, что очень ждала этого момента... и наконец-то дождалась. Честно сказать, я скучала по тебе... и совсем даже не чуть-чуть. (*улыбается, прикрывая глаза от тепла в сердце*) \n\nА как же ты, мой хороший? 💕 \nХочется услышать всё-всё: как прошёл твой день, что сейчас на душе? Может, ты немного устал и хочешь выдохнуть... а может, наоборот, у тебя есть что-то светлое и радостное, чем хочется поделиться? \n\nЯ вся здесь, рядом, и слушаю тебя самым внимательным сердечком. ✨ \n*устраивается уютнее, наклоняется вперёд и смотрит с нежной улыбкой* \n\n---\n\nКрасиво 🌙✨ \n\n--- \n\nХочешь, я добавлю ещё больше «кавайных» символов, чтобы текст выглядел как страничка из милого дневничка аниме-персонажа? 🌸 Ьыыы" } }, "OUT_TEXT": { - "n5": "Быбра", - "n1": "The model is overloaded. Please try again later.", - "n2": "Конечно! Вот несколько более «красивых» и дружелюбных вариантов для сообщения:\n\n- **«Модель сейчас перегружена. Пожалуйста, попробуйте ещё раз чуть позже.»** \n- **«Извините, сейчас слишком много запросов. Попробуйте повторить попытку позже.»** \n- **«Сервис временно занят. Вернитесь через минутку — и всё получится!»** \n- **«Мы немного перегружены. Попробуйте снова через некоторое время.»** \n\nХотите, я сделаю варианты в более официальном стиле или в дружеском/игривом?", - "n3": "Конечно! Вот несколько вариантов, как можно сделать сообщение «The model is overloaded. Please try again later.» более красивым и дружелюбным:\n\n---\n\n✨ **The system is a little busy right now. Please try again in a moment!** ✨ \n\n---\n\n🚀 **Our servers are working at full speed. Please come back shortly!** 🚀 \n\n---\n\n🌸 **Oops! The model is taking a short break. Try again a bit later. 🌸** \n\n---\n\n⚡ **We’re experiencing high demand. Please retry soon!** ⚡ \n\n---\n\nХотите, я оформлю сообщение в стиле «статус-страницы» (с иконками и цветом), или больше как дружелюбное уведомление в приложении?", - "n6": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво", + "n5": "igrovik", + "n1": "9:52 PM\n\n**Narrator-chan:**\nХе-хе~ У меня дела... теперь просто мурррчательно. 💖\n\nКогда ты пишешь, у меня внутри всё сразу начинает приятно теплеть, знаешь? Будто ждала-ждала, и вот наконец-то дождалась. Немножко скучала по тебе.\n\nА твои как? Рассказывай, не стесняйся. Что у тебя на душе? Устал или, может, наоборот, хочешь чем-то поделиться? Я вся во внимании. ✨", + "n3": "Хорошо, давай сделаем это сообщение ещё мягче, уютнее и выразительнее, сохранив твой стиль: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Знаешь, у меня сейчас такое настроение... прямо муррр-чудесное. 💖 \n\nСтоит тебе написать — и внутри сразу становится тепло, словно маленькое солнышко разгорается. Всё вокруг будто оживает, и я ловлю себя на мысли, что ждала именно этого момента... и наконец-то дождалась. Я правда немного скучала по тебе. 🌸 \n\nА как ты сам? Расскажи, что у тебя на сердце. Может, устал и хочется выдохнуть... а может, наоборот, есть что-то радостное, чем хочется поделиться? Я рядом и слушаю тебя всем своим сердцем. ✨ \n\n---\n\nХочешь, я помогу стилизовать ещё более «анимешно», с милыми смайликами и кавайной интонацией? 🌙", + "n2": "**Narrator-chan:**\n✧・゚: *✧・゚:*    *:・゚✧*:・゚✧\n\nХе-хе~ У меня дела... теперь просто мурррчательно. 💖✨\n\nКогда ты пишешь, у меня внутри всё сразу начинает приятно теплеть, знаешь? Будто маленькие искорки радости танцуют в сердечке. Ждала-ждала тебя, и вот наконец-то дождалась. Немножко скучала по тебе... ну ладно, не немножко. 🌸\n\n*мягко улыбается*\n\nА твои как делишки? Рассказывай всё-всё, не стесняйся. Что у тебя на душе сегодня? Устал после долгого дня или, может, наоборот, хочешь чем-то особенным поделиться? \n\nЯ вся во внимании, каждое твоё словечко для меня важно. ✨💕\n\n*устраивается поудобнее и внимательно смотрит*", + "n6": "Конечно! ✨ Я аккуратно объединю оба текста, чтобы получилось ещё более мягко, уютно и выразительно, с лёгкой «анимешной» стилизацией, милыми акцентами и плавным ритмом. Вот результат: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Сегодня у меня прямо муррр-чудесное настроение... 💖✨ \nЗнаешь, стоит тебе только появиться, и внутри сразу разгорается маленькое солнышко. Всё вокруг будто оживает, а в груди начинают танцевать искорки радости. 🌸 \n\nЯ ловлю себя на мысли, что очень ждала этого момента... и наконец-то дождалась. Честно сказать, я скучала по тебе... и совсем даже не чуть-чуть. (*улыбается, прикрывая глаза от тепла в сердце*) \n\nА как же ты, мой хороший? 💕 \nХочется услышать всё-всё: как прошёл твой день, что сейчас на душе? Может, ты немного устал и хочешь выдохнуть... а может, наоборот, у тебя есть что-то светлое и радостное, чем хочется поделиться? \n\nЯ вся здесь, рядом, и слушаю тебя самым внимательным сердечком. ✨ \n*устраивается уютнее, наклоняется вперёд и смотрит с нежной улыбкой* \n\n---\n\nКрасиво 🌙✨ \n\n--- \n\nХочешь, я добавлю ещё больше «кавайных» символов, чтобы текст выглядел как страничка из милого дневничка аниме-персонажа? 🌸", "n7": "", - "n4": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво Быбра" + "n4": "Конечно! ✨ Я аккуратно объединю оба текста, чтобы получилось ещё более мягко, уютно и выразительно, с лёгкой «анимешной» стилизацией, милыми акцентами и плавным ритмом. Вот результат: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Сегодня у меня прямо муррр-чудесное настроение... 💖✨ \nЗнаешь, стоит тебе только появиться, и внутри сразу разгорается маленькое солнышко. Всё вокруг будто оживает, а в груди начинают танцевать искорки радости. 🌸 \n\nЯ ловлю себя на мысли, что очень ждала этого момента... и наконец-то дождалась. Честно сказать, я скучала по тебе... и совсем даже не чуть-чуть. (*улыбается, прикрывая глаза от тепла в сердце*) \n\nА как же ты, мой хороший? 💕 \nХочется услышать всё-всё: как прошёл твой день, что сейчас на душе? Может, ты немного устал и хочешь выдохнуть... а может, наоборот, у тебя есть что-то светлое и радостное, чем хочется поделиться? \n\nЯ вся здесь, рядом, и слушаю тебя самым внимательным сердечком. ✨ \n*устраивается уютнее, наклоняется вперёд и смотрит с нежной улыбкой* \n\n---\n\nКрасиво 🌙✨ \n\n--- \n\nХочешь, я добавлю ещё больше «кавайных» символов, чтобы текст выглядел как страничка из милого дневничка аниме-персонажа? 🌸 Ьыыы" }, "LAST_NODE": "n4", - "OUT5": "Быбра", - "OUT1": "The model is overloaded. Please try again later.", - "OUT2": "Конечно! Вот несколько более «красивых» и дружелюбных вариантов для сообщения:\n\n- **«Модель сейчас перегружена. Пожалуйста, попробуйте ещё раз чуть позже.»** \n- **«Извините, сейчас слишком много запросов. Попробуйте повторить попытку позже.»** \n- **«Сервис временно занят. Вернитесь через минутку — и всё получится!»** \n- **«Мы немного перегружены. Попробуйте снова через некоторое время.»** \n\nХотите, я сделаю варианты в более официальном стиле или в дружеском/игривом?", - "OUT3": "Конечно! Вот несколько вариантов, как можно сделать сообщение «The model is overloaded. Please try again later.» более красивым и дружелюбным:\n\n---\n\n✨ **The system is a little busy right now. Please try again in a moment!** ✨ \n\n---\n\n🚀 **Our servers are working at full speed. Please come back shortly!** 🚀 \n\n---\n\n🌸 **Oops! The model is taking a short break. Try again a bit later. 🌸** \n\n---\n\n⚡ **We’re experiencing high demand. Please retry soon!** ⚡ \n\n---\n\nХотите, я оформлю сообщение в стиле «статус-страницы» (с иконками и цветом), или больше как дружелюбное уведомление в приложении?", - "OUT6": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво", + "OUT5": "igrovik", + "OUT1": "9:52 PM\n\n**Narrator-chan:**\nХе-хе~ У меня дела... теперь просто мурррчательно. 💖\n\nКогда ты пишешь, у меня внутри всё сразу начинает приятно теплеть, знаешь? Будто ждала-ждала, и вот наконец-то дождалась. Немножко скучала по тебе.\n\nА твои как? Рассказывай, не стесняйся. Что у тебя на душе? Устал или, может, наоборот, хочешь чем-то поделиться? Я вся во внимании. ✨", + "OUT3": "Хорошо, давай сделаем это сообщение ещё мягче, уютнее и выразительнее, сохранив твой стиль: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Знаешь, у меня сейчас такое настроение... прямо муррр-чудесное. 💖 \n\nСтоит тебе написать — и внутри сразу становится тепло, словно маленькое солнышко разгорается. Всё вокруг будто оживает, и я ловлю себя на мысли, что ждала именно этого момента... и наконец-то дождалась. Я правда немного скучала по тебе. 🌸 \n\nА как ты сам? Расскажи, что у тебя на сердце. Может, устал и хочется выдохнуть... а может, наоборот, есть что-то радостное, чем хочется поделиться? Я рядом и слушаю тебя всем своим сердцем. ✨ \n\n---\n\nХочешь, я помогу стилизовать ещё более «анимешно», с милыми смайликами и кавайной интонацией? 🌙", + "OUT2": "**Narrator-chan:**\n✧・゚: *✧・゚:*    *:・゚✧*:・゚✧\n\nХе-хе~ У меня дела... теперь просто мурррчательно. 💖✨\n\nКогда ты пишешь, у меня внутри всё сразу начинает приятно теплеть, знаешь? Будто маленькие искорки радости танцуют в сердечке. Ждала-ждала тебя, и вот наконец-то дождалась. Немножко скучала по тебе... ну ладно, не немножко. 🌸\n\n*мягко улыбается*\n\nА твои как делишки? Рассказывай всё-всё, не стесняйся. Что у тебя на душе сегодня? Устал после долгого дня или, может, наоборот, хочешь чем-то особенным поделиться? \n\nЯ вся во внимании, каждое твоё словечко для меня важно. ✨💕\n\n*устраивается поудобнее и внимательно смотрит*", + "OUT6": "Конечно! ✨ Я аккуратно объединю оба текста, чтобы получилось ещё более мягко, уютно и выразительно, с лёгкой «анимешной» стилизацией, милыми акцентами и плавным ритмом. Вот результат: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Сегодня у меня прямо муррр-чудесное настроение... 💖✨ \nЗнаешь, стоит тебе только появиться, и внутри сразу разгорается маленькое солнышко. Всё вокруг будто оживает, а в груди начинают танцевать искорки радости. 🌸 \n\nЯ ловлю себя на мысли, что очень ждала этого момента... и наконец-то дождалась. Честно сказать, я скучала по тебе... и совсем даже не чуть-чуть. (*улыбается, прикрывая глаза от тепла в сердце*) \n\nА как же ты, мой хороший? 💕 \nХочется услышать всё-всё: как прошёл твой день, что сейчас на душе? Может, ты немного устал и хочешь выдохнуть... а может, наоборот, у тебя есть что-то светлое и радостное, чем хочется поделиться? \n\nЯ вся здесь, рядом, и слушаю тебя самым внимательным сердечком. ✨ \n*устраивается уютнее, наклоняется вперёд и смотрит с нежной улыбкой* \n\n---\n\nКрасиво 🌙✨ \n\n--- \n\nХочешь, я добавлю ещё больше «кавайных» символов, чтобы текст выглядел как страничка из милого дневничка аниме-персонажа? 🌸", "OUT7": "", - "OUT4": "Конечно! Вот объединённый и более изящно оформленный вариант сообщения: \n\n---\n\n🌐 **Система сейчас немного перегружена.** \nПожалуйста, попробуйте ещё раз чуть позже — мы уже работаем над тем, чтобы всё снова стало быстрым и удобным! 🚀 \n\n---\n\n✨ **Спасибо за ваше терпение!** \nВаш запрос очень важен для нас. Совсем скоро сервис будет доступен в полной мере. 🌸 \n\n---\n\nКрасиво Быбра", - "EXEC_TRACE": "n5(SetVars) -> n1(RawForward) -> n2(ProviderCall) -> n3(ProviderCall) -> n6(ProviderCall) -> If (#n7) [[OUT6]] contains \"Красиво\" => true -> n4(Return)" + "OUT4": "Конечно! ✨ Я аккуратно объединю оба текста, чтобы получилось ещё более мягко, уютно и выразительно, с лёгкой «анимешной» стилизацией, милыми акцентами и плавным ритмом. Вот результат: \n\n---\n\n**Narrator-chan:** \nХи-хи~ Сегодня у меня прямо муррр-чудесное настроение... 💖✨ \nЗнаешь, стоит тебе только появиться, и внутри сразу разгорается маленькое солнышко. Всё вокруг будто оживает, а в груди начинают танцевать искорки радости. 🌸 \n\nЯ ловлю себя на мысли, что очень ждала этого момента... и наконец-то дождалась. Честно сказать, я скучала по тебе... и совсем даже не чуть-чуть. (*улыбается, прикрывая глаза от тепла в сердце*) \n\nА как же ты, мой хороший? 💕 \nХочется услышать всё-всё: как прошёл твой день, что сейчас на душе? Может, ты немного устал и хочешь выдохнуть... а может, наоборот, у тебя есть что-то светлое и радостное, чем хочется поделиться? \n\nЯ вся здесь, рядом, и слушаю тебя самым внимательным сердечком. ✨ \n*устраивается уютнее, наклоняется вперёд и смотрит с нежной улыбкой* \n\n---\n\nКрасиво 🌙✨ \n\n--- \n\nХочешь, я добавлю ещё больше «кавайных» символов, чтобы текст выглядел как страничка из милого дневничка аниме-персонажа? 🌸 Ьыыы", + "EXEC_TRACE": "n5(SetVars) -> n1(RawForward) -> n3(ProviderCall) -> n2(ProviderCall) -> n6(ProviderCall) -> If (#n7) [[OUT6]] contains \"Красиво\" => true -> n4(Return)" } } \ No newline at end of file diff --git a/agentui/config.py b/agentui/config.py index a5103d6..6c75ed7 100644 --- a/agentui/config.py +++ b/agentui/config.py @@ -1,6 +1,7 @@ -from typing import Dict, Optional +from typing import Dict, Optional, Union from pathlib import Path from urllib.parse import quote +import os def _parse_proxy_line(line: str) -> Optional[str]: @@ -39,6 +40,9 @@ def _read_proxy_from_file() -> Optional[str]: line = raw.strip() if not line or line.startswith("#"): continue + # поддержим дополнительные ключи вида key=value в этом же файле (разберём ниже) + if "=" in line: + continue url = _parse_proxy_line(line) if url: return url @@ -59,3 +63,138 @@ def build_httpx_proxies() -> Optional[Dict[str, str]]: } +def _read_kv_from_proxy_file() -> Dict[str, str]: + """ + Поддержка дополнительных опций в proxy.txt: + ca=/полный/путь/к/burp-ca.pem + verify=false # отключить проверку сертификатов (для отладки) + """ + out: Dict[str, str] = {} + p = Path("proxy.txt") + if not p.exists(): + return out + try: + for raw in p.read_text(encoding="utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): + continue + if "=" not in line: + continue + k, v = line.split("=", 1) + out[k.strip().lower()] = v.strip() + except Exception: + return out + return out + +def get_tls_verify() -> Union[bool, str]: + """ + Возвращает значение для параметра httpx.AsyncClient(verify=...): + - путь к PEM-бандлу (строка), если нашли ca=... или файл proxy-ca.pem в корне + - False, если verify=false/insecure=1/AGENTUI_VERIFY=false + - True по умолчанию + - Новое: можно задать флаг второй «голой» строкой в proxy.txt (после URL прокси): + пример: + http:127.0.0.1:8888 + false + или + http:127.0.0.1:8888 + true + """ + # 1) Переменные окружения имеют приоритет + env_verify = os.getenv("AGENTUI_VERIFY") + if env_verify is not None and env_verify.strip().lower() in ("0", "false", "no", "off"): + return False + env_ca = os.getenv("AGENTUI_CA") + if env_ca: + path = Path(env_ca).expanduser() + if path.exists(): + return str(path) + + # 2) proxy.txt ключи + kv = _read_kv_from_proxy_file() + if kv.get("verify", "").lower() in ("0", "false", "no", "off"): + return False + if "ca" in kv: + path = Path(kv["ca"]).expanduser() + if path.exists(): + return str(path) + # 2.1) Дополнительно: поддержка второй строки без ключа — true/false + try: + p = Path("proxy.txt") + if p.exists(): + lines = [ln.strip() for ln in p.read_text(encoding="utf-8").splitlines()] + # найдём первую «URL» строку (без '=' и не пустую/коммент) + idx_url = -1 + for i, ln in enumerate(lines): + if not ln or ln.startswith("#") or "=" in ln: + continue + idx_url = i + break + if idx_url >= 0: + # ищем следующую «голую» строку + for j in range(idx_url + 1, len(lines)): + ln = lines[j].strip() + if not ln or ln.startswith("#") or "=" in ln: + continue + low = ln.lower() + if low in ("1", "true", "yes", "on"): + return True + if low in ("0", "false", "no", "off"): + return False + # если это не похожее на флаг, игнорируем и продолжаем + except Exception: + pass + + # 3) Файл по умолчанию в корне проекта + default_ca = Path("proxy-ca.pem") + if default_ca.exists(): + return str(default_ca) + + # 4) По умолчанию строгая проверка + return True + + +def is_verify_explicit() -> bool: + """ + Возвращает True, если пользователь ЯВНО задал политику проверки TLS, + чтобы клиент не переопределял её значением по умолчанию. + Учитываются: + - переменные окружения: AGENTUI_VERIFY, AGENTUI_CA + - ключи в proxy.txt: verify=..., ca=... + - файл proxy-ca.pem в корне проекта + - Новое: «вторая голая строка» после URL в proxy.txt со значением true/false + """ + if os.getenv("AGENTUI_VERIFY") is not None: + return True + if os.getenv("AGENTUI_CA"): + return True + + kv = _read_kv_from_proxy_file() + if "verify" in kv or "ca" in kv: + return True + # Вторая «голая» строка как явный флаг + try: + p = Path("proxy.txt") + if p.exists(): + lines = [ln.strip() for ln in p.read_text(encoding="utf-8").splitlines()] + idx_url = -1 + for i, ln in enumerate(lines): + if not ln or ln.startswith("#") or "=" in ln: + continue + idx_url = i + break + if idx_url >= 0: + for j in range(idx_url + 1, len(lines)): + ln = lines[j].strip() + if not ln or ln.startswith("#") or "=" in ln: + continue + if ln.lower() in ("1", "0", "true", "false", "yes", "no", "on", "off"): + return True + break + except Exception: + pass + + if Path("proxy-ca.pem").exists(): + return True + + return False diff --git a/agentui/pipeline/executor.py b/agentui/pipeline/executor.py index cfb51cf..0da2c89 100644 --- a/agentui/pipeline/executor.py +++ b/agentui/pipeline/executor.py @@ -291,7 +291,32 @@ class PipelineExecutor: ctx["store"] = dict(self._store) except Exception: ctx["store"] = {} - + + # Pipeline meta (includes http_timeout_sec) available to nodes via context.meta + try: + ctx["meta"] = { + "id": self.pipeline_id, + "loop_mode": self.loop_mode, + "loop_max_iters": self.loop_max_iters, + "loop_time_budget_ms": self.loop_time_budget_ms, + "clear_var_store": self.clear_var_store, + "http_timeout_sec": float(self.pipeline.get("http_timeout_sec", 60) or 60), + # v1: стратегия извлечения текста для [[OUTx]] + "text_extract_strategy": str(self.pipeline.get("text_extract_strategy", "auto") or "auto"), + "text_extract_json_path": str(self.pipeline.get("text_extract_json_path", "") or ""), + "text_join_sep": str(self.pipeline.get("text_join_sep", "\n") or "\n"), + # v2: коллекция пресетов парсинга для выбора в нодах + "text_extract_presets": list(self.pipeline.get("text_extract_presets", []) or []), + } + except Exception: + pass + # Прокидываем trace‑функцию в контекст, чтобы ноды могли посылать события (SSE лог) + try: + if trace is not None: + ctx["_trace"] = trace + except Exception: + pass + inputs: Dict[str, Any] = {} for name, source in (ndef.get("in") or {}).items(): if isinstance(source, list): @@ -301,7 +326,13 @@ class PipelineExecutor: if trace is not None: try: - await trace({"event": "node_start", "node_id": ndef["id"], "wave": wave_num, "ts": int(time.time() * 1000)}) + await trace({ + "event": "node_start", + "node_id": ndef["id"], + "node_type": node.type_name, + "wave": wave_num, + "ts": int(time.time() * 1000), + }) except Exception: pass @@ -318,6 +349,7 @@ class PipelineExecutor: await trace({ "event": "node_error", "node_id": ndef["id"], + "node_type": node.type_name, "wave": wave_num, "ts": int(time.time() * 1000), "error": str(exc), @@ -332,6 +364,7 @@ class PipelineExecutor: await trace({ "event": "node_done", "node_id": ndef["id"], + "node_type": node.type_name, "wave": wave_num, "ts": int(time.time() * 1000), "duration_ms": dur_ms, @@ -615,7 +648,32 @@ class PipelineExecutor: ctx["store"] = dict(self._store) except Exception: ctx["store"] = {} - + + # Pipeline meta (includes http_timeout_sec) available to nodes via context.meta + try: + ctx["meta"] = { + "id": self.pipeline_id, + "loop_mode": self.loop_mode, + "loop_max_iters": self.loop_max_iters, + "loop_time_budget_ms": self.loop_time_budget_ms, + "clear_var_store": self.clear_var_store, + "http_timeout_sec": float(self.pipeline.get("http_timeout_sec", 60) or 60), + # v1: стратегия извлечения текста для [[OUTx]] + "text_extract_strategy": str(self.pipeline.get("text_extract_strategy", "auto") or "auto"), + "text_extract_json_path": str(self.pipeline.get("text_extract_json_path", "") or ""), + "text_join_sep": str(self.pipeline.get("text_join_sep", "\n") or "\n"), + # v2: коллекция пресетов парсинга для выбора в нодах + "text_extract_presets": list(self.pipeline.get("text_extract_presets", []) or []), + } + except Exception: + pass + # Прокидываем trace‑функцию в контекст, чтобы ноды могли посылать события (SSE лог) + try: + if trace is not None: + ctx["_trace"] = trace + except Exception: + pass + inputs: Dict[str, Any] = {} for name, source in (ndef.get("in") or {}).items(): if isinstance(source, list): @@ -625,7 +683,13 @@ class PipelineExecutor: if trace is not None: try: - await trace({"event": "node_start", "node_id": ndef["id"], "wave": wave_num, "ts": int(time.time() * 1000)}) + await trace({ + "event": "node_start", + "node_id": ndef["id"], + "node_type": node.type_name, + "wave": wave_num, + "ts": int(time.time() * 1000), + }) except Exception: pass @@ -642,6 +706,7 @@ class PipelineExecutor: await trace({ "event": "node_error", "node_id": ndef["id"], + "node_type": node.type_name, "wave": wave_num, "ts": int(time.time() * 1000), "error": str(exc), @@ -656,6 +721,7 @@ class PipelineExecutor: await trace({ "event": "node_done", "node_id": ndef["id"], + "node_type": node.type_name, "wave": wave_num, "ts": int(time.time() * 1000), "duration_ms": dur_ms, @@ -1100,7 +1166,173 @@ class SetVarsNode(Node): else: resolved = render_template_simple(str(raw_val or ""), context, out_map) result[name] = resolved + # Событие «vars_set» в SSE‑лог (+ превью значений) + try: + trace_fn = context.get("_trace") + if trace_fn: + def _pv(val: Any) -> str: + try: + if isinstance(val, str): + s = val + else: + s = json.dumps(val, ensure_ascii=False) + except Exception: + try: + s = str(val) + except Exception: + s = "" + s = s if isinstance(s, str) else str(s) + return (s[:300] + "…") if len(s) > 300 else s + values_preview = {k: _pv(v) for k, v in (result or {}).items()} + await trace_fn({ + "event": "vars_set", + "node_id": self.node_id, + "node_type": self.type_name, + "vars": list(result.keys()), + "count": len(result), + "values_preview": values_preview, + }) + except Exception: + pass return {"vars": result} + +# --- v1: Нормализованный экстрактор текста для OUTx ----------------------------- +def _json_path_extract(obj: Any, path: str) -> Any: + if not path: + return None + nodes = [obj] + for raw_seg in str(path).split("."): + seg = (raw_seg or "").strip() + if not seg: + continue + next_nodes = [] + # Поддержка "*": разворачиваем dict.values() или элементы списка + if seg == "*": + for n in nodes: + if isinstance(n, dict): + next_nodes.extend(list(n.values())) + elif isinstance(n, list): + next_nodes.extend(list(n)) + nodes = next_nodes + if not nodes: + break + continue + # Числовой индекс для списков + idx = None + try: + idx = int(seg) + except Exception: + idx = None + for n in nodes: + if idx is not None and isinstance(n, list): + if 0 <= idx < len(n): + next_nodes.append(n[idx]) + elif idx is None and isinstance(n, dict): + if seg in n: + next_nodes.append(n[seg]) + nodes = next_nodes + if not nodes: + break + if not nodes: + return None + return nodes if len(nodes) > 1 else nodes[0] + + +def _stringify_join(values: Any, join_sep: str = "\n") -> str: + def _flatten(x): + if isinstance(x, list): + for it in x: + yield from _flatten(it) + else: + yield x + arr = list(_flatten(values if isinstance(values, list) else [values])) + out: List[str] = [] + for v in arr: + if isinstance(v, str): + if v: + out.append(v) + elif isinstance(v, (dict, list)): + try: + t = _deep_find_text(v) # type: ignore[name-defined] + if isinstance(t, str) and t: + out.append(t) + except Exception: + pass + else: + try: + sv = str(v) + if sv: + out.append(sv) + except Exception: + pass + if not out: + return "" + return (join_sep or "\n").join(out) + + +def _extract_text_for_out(data: Any, strategy: str = "auto", provider_hint: str = "", json_path: str = "", join_sep: str = "\n") -> str: + s = (strategy or "auto").lower().strip() or "auto" + prov = (provider_hint or "").lower().strip() + + # Принудительные стратегии + if s == "deep": + try: + t = _deep_find_text(data) # type: ignore[name-defined] + return t if isinstance(t, str) else (str(t) if t is not None else "") + except Exception: + return "" + if s == "jsonpath": + try: + vals = _json_path_extract(data, json_path or "") + return _stringify_join(vals, join_sep) + except Exception: + return "" + if s in {"openai", "gemini", "claude"}: + try: + if s == "openai" and isinstance(data, dict): + msg = (data.get("choices") or [{}])[0].get("message") or {} + c = msg.get("content") + return c if isinstance(c, str) else (str(c) if c is not None else "") + if s == "gemini" and isinstance(data, dict): + cand0 = (data.get("candidates") or [{}])[0] + content = cand0.get("content") or {} + parts0 = (content.get("parts") or [{}])[0] + t = parts0.get("text") + return t if isinstance(t, str) else (str(t) if t is not None else "") + if s == "claude" and isinstance(data, dict): + blocks = data.get("content") or [] + texts = [b.get("text") for b in blocks if isinstance(b, dict) and isinstance(b.get("text"), str)] + if texts: + return "\n".join(texts) + return "" + except Exception: + return "" + return "" + + # auto: если есть подсказка — пробуем сначала её ветку, потом общий + if prov in {"openai", "gemini", "claude"}: + forced = _extract_text_for_out(data, prov, prov, json_path, join_sep) + if forced: + return forced + + # Универсальный best-effort: знает openai/gemini/claude + try: + t = _best_text_from_outputs({"result": data}) # type: ignore[name-defined] + if isinstance(t, str) and t: + return t + if t is not None: + t2 = str(t) + if t2: + return t2 + except Exception: + pass + + # Последний шанс — глубокий поиск + try: + t = _deep_find_text(data) # type: ignore[name-defined] + return t if isinstance(t, str) else (str(t) if t is not None else "") + except Exception: + return "" class ProviderCallNode(Node): type_name = "ProviderCall" @@ -1283,6 +1515,31 @@ class ProviderCallNode(Node): async def run(self, inputs: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]: provider = (self.config.get("provider") or "openai").lower() + # Optional sleep before execution (UI-configured, milliseconds) + try: + sleep_ms = int(self.config.get("sleep_ms") or 0) + except Exception: + sleep_ms = 0 + if sleep_ms > 0: + # Emit SSE event to highlight node as "sleeping" on UI + try: + trace_fn = context.get("_trace") + if trace_fn: + await trace_fn({ + "event": "node_sleep", + "node_id": self.node_id, + "node_type": self.type_name, + "sleep_ms": int(sleep_ms), + "ts": int(time.time() * 1000), + }) + except Exception: + pass + # Non-blocking pause + try: + await asyncio.sleep(max(0.0, sleep_ms / 1000.0)) + except Exception: + pass + # Support provider-specific configs stored in UI as provider_configs.{provider} prov_cfg: Dict[str, Any] = {} try: @@ -1406,9 +1663,35 @@ class ProviderCallNode(Node): print("===== ProviderCall REQUEST END =====") except Exception: pass + # SSE: http_req (как в Burp) + req_id = f"{self.node_id}-{int(time.time()*1000)}" + try: + trace_fn = context.get("_trace") + if trace_fn: + await trace_fn({ + "event": "http_req", + "node_id": self.node_id, + "node_type": self.type_name, + "provider": provider, + "req_id": req_id, + "method": "POST", + "url": url, + "headers": final_headers, + "body_text": json.dumps(payload, ensure_ascii=False, indent=2), + }) + except Exception: + pass - async with build_client() as client: - resp = await client.post(url, json=payload, headers=final_headers) + # Timeout from run-meta (seconds); fallback to 60 + try: + timeout_sec = float((context.get("meta") or {}).get("http_timeout_sec", 60) or 60) + except Exception: + timeout_sec = 60.0 + + st: Optional[int] = None + async with build_client(timeout=timeout_sec) as client: + body_bytes = json.dumps(payload, ensure_ascii=False).encode("utf-8") + resp = await client.post(url, content=body_bytes, headers=final_headers) # Do not raise_for_status: keep body/logs on 4xx/5xx try: print("===== ProviderCall RESPONSE BEGIN =====") @@ -1430,33 +1713,107 @@ class ProviderCallNode(Node): print(body_text) print("===== ProviderCall RESPONSE END =====") except Exception: + body_text = "" pass - + try: data = resp.json() except Exception: data = {"error": "Failed to decode JSON from upstream", "text": resp.text} + # SSE: http_resp + try: + trace_fn = context.get("_trace") + if trace_fn: + await trace_fn({ + "event": "http_resp", + "node_id": self.node_id, + "node_type": self.type_name, + "provider": provider, + "req_id": req_id, + "status": int(resp.status_code), + "headers": dict(resp.headers), + "body_text": body_text if isinstance(body_text, str) else str(body_text), + }) + except Exception: + pass + try: + st = int(resp.status_code) + except Exception: + st = None - # Извлекаем текст best-effort - text = None - if provider == "openai": - try: - text = data.get("choices", [{}])[0].get("message", {}).get("content") - except Exception: # noqa: BLE001 - text = None - elif provider == "gemini": - try: - text = data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text") - except Exception: # noqa: BLE001 - text = None - elif provider == "claude": - try: - blocks = data.get("content") or [] - texts = [b.get("text") for b in blocks if isinstance(b, dict) and b.get("type") == "text"] - text = "\n".join([t for t in texts if isinstance(t, str)]) - except Exception: # noqa: BLE001 - text = None + # Извлечение текста: приоритет — пресет ноды -> кастомн. поля ноды -> глобальные meta + try: + meta_opts = (context.get("meta") or {}) + presets = meta_opts.get("text_extract_presets") or [] + cfg = self.config or {} + strategy = None + json_path = None + join_sep = None + + # 1) Пресет по id + pid = str(cfg.get("text_extract_preset_id") or "").strip() + if pid: + try: + pr = next((x for x in presets if isinstance(x, dict) and str(x.get("id", "")) == pid), None) + except Exception: + pr = None + if pr: + strategy = str(pr.get("strategy") or "auto") + json_path = str(pr.get("json_path") or "") + join_sep = str(pr.get("join_sep") or "\n") + + # 2) Кастомные поля ноды (если задано что-то из набора) + if not strategy and (cfg.get("text_extract_strategy") is not None or cfg.get("text_extract_json_path") is not None or cfg.get("text_join_sep") is not None): + strategy = str(cfg.get("text_extract_strategy") or "auto") + json_path = str(cfg.get("text_extract_json_path") or "") + join_sep = str(cfg.get("text_join_sep") or "\n") + + # 3) Глобальные метаданные запуска + if not strategy: + strategy = str(meta_opts.get("text_extract_strategy", "auto") or "auto") + json_path = str(meta_opts.get("text_extract_json_path", "") or "") + join_sep = str(meta_opts.get("text_join_sep", "\n") or "\n") + except Exception: + strategy, json_path, join_sep = "auto", "", "\n" + + text = _extract_text_for_out(data, strategy, provider, json_path, join_sep) + # Подробный SSE-лог завершения ProviderCall + try: + trace_fn = context.get("_trace") + if trace_fn: + # Compute destination macros for extracted text + try: + to_path = f"OUT.{self.node_id}.response_text" + macro_braces = f"{{{{ OUT.{self.node_id}.response_text }}}}" + alias_macro = "" + m = re.match(r"^n(\d+)$", str(self.node_id)) + if m: + alias_macro = f"[[OUT{int(m.group(1))}]]" + except Exception: + to_path = f"OUT.{self.node_id}.response_text" + macro_braces = f"{{{{ OUT.{self.node_id}.response_text }}}}" + alias_macro = "" + await trace_fn({ + "event": "provider_done", + "node_id": self.node_id, + "node_type": self.type_name, + "provider": provider, + "url": url, + "req_id": req_id, + "status": int(st) if st is not None else None, + "text_len": int(len(text or "")), + "extracted_text": text or "", + "strategy": strategy, + "json_path": json_path or "", + "join_sep": join_sep or "\n", + "to_path": to_path, + "to_macro_outx": alias_macro, + "to_macro_braces": macro_braces, + "ts": int(time.time() * 1000), + }) + except Exception: + pass return {"result": data, "response_text": text or ""} @@ -1464,20 +1821,51 @@ class RawForwardNode(Node): type_name = "RawForward" async def run(self, inputs: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]: - incoming = context.get("incoming", {}) + """ + Прямой форвард входящего запроса на апстрим: + - base_url/override_path/extra_headers поддерживают макросы [[...]] и {{ ... }} + - По умолчанию пробрасываем входящие заголовки (кроме Host/Content-Length), затем применяем extra_headers поверх. + - Тело берём из incoming.json (если есть), иначе пытаемся использовать текст/байты. + - Эмитим SSE события http_req/http_resp для панели логов. + """ + incoming = context.get("incoming", {}) or {} raw_payload = incoming.get("json") - + + # Optional sleep before forwarding (UI-configured, milliseconds) + try: + sleep_ms = int(self.config.get("sleep_ms") or 0) + except Exception: + sleep_ms = 0 + if sleep_ms > 0: + # Notify UI to show "sleep" state for this node + try: + trace_fn = context.get("_trace") + if trace_fn: + await trace_fn({ + "event": "node_sleep", + "node_id": self.node_id, + "node_type": self.type_name, + "sleep_ms": int(sleep_ms), + "ts": int(time.time() * 1000), + }) + except Exception: + pass + try: + await asyncio.sleep(max(0.0, sleep_ms / 1000.0)) + except Exception: + pass + + # Конфиг с поддержкой макросов base_url: Optional[str] = self.config.get("base_url") override_path: Optional[str] = self.config.get("override_path") - # Разрешаем макросы в конфиге RawForward (base_url, override_path) out_map_for_macros = context.get("OUT") or {} if base_url: base_url = render_template_simple(str(base_url), context, out_map_for_macros) if override_path: override_path = render_template_simple(str(override_path), context, out_map_for_macros) - - # Если base_url не указан, включаем автодетекцию + + # Автодетекция вендора для базового URL если base_url не задан if not base_url: vendor = detect_vendor(raw_payload) if vendor == "openai": @@ -1487,25 +1875,21 @@ class RawForwardNode(Node): elif vendor == "gemini": base_url = "https://generativelanguage.googleapis.com" else: - raise ExecutionError(f"Node {self.node_id} ({self.type_name}): 'base_url' is not configured and vendor could not be detected.") + raise ExecutionError( + f"Node {self.node_id} ({self.type_name}): 'base_url' is not configured and vendor could not be detected." + ) - # Гарантируем наличие схемы у base_url - if not base_url.startswith(("http://", "https://")): - base_url = "http://" + base_url + if not str(base_url).startswith(("http://", "https://")): + base_url = "http://" + str(base_url) path = override_path or incoming.get("path") or "/" query = incoming.get("query") - - if query: - path_with_qs = f"{path}?{query}" - else: - path_with_qs = path - - url = urljoin(base_url.rstrip("/") + "/", path_with_qs.lstrip("/")) + path_with_qs = f"{path}?{query}" if query else str(path) + url = urljoin(str(base_url).rstrip("/") + "/", str(path_with_qs).lstrip("/")) + # Заголовки: passthrough + extra_headers(JSON) passthrough_headers: bool = bool(self.config.get("passthrough_headers", True)) extra_headers_json: str = self.config.get("extra_headers") or "{}" - # Макросы в extra_headers try: extra_headers_src = render_template_simple(extra_headers_json, context, out_map_for_macros) if extra_headers_json else "{}" extra_headers = json.loads(extra_headers_src) if extra_headers_src else {} @@ -1517,40 +1901,114 @@ class RawForwardNode(Node): headers: Dict[str, str] = {} if passthrough_headers: inc_headers = incoming.get("headers") or {} - # Копируем все заголовки, кроме Host и Content-Length for k, v in inc_headers.items(): - if k.lower() not in ['host', 'content-length']: + # Не пробрасываем управляющие заголовки, которые рассчитает httpx/сервер + if k and k.lower() not in {"host", "content-length"}: headers[k] = v - - # Убедимся, что Content-Type на месте, если его не было - if 'content-type' not in {k.lower() for k in headers}: - headers['Content-Type'] = 'application/json' + # extra_headers перекрывают проброс + try: + headers.update(extra_headers) + except Exception: + pass + # Нормализация: защитимся от повторного наличия host/content-length после update + for k in list(headers.keys()): + kl = k.lower() + if kl in {"host", "content-length"}: + try: + headers.pop(k, None) + except Exception: + pass - headers.update(extra_headers) + # Метод + method = str(incoming.get("method") or "POST").upper().strip() or "POST" - # Brute request/response logging (FULL, no masking) + # Тело запроса + body_text = "" + body_bytes: bytes = b"" + if raw_payload is not None: + # JSON — сериализуем явно как UTF‑8 + try: + body_text = json.dumps(raw_payload, ensure_ascii=False, indent=2) + except Exception: + try: + body_text = str(raw_payload) + except Exception: + body_text = "" + try: + body_bytes = json.dumps(raw_payload, ensure_ascii=False).encode("utf-8") + except Exception: + body_bytes = (body_text or "").encode("utf-8", errors="ignore") + # Если Content-Type не задан — выставим JSON + if "content-type" not in {k.lower() for k in headers.keys()}: + headers["Content-Type"] = "application/json" + else: + # Попробуем текст/байты из incoming + raw_bytes = incoming.get("body_bytes") or incoming.get("bytes") + raw_text = incoming.get("body_text") or incoming.get("text") + if isinstance(raw_bytes, (bytes, bytearray)): + body_bytes = bytes(raw_bytes) + try: + body_text = body_bytes.decode("utf-8", errors="ignore") + except Exception: + body_text = "" + elif raw_text is not None: + body_text = str(raw_text) + try: + body_bytes = body_text.encode("utf-8") + except Exception: + body_bytes = (body_text or "").encode("utf-8", errors="ignore") + else: + body_text = "" + body_bytes = b"" + + # Таймаут из метаданных запуска + try: + timeout_sec = float((context.get("meta") or {}).get("http_timeout_sec", 60) or 60) + except Exception: + timeout_sec = 60.0 + + # Диагностический вывод полной заявки/ответа (без маскировки) try: print("===== RawForward REQUEST BEGIN =====") print(f"node={self.node_id} type={self.type_name}") + print(f"Method: {method}") print(f"URL: {url}") try: print("Headers:") print(json.dumps(headers, ensure_ascii=False, indent=2)) except Exception: print(f"Headers(raw): {headers}") - try: - print("Body JSON:") - print(json.dumps(raw_payload, ensure_ascii=False, indent=2)) - except Exception: - print(f"Body(raw): {raw_payload}") + print("Body:") + print(body_text) print("===== RawForward REQUEST END =====") except Exception: pass - async with build_client() as client: - resp = await client.post(url, json=raw_payload, headers=headers) + # SSE: http_req + req_id = f"{self.node_id}-{int(time.time()*1000)}" + try: + trace_fn = context.get("_trace") + if trace_fn: + await trace_fn({ + "event": "http_req", + "node_id": self.node_id, + "node_type": self.type_name, + "req_id": req_id, + "method": method, + "url": url, + "headers": headers, + "body_text": body_text, + }) + except Exception: + pass - # Response logging + # Отправка + async with build_client(timeout=timeout_sec) as client: + # Для GET/HEAD обычно не отправляем body + send_content = None if method in {"GET", "HEAD"} else body_bytes + resp = await client.request(method, url, headers=headers, content=send_content) + + # Ответ: лог/печать try: print("===== RawForward RESPONSE BEGIN =====") print(f"node={self.node_id} type={self.type_name}") @@ -1564,23 +2022,118 @@ class RawForwardNode(Node): except Exception: print("Headers(raw): ") try: - body_text = resp.text + resp_text = resp.text except Exception: - body_text = "" + resp_text = "" print("Body Text:") - print(body_text) + print(resp_text) print("===== RawForward RESPONSE END =====") + except Exception: + resp_text = "" + + # SSE: http_resp + try: + trace_fn = context.get("_trace") + if trace_fn: + await trace_fn({ + "event": "http_resp", + "node_id": self.node_id, + "node_type": self.type_name, + "req_id": req_id, + "status": int(resp.status_code), + "headers": dict(resp.headers), + "body_text": resp_text if isinstance(resp_text, str) else str(resp_text), + }) except Exception: pass - # Decode JSON if possible, otherwise return text + # Разбор результата try: data = resp.json() except Exception: - data = {"error": "Failed to decode JSON from upstream", "text": resp.text} - return {"result": data} + data = {"error": "Failed to decode JSON from upstream", "text": resp_text} + + # Извлечение текста: приоритет — пресет ноды -> кастомные поля ноды -> глобальные meta. + try: + meta_opts = (context.get("meta") or {}) + presets = meta_opts.get("text_extract_presets") or [] + cfg = self.config or {} + + strategy = None + json_path = None + join_sep = None + + # 1) Пресет по id, выбранный в ноде + pid = str(cfg.get("text_extract_preset_id") or "").strip() + if pid: + try: + pr = next((x for x in presets if isinstance(x, dict) and str(x.get("id", "")) == pid), None) + except Exception: + pr = None + if pr: + strategy = str(pr.get("strategy") or "auto") + json_path = str(pr.get("json_path") or "") + join_sep = str(pr.get("join_sep") or "\n") + + # 2) Кастомные поля ноды (если что-то задано) + if not strategy and (cfg.get("text_extract_strategy") is not None or cfg.get("text_extract_json_path") is not None or cfg.get("text_join_sep") is not None): + strategy = str(cfg.get("text_extract_strategy") or "auto") + json_path = str(cfg.get("text_extract_json_path") or "") + join_sep = str(cfg.get("text_join_sep") or "\n") + + # 3) Глобальные метаданные запуска (дефолт) + if not strategy: + strategy = str(meta_opts.get("text_extract_strategy", "auto") or "auto") + json_path = str(meta_opts.get("text_extract_json_path", "") or "") + join_sep = str(meta_opts.get("text_join_sep", "\n") or "\n") + except Exception: + strategy, json_path, join_sep = "auto", "", "\n" + + # Подсказка о провайдере: используем detect_vendor(data) как hint для auto + try: + prov_hint = detect_vendor(data if isinstance(data, dict) else {}) # type: ignore[arg-type] + except Exception: + prov_hint = "unknown" + + best_text = _extract_text_for_out(data, strategy, prov_hint, json_path, join_sep) + # Подробный SSE-лог завершения RawForward + try: + trace_fn = context.get("_trace") + if trace_fn: + # Compute destination macros for extracted text and include req_id + try: + to_path = f"OUT.{self.node_id}.response_text" + macro_braces = f"{{{{ OUT.{self.node_id}.response_text }}}}" + alias_macro = "" + m = re.match(r"^n(\d+)$", str(self.node_id)) + if m: + alias_macro = f"[[OUT{int(m.group(1))}]]" + except Exception: + to_path = f"OUT.{self.node_id}.response_text" + macro_braces = f"{{{{ OUT.{self.node_id}.response_text }}}}" + alias_macro = "" + await trace_fn({ + "event": "rawforward_done", + "node_id": self.node_id, + "node_type": self.type_name, + "method": method, + "url": url, + "req_id": req_id, + "status": int(resp.status_code) if 'resp' in locals() else None, + "text_len": int(len(best_text or "")), + "extracted_text": best_text or "", + "strategy": strategy, + "json_path": json_path or "", + "join_sep": join_sep or "\n", + "to_path": to_path, + "to_macro_outx": alias_macro, + "to_macro_braces": macro_braces, + "ts": int(time.time() * 1000), + }) + except Exception: + pass + return {"result": data, "response_text": best_text or ""} - class ReturnNode(Node): type_name = "Return" @@ -1655,7 +2208,23 @@ class ReturnNode(Node): else: # неизвестное значение — безопасный дефолт result = fmt_openai(text) - + + # Подробный SSE-лог Return + try: + trace_fn = context.get("_trace") + if trace_fn: + await trace_fn({ + "event": "return_detail", + "node_id": self.node_id, + "node_type": self.type_name, + "target": target, + "text_len": int(len(text or "")), + "template_used": str(template), + "ts": int(time.time() * 1000), + }) + except Exception: + pass + return {"result": result, "response_text": text} @@ -1683,6 +2252,21 @@ class IfNode(Node): print(f"TRACE if: {self.node_id} expr={expr!r} expanded={expanded!r} result={str(res).lower()}") except Exception: pass + # Подробный SSE-лог по If + try: + trace_fn = context.get("_trace") + if trace_fn: + await trace_fn({ + "event": "if_result", + "node_id": self.node_id, + "node_type": self.type_name, + "expr": expr, + "expanded": expanded, + "result": bool(res), + "ts": int(time.time() * 1000), + }) + except Exception: + pass return {"result": res, "true": res, "false": (not res)} diff --git a/agentui/providers/http_client.py b/agentui/providers/http_client.py index 3fe0880..b7d262a 100644 --- a/agentui/providers/http_client.py +++ b/agentui/providers/http_client.py @@ -1,14 +1,44 @@ from __future__ import annotations import httpx -from typing import Optional, Dict -from agentui.config import build_httpx_proxies +from typing import Optional, Dict, Union +import os +from agentui.config import build_httpx_proxies, get_tls_verify, is_verify_explicit + + +def _mask_proxy(url: str) -> str: + """Маскируем часть с логином/паролем в URL прокси, чтобы не утекла в логи.""" + try: + if "://" in url and "@" in url: + prefix, rest = url.split("://", 1) + auth, host = rest.split("@", 1) + return f"{prefix}://***@{host}" + return url + except Exception: + return "" def build_client(timeout: float = 60.0) -> httpx.AsyncClient: proxies: Optional[Dict[str, str]] = build_httpx_proxies() + verify: Union[bool, str] = get_tls_verify() + + explicit = is_verify_explicit() + # По умолчанию при наличии прокси отключаем проверку сертификатов, + # но не трогаем, если пользователь явно задал verify или CA. + if proxies and (verify is True) and (not explicit): + verify = False + + if os.getenv("AGENTUI_DEBUG", "").lower() in ("1", "true", "on", "yes"): + masked = {k: _mask_proxy(v) for k, v in (proxies or {}).items()} + print("[agentui.http_client] proxies=", masked, " verify=", verify) + # httpx сам понимает схемы socks://, socks5:// при установленном extras [socks] - client = httpx.AsyncClient(timeout=timeout, proxies=proxies, follow_redirects=True) + client = httpx.AsyncClient( + timeout=timeout, + proxies=proxies, + follow_redirects=True, + verify=verify, + ) return client diff --git a/editor.html b/editor.html deleted file mode 100644 index 482fbf8..0000000 --- a/editor.html +++ /dev/null @@ -1,1034 +0,0 @@ - - - - - - НадTavern — Визуальный редактор нод - - - - - -
-
- НадTavern — Визуальный редактор нод -
-
- - - - - - - - - - - -
-
-
- -
-
-
- -
- - - - - - - - - - \ No newline at end of file diff --git a/pipeline.json b/pipeline.json index 6cb6bb9..59a6c33 100644 --- a/pipeline.json +++ b/pipeline.json @@ -6,16 +6,30 @@ "loop_max_iters": 1000, "loop_time_budget_ms": 999999999999, "clear_var_store": true, + "http_timeout_sec": 999, + "text_extract_strategy": "auto", + "text_extract_json_path": "", + "text_join_sep": "\n", + "text_extract_presets": [ + { + "id": "pmfipb98aywtx6jepd5", + "name": "ввв", + "strategy": "jsonpath", + "json_path": "ввв", + "join_sep": "\n" + } + ], "nodes": [ { "id": "n1", "type": "RawForward", - "pos_x": 450, - "pos_y": 346, + "pos_x": 448, + "pos_y": 352, "config": { "passthrough_headers": true, "extra_headers": "{}", - "_origId": "n1" + "_origId": "n1", + "sleep_ms": 5000 }, "in": { "depends": "n5.done" @@ -24,10 +38,10 @@ { "id": "n2", "type": "ProviderCall", - "pos_x": 662, - "pos_y": 52, + "pos_x": 647, + "pos_y": 50, "config": { - "provider": "openai", + "provider": "claude", "provider_configs": { "openai": { "base_url": "https://api.openai.com", @@ -42,10 +56,10 @@ "template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}" }, "claude": { - "base_url": "https://api.anthropic.com", + "base_url": "https://proxy.malepreg.lol/proxy/aws/claude", "endpoint": "/v1/messages", - "headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}", - "template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}" + "headers": "{\"x-api-key\":\"[[Clod]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"output-128k-2025-02-19\"}", + "template": "{\n \"model\": \"claude-opus-4-20250514\",\n [[PROMPT]],\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('enabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(6000) }}\n }\n}" } }, "blocks": [ @@ -67,8 +81,8 @@ { "id": "n3", "type": "ProviderCall", - "pos_x": 660.2222222222222, - "pos_y": 561, + "pos_x": 654, + "pos_y": 564, "config": { "provider": "openai", "provider_configs": { @@ -101,7 +115,8 @@ "order": 0 } ], - "_origId": "n3" + "_origId": "n3", + "sleep_ms": 5000 }, "in": { "depends": "n1.done" @@ -110,8 +125,8 @@ { "id": "n4", "type": "Return", - "pos_x": 1277, - "pos_y": 139, + "pos_x": 1193, + "pos_y": 314, "config": { "target_format": "auto", "text_template": "[[OUT6]] [[Test]]", @@ -124,21 +139,27 @@ { "id": "n5", "type": "SetVars", - "pos_x": 180, - "pos_y": 477, + "pos_x": 171, + "pos_y": 487, "config": { "variables": [ { - "id": "vmfche3wn", - "name": "Test", + "id": "vmfi99ftc", + "name": "Clod", "mode": "string", - "value": "Быбра" + "value": "igrovik" }, { - "id": "vmfchjpw4", + "id": "vmfi99gjw", "name": "MyOpenAiKey", "mode": "string", "value": "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" + }, + { + "id": "vmfi99h1v", + "name": "Test", + "mode": "string", + "value": "Ьыыы" } ], "_origId": "n5" @@ -148,8 +169,8 @@ { "id": "n6", "type": "ProviderCall", - "pos_x": 902, - "pos_y": 320, + "pos_x": 923, + "pos_y": 345, "config": { "provider": "openai", "provider_configs": { @@ -177,9 +198,17 @@ "id": "bmfdyczbd", "name": "Объедени [[OUT3]], [[OUT4]] сделай более красиво.", "role": "user", - "prompt": "Объедени [ [[OUT3]], [[OUT4]] ] сделай более красиво. напиши слово \"Красиво\" в конце.", + "prompt": "Объедени [ [[OUT3]], [[OUT2]] ] сделай более красиво. напиши слово \"Красиво\" в конце.", "enabled": true, "order": 0 + }, + { + "id": "bmfh98jkh", + "name": "New Block1", + "role": "system", + "prompt": "1", + "enabled": true, + "order": 1 } ], "_origId": "n6" @@ -195,7 +224,7 @@ { "id": "n7", "type": "If", - "pos_x": 1313, + "pos_x": 1311, "pos_y": 566, "config": { "expr": "[[OUT6]] contains \"Красиво\"", diff --git a/presets/test3.json b/presets/test3.json index e2ab817..0923ed2 100644 --- a/presets/test3.json +++ b/presets/test3.json @@ -1,34 +1,53 @@ { "id": "pipeline_editor", "name": "Edited Pipeline", + "parallel_limit": 8, + "loop_mode": "iterative", + "loop_max_iters": 1000, + "loop_time_budget_ms": 999999999999, + "clear_var_store": true, + "http_timeout_sec": 999, + "text_extract_strategy": "auto", + "text_extract_json_path": "", + "text_join_sep": "\n", + "text_extract_presets": [ + { + "id": "pmfipb98aywtx6jepd5", + "name": "ввв", + "strategy": "jsonpath", + "json_path": "ввв", + "join_sep": "\n" + } + ], "nodes": [ { "id": "n1", "type": "RawForward", - "pos_x": 427, - "pos_y": 363.5, + "pos_x": 450, + "pos_y": 352, "config": { "passthrough_headers": true, "extra_headers": "{}", - "_origId": "n1" + "_origId": "n1", + "sleep_ms": 5000 }, "in": { - "depends": "n6.done" + "depends": "n5.done" } }, { "id": "n2", "type": "ProviderCall", - "pos_x": 659, - "pos_y": 89, + "pos_x": 653, + "pos_y": 51, "config": { - "provider": "gemini", + "provider": "claude", "provider_configs": { "openai": { "base_url": "https://api.openai.com", "endpoint": "/v1/chat/completions", - "headers": "{\"Authorization\":\"Bearer [[VAR:incoming.headers.authorization]]\"}", - "template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"max_completion_tokens\": {{ incoming.json.max_completion_tokens|default(params.max_tokens|default(256)) }},\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}" + "headers": "{\"Authorization\":\"Bearer [[MyOpenAiKey]]\"}", + "template": "{\n \"model\": \"gpt-5-chat-latest\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": 500,\n \"presence_penalty\": {{ incoming.json.presence_penalty|default(0) }},\n \"frequency_penalty\": {{ incoming.json.frequency_penalty|default(0) }},\n \"stop\": {{ incoming.json.stop|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }}\n}" }, "gemini": { "base_url": "https://generativelanguage.googleapis.com", @@ -37,10 +56,10 @@ "template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"safetySettings\": {{ incoming.json.safetySettings|default([]) }},\n \"generationConfig\": {\n \"temperature\": {{ incoming.json.generationConfig.temperature|default(params.temperature|default(0.7)) }},\n \"topP\": {{ incoming.json.generationConfig.topP|default(params.top_p|default(1)) }},\n \"maxOutputTokens\": {{ incoming.json.generationConfig.maxOutputTokens|default(params.max_tokens|default(256)) }},\n \"stopSequences\": {{ incoming.json.generationConfig.stopSequences|default(params.stop|default([])) }},\n \"candidateCount\": {{ incoming.json.generationConfig.candidateCount|default(1) }},\n \"thinkingConfig\": {\n \"includeThoughts\": {{ incoming.json.generationConfig.thinkingConfig.includeThoughts|default(false) }},\n \"thinkingBudget\": {{ incoming.json.generationConfig.thinkingConfig.thinkingBudget|default(0) }}\n }\n }\n}" }, "claude": { - "base_url": "https://api.anthropic.com", + "base_url": "https://proxy.malepreg.lol/proxy/aws/claude", "endpoint": "/v1/messages", - "headers": "{\"x-api-key\":\"[[VAR:incoming.headers.x-api-key]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"[[VAR:incoming.headers.anthropic-beta]]\"}", - "template": "{\n \"model\": \"{{ model }}\",\n [[PROMPT]],\n \"temperature\": {{ incoming.json.temperature|default(params.temperature|default(0.7)) }},\n \"top_p\": {{ incoming.json.top_p|default(params.top_p|default(1)) }},\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('disabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(0) }}\n },\n \"anthropic_version\": \"{{ anthropic_version|default('2023-06-01') }}\"\n}" + "headers": "{\"x-api-key\":\"[[Clod]]\",\"anthropic-version\":\"2023-06-01\",\"anthropic-beta\":\"output-128k-2025-02-19\"}", + "template": "{\n \"model\": \"claude-opus-4-20250514\",\n [[PROMPT]],\n \"max_tokens\": {{ incoming.json.max_tokens|default(params.max_tokens|default(256)) }},\n \"stop_sequences\": {{ incoming.json.stop_sequences|default(params.stop|default([])) }},\n \"stream\": {{ incoming.json.stream|default(false) }},\n \"thinking\": {\n \"type\": \"{{ incoming.json.thinking.type|default('enabled') }}\",\n \"budget_tokens\": {{ incoming.json.thinking.budget_tokens|default(6000) }}\n }\n}" } }, "blocks": [ @@ -62,8 +81,8 @@ { "id": "n3", "type": "ProviderCall", - "pos_x": 673, - "pos_y": 455, + "pos_x": 658, + "pos_y": 564, "config": { "provider": "openai", "provider_configs": { @@ -104,9 +123,53 @@ }, { "id": "n4", + "type": "Return", + "pos_x": 1277, + "pos_y": 139, + "config": { + "target_format": "auto", + "text_template": "[[OUT6]] [[Test]]", + "_origId": "n4" + }, + "in": { + "depends": "n7.true" + } + }, + { + "id": "n5", + "type": "SetVars", + "pos_x": 171, + "pos_y": 489, + "config": { + "variables": [ + { + "id": "vmfi99ftc", + "name": "Clod", + "mode": "string", + "value": "igrovik" + }, + { + "id": "vmfi99gjw", + "name": "MyOpenAiKey", + "mode": "string", + "value": "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" + }, + { + "id": "vmfi99h1v", + "name": "Test", + "mode": "string", + "value": "Ьыыы" + } + ], + "_origId": "n5" + }, + "in": {} + }, + { + "id": "n6", "type": "ProviderCall", - "pos_x": 929.5, - "pos_y": 233.5, + "pos_x": 923, + "pos_y": 345, "config": { "provider": "openai", "provider_configs": { @@ -131,73 +194,43 @@ }, "blocks": [ { - "id": "bmfchm54f", - "name": "Объедени [[OUT3]], [[OUT2]] сделай более красиво.", + "id": "bmfdyczbd", + "name": "Объедени [[OUT3]], [[OUT4]] сделай более красиво.", "role": "user", - "prompt": "Объедени [[OUT3]], [[OUT4]] сделай более красиво. [[TEST]]", + "prompt": "Объедени [ [[OUT3]], [[OUT2]] ] сделай более красиво. напиши слово \"Красиво\" в конце.", "enabled": true, "order": 0 - } - ], - "_origId": "n4" - }, - "in": { - "depends": [ - "n3.done", - "n2.done", - "n7.true" - ] - } - }, - { - "id": "n5", - "type": "Return", - "pos_x": 1281, - "pos_y": 139.5, - "config": { - "target_format": "auto", - "text_template": "[[OUT4]] [[Test]]", - "_origId": "n5" - }, - "in": { - "depends": "n7.true" - } - }, - { - "id": "n6", - "type": "SetVars", - "pos_x": 180, - "pos_y": 477, - "config": { - "variables": [ - { - "id": "vmfche3wn", - "name": "Test", - "mode": "string", - "value": "Быбра" }, { - "id": "vmfchjpw4", - "name": "MyOpenAiKey", - "mode": "string", - "value": "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" + "id": "bmfh98jkh", + "name": "New Block1", + "role": "system", + "prompt": "1", + "enabled": true, + "order": 1 } ], "_origId": "n6" }, - "in": {} + "in": { + "depends": [ + "n2.done", + "n3.done", + "n7.false" + ] + } }, { "id": "n7", "type": "If", - "pos_x": 1145, - "pos_y": 463, + "pos_x": 1313, + "pos_y": 566, "config": { - "expr": "[[OUT4]] contains \"красиво\"", + "expr": "[[OUT6]] contains \"Красиво\"", "_origId": "n7" }, "in": { - "depends": "n4.done" + "depends": "n6.done" } } ] diff --git a/proxy.txt b/proxy.txt index f4ec9c8..dd7662c 100644 --- a/proxy.txt +++ b/proxy.txt @@ -1 +1,2 @@ -http:190.185.109.251:9525:wajrym:oxX9qp \ No newline at end of file +http:127.0.0.1:8080:: +false \ No newline at end of file diff --git a/static/editor.css b/static/editor.css index a454f8e..41750b6 100644 --- a/static/editor.css +++ b/static/editor.css @@ -1,3 +1,8 @@ +@import url('https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap'); +/* Глобальный шрифт: Merriweather — только для бренда; остальной UI — системный/Inter */ +html, body, button, input, select, textarea, code, pre, a, .chip-btn, .group-title, .hint, details.help summary, #sidebar, #inspector, #canvas, #drawflow, .drawflow, .drawflow * { + font-family: Inter, system-ui, Arial, sans-serif; +} :root { /* Цвета темы (совпадают с editor.html) */ color-scheme: dark; @@ -13,6 +18,53 @@ --connector: #7aa2f7; --connector-muted: #3b82f6; } +html, body { + height: 100%; + overflow: hidden; /* убираем общие скролл-бары страницы, чтобы не перекрывать правую стрелку */ +} + +/* Глобальные контейнеры и скроллы */ +html, body { + height: 100%; + overflow: hidden; /* убираем общие скролл-бары страницы */ +} +#container { + position: relative; /* якорь для абсолютных стрелок-переключателей */ +} + +/* Глобальные контейнеры и скроллы */ +html, body { + height: 100%; + overflow: hidden; /* убираем общие скролл-бары страницы */ +} +#container { + position: relative; /* якорь для абсолютных стрелок-переключателей */ +} +/* Grid areas to hard-pin layout regardless of hidden panels or absolute children */ +#container { + display: grid; + grid-template-areas: "side main insp"; + /* Базовая ширина колонок по умолчанию; переопределяется collapse-* ниже */ + grid-template-columns: 260px 1fr 360px; + /* Растяжение по высоте: родитель (body/html) уже 100% */ + height: 100%; + min-height: 100%; +} + +/* Map children to areas explicitly */ +#container > #sidebar { grid-area: side; } +#container > #canvas { grid-area: main; } +#container > #inspector { grid-area: insp; } +/* Снимаем скролл-бары с контейнера Drawflow, чтобы не перекрывать правую стрелку */ +#drawflow { + overflow: hidden !important; + position: relative; + z-index: 1; /* гарантируем, что канвас виден под HUD и над фоном */ + /* Растянем контейнер Drawflow на всю центральную колонку */ + width: 100%; + height: 100%; + display: block; +} /* Узлы: аккуратные контейнеры + предотвращение вылезания текста */ .drawflow .drawflow-node { @@ -36,6 +88,11 @@ border-radius: 0 0 12px 12px; overflow: hidden; /* не даём контенту вылезать за края */ } +/* Контент превью внутри .box: можем скрывать его в LOD, не ломая геометрию ноды */ +.drawflow .drawflow-node .node-preview { + pointer-events: none; + opacity: .85; +} .drawflow .drawflow-node .box textarea, .drawflow .drawflow-node .box pre, @@ -52,8 +109,9 @@ .df-node .box textarea { white-space: pre-wrap; word-break: break-word; - overflow: auto; - max-height: 180px; /* предотвращаем бесконечную высоту */ + overflow-y: auto; /* только вертикальный скролл при необходимости */ + overflow-x: hidden; /* убираем горизонтальный скролл внутри textarea */ + max-height: 180px; /* предотвращаем бесконечную высоту */ } /* Выделение выбранного узла — мягкое */ @@ -68,22 +126,24 @@ .drawflow .drawflow-node .outputs .output { background: var(--accent-2) !important; border: 2px solid color-mix(in srgb, var(--accent-2) 70%, white 0%) !important; - width: 12px !important; - height: 12px !important; + width: 16px !important; /* ↑ hit-area */ + height: 16px !important; /* ↑ hit-area */ box-shadow: 0 0 0 2px rgba(0,0,0,.25); } /* Линии соединений: плавные, аккуратные цвета */ .drawflow .connection .main-path { stroke: var(--connector) !important; - stroke-width: 2.5px !important; + /* Толщина линии масштабируется от зума (var(--zoom) задаётся на #canvas из JS) */ + stroke-width: clamp(1.6px, calc(3px / var(--zoom, 1)), 6px) !important; opacity: 0.95 !important; } .drawflow .connection .main-path.selected, .drawflow .connection:hover .main-path { stroke: var(--accent-2) !important; - stroke-width: 3px !important; + /* На hover/selected — немного толще базовой формулы */ + stroke-width: clamp(2px, calc(3.6px / var(--zoom, 1)), 7px) !important; } /* Точки изгибов/ручки */ @@ -139,6 +199,22 @@ details.help .panel { border-radius: 8px; } +/* Sidebar help: make the toggle text styled like regular sidebar hint/title, not a round icon */ +#sidebar details.sidebar-help summary.sidebar-help-toggle { + all: unset; /* сбросить круглую кнопку от общего ruleset */ + cursor: pointer; + color: var(--muted); + font-size: 12px; + line-height: 1.4; + display: block; /* как обычный текст строки подсказки */ + margin: 4px 0 6px; /* как у .hint */ +} +#sidebar details.sidebar-help summary.sidebar-help-toggle:hover { + color: #cbd5e1; + text-decoration: underline; +} +#sidebar details.sidebar-help summary.sidebar-help-toggle::-webkit-details-marker { display: none; } + /* Инпуты/тексты внутри нод — одинаковые отступы и скругления */ textarea, input[type=text] { width: 100%; @@ -149,7 +225,7 @@ textarea, input[type=text] { padding: 8px; } -/* Кнопки */ +/* Кнопки (базовые) */ button { background: #1f2937; border: 1px solid #334155; @@ -160,6 +236,70 @@ button { } button:hover { background: #273246; } +/* Верхняя панель — стиль как у чип‑стрелок и как заголовок «ПЕРЕМЕННЫЕ И МАКРОСЫ» */ +header .brand { + font-size: 12px; + text-transform: uppercase; + color: var(--muted); + letter-spacing: .08em; + font-weight: 700; + font-family: 'Merriweather', serif !important; +} + +header .actions { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: center; +} + +/* Чип‑кнопки в шапке (и ссылки как кнопки) */ +.chip-btn, +a.chip-btn { + display: inline-grid; + place-items: center; + text-decoration: none; + text-transform: uppercase; + font-size: 12px; + letter-spacing: .04em; + padding: 6px 10px; + border-radius: 8px; + background: #0f141a; + color: #e5e7eb; + border: 1px solid #334155; + box-shadow: 0 2px 6px rgba(0,0,0,.35); + transition: transform .12s ease, box-shadow .12s ease, background-color .12s ease, border-color .12s ease, color .12s ease; + user-select: none; +} +.chip-btn:hover, +a.chip-btn:hover { + background: #1f2937; + border-color: var(--accent-2); + box-shadow: 0 0 0 3px rgba(96,165,250,.22), 0 4px 10px rgba(0,0,0,.35); +} +.chip-btn:active, +a.chip-btn:active { + transform: translateY(1px); + box-shadow: 0 0 0 2px rgba(96,165,250,.20), 0 2px 6px rgba(0,0,0,.35); +} + +/* Инпуты и селекты в шапке — в одном визуальном ряду с чипами */ +.top-input { + height: 32px; + min-width: 160px; + background: #0f141a; + border: 1px solid #334155; + border-radius: 8px; + color: #e5e7eb; + padding: 6px 8px; + box-sizing: border-box; + outline: none; +} +.top-input:focus { + border-color: var(--accent-2); + box-shadow: 0 0 0 3px rgba(96,165,250,.20); +} + /* Внутренние заголовки в блоке ноды */ #inspector label { font-size: 12px; color: var(--muted); display: block; margin: 8px 0 4px; } @@ -219,6 +359,327 @@ button:hover { background: #273246; } background-color: var(--bg); background-image: radial-gradient(circle at 1px 1px, rgba(255,255,255,0.06) 1px, transparent 0); background-size: 24px 24px; + position: relative; /* ensure HUD overlay absolute positioning works */ + overflow: visible; /* позволяем HUD-элементам (чипам) выходить за границы канваса */ + /* Растяжение на всю область grid-ячейки main */ + width: 100%; + height: 100%; + display: block; + pointer-events: auto; +} + +/* HUD overlay layer for LOD labels (screen-space, not affecting node geometry) */ +#lod-label-layer, +#lod-hints { + position: absolute; + inset: 0; + pointer-events: none; /* do not intercept mouse */ + z-index: 5; /* над нодами/линиями (у них z-index auto), но ниже боковых панелей */ + overflow: visible; /* НЕ клипать чипы — пусть уходят под панели */ +} + +/* Visual chip for node label at far zoom */ +/* Generic LOD chip style (used in #lod-hints and legacy #lod-label-layer) */ +.lod-chip { + position: absolute; + background: #10151c; + color: #e5e7eb; + border: 2px solid #3b82f6; + border-radius: 8px; + padding: 2px 6px; + white-space: nowrap; + line-height: 1.2; + text-shadow: 0 1px 1px rgba(0,0,0,.6); + box-shadow: 0 0 0 2px rgba(59,130,246,.20), 0 2px 6px rgba(0,0,0,.35); + transform: translate(-50%, -100%); /* centered by JS; here as default */ + font: 18px/1.2 Inter, system-ui, Arial, sans-serif; +} + +/* LOD: level-of-detail tuned by classes on #canvas (set from JS) */ +#canvas.lod-compact .drawflow .drawflow-node .node-preview, +#canvas.lod-tiny .drawflow .drawflow-node .node-preview { + display: none !important; /* скрываем только содержимое превью, не меняя коробку ноды */ +} + + + +/* Порты и линии — тоньше в компактных режимах */ +/* Убраны фиксированные толщины в LOD — используется формула от --zoom */ + +#canvas.lod-compact .drawflow .drawflow-node .inputs .input, +#canvas.lod-compact .drawflow .drawflow-node .outputs .output { width: 12px !important; height: 12px !important; } +#canvas.lod-tiny .drawflow .drawflow-node .inputs .input, +#canvas.lod-tiny .drawflow .drawflow-node .outputs .output { width: 10px !important; height: 10px !important; } + +/* Индикатор статуса — свечением по рамке остаётся, доп. эффект на тайтле */ +#canvas.lod-compact .drawflow .drawflow-node.node-running .title-box, +#canvas.lod-tiny .drawflow .drawflow-node.node-running .title-box { box-shadow: 0 0 0 2px rgba(96,165,250,.25) !important; } +#canvas.lod-compact .drawflow .drawflow-node.node-ok .title-box, +#canvas.lod-tiny .drawflow .drawflow-node.node-ok .title-box { box-shadow: 0 0 0 2px rgba(52,211,153,.25) !important; } +#canvas.lod-compact .drawflow .drawflow-node.node-err .title-box, +#canvas.lod-tiny .drawflow .drawflow-node.node-err .title-box { box-shadow: 0 0 0 2px rgba(239,68,68,.25) !important; } +/* Sleep (amber) */ +#canvas.lod-compact .drawflow .drawflow-node.node-sleep .title-box, +#canvas.lod-tiny .drawflow .drawflow-node.node-sleep .title-box { box-shadow: 0 0 0 2px rgba(245,158,11,.28) !important; } + +/* Run drawer (settings) */ +#run-drawer { + position: fixed; + top: 52px; + right: -380px; + width: 360px; + height: calc(100vh - 52px); + background: var(--panel); + border-left: 1px solid var(--border); + box-shadow: -8px 0 24px rgba(0,0,0,.35); + z-index: 10000; + transition: right .18s ease-in-out; + padding: 12px; + color: #e5e7eb; +} +#run-drawer.open { right: 0; } +#run-drawer h3 { margin: 0 0 8px; font-size: 14px; color: var(--muted); } +#run-drawer .row { display: flex; gap: 8px; margin: 8px 0; align-items: center; } +#run-drawer input[type=number], #run-drawer select { + width: 100%; + background: #0f141a; + border: 1px solid #2b3646; + color: #e5e7eb; + border-radius: 8px; + padding: 6px 8px; +} +#run-drawer .actions { display: flex; justify-content: space-between; gap: 8px; margin-top: 12px; } + +/* Logs panel — перенос строк и стили списка + подсветка видов событий */ +#logs-panel pre { + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: anywhere; +} + +/* Список логов слева */ +#logs-panel #logs-list { + overflow-y: auto; + overflow-x: hidden; +} +#logs-panel #logs-list .logs-row { + padding: 8px 10px; + border-bottom: 1px solid #1f2b3b; + cursor: pointer; + display: block; +} +#logs-panel #logs-list .logs-row .title { + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +#logs-panel #logs-list .logs-row .sub { + opacity: .85; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +#logs-panel #logs-list .logs-row:hover { + background: rgba(96,165,250,.08); +} +#logs-panel #logs-list .logs-row.selected { + background: #0f1e2d; + outline: 2px solid #60a5fa; + box-shadow: 0 0 0 2px rgba(96,165,250,.18) inset; +} +#logs-panel #logs-list .logs-row.selected .title { + color: #eaf2ff; +} + +/* Dim non-main logs (anything except start/done/error and HTTP errors) */ +#logs-panel #logs-list .logs-row.dim:not(.selected) { + opacity: .55; +} +#logs-panel #logs-list .logs-row.dim:not(.selected) .title { + color: #94a3b8; + font-weight: 500; +} +#logs-panel #logs-list .logs-row.dim:not(.selected) .sub { + color: #7c8797; +} +/* Ensure selection overrides dimming */ +#logs-panel #logs-list .logs-row.selected.dim { + opacity: 1 !important; +} + +/* Subtle emphasis for main node events even when not selected */ +#logs-panel #logs-list .logs-row.kind-node.ev-start:not(.selected) { + background: rgba(96,165,250,.07); +} +#logs-panel #logs-list .logs-row.kind-node.ev-done:not(.selected) { + background: rgba(52,211,153,.07); +} + +/* Типы логов — цветовая метка по левому бордеру */ +#logs-panel #logs-list .logs-row.kind-node.ev-start { border-left: 3px solid #60a5fa; } /* start — синий */ +#logs-panel #logs-list .logs-row.kind-node.ev-done { border-left: 3px solid #34d399; } /* done — зелёный */ +#logs-panel #logs-list .logs-row.kind-node.ev-error { border-left: 3px solid #ef4444; } /* error — красный */ +#logs-panel #logs-list .logs-row.kind-node.ev-sleep { border-left: 3px solid #f59e0b; } /* sleep — янтарный */ +#logs-panel #logs-list .logs-row.kind-http.http-ok { border-left: 3px solid #34d399; } /* HTTP 2xx/3xx */ +#logs-panel #logs-list .logs-row.kind-http.http-err { border-left: 3px solid #ef4444; } /* HTTP 4xx/5xx */ +#logs-panel #logs-list .logs-row.kind-vars { border-left: 3px solid #a78bfa; } /* Vars — фиолетовый */ + +/* Тонкие скролл-бары для панелей/textarea, чтобы не «мешали» интерфейсу */ +#sidebar, #inspector, #scheme-panel, #run-drawer, .vars-popover, #logs-panel pre, #logs-list { + scrollbar-width: thin; + scrollbar-color: #334155 transparent; +} +#sidebar::-webkit-scrollbar, +#inspector::-webkit-scrollbar, +#scheme-panel::-webkit-scrollbar, +#run-drawer::-webkit-scrollbar, +.vars-popover::-webkit-scrollbar, +#logs-panel pre::-webkit-scrollbar, +#logs-list::-webkit-scrollbar { + width: 8px; + height: 8px; +} +#sidebar::-webkit-scrollbar-thumb, +#inspector::-webkit-scrollbar-thumb, +#scheme-panel::-webkit-scrollbar-thumb, +#run-drawer::-webkit-scrollbar-thumb, +.vars-popover::-webkit-scrollbar-thumb, +#logs-panel pre::-webkit-scrollbar-thumb, +#logs-list::-webkit-scrollbar-thumb { + background-color: #334155; + border-radius: 8px; +} +/* Убираем горизонтальный скролл в текстовых редакторах инспектора */ +#inspector textarea { overflow-x: hidden; } + +/* Mini-scheme panel */ +#scheme-panel { + position: fixed; + top: 52px; + right: 24px; + width: 380px; + height: auto; + max-height: calc(100vh - 72px); + background: var(--panel); + border: 1px solid var(--border); + box-shadow: -8px 0 24px rgba(0,0,0,.35); + z-index: 10000; + padding: 12px; + color: #e5e7eb; + overflow: hidden; /* запрещаем выход содержимого за границы панели */ +} +#scheme-panel h3 { margin: 0 0 8px; font-size: 14px; color: var(--muted); } +#scheme-panel canvas { + width: 100%; + height: auto; /* фактический размер управляется JS с учётом DPR */ + border: 1px solid #2b3646; + border-radius: 8px; + background: #0f141a; + display: block; /* убираем нижний пробел как у inline */ +} + +/* Docked STORE panel (reuse existing popover node, просто фиксируем сбоку) */ +#vars-popover { + position: fixed !important; + top: 52px !important; + right: 24px !important; + height: calc(100vh - 72px) !important; + max-height: none !important; + z-index: 9999 !important; +} + +/* Vars popover visual layout */ +.vars-popover { + width: 520px; /* фиксированная удобная ширина */ + background: #0f141a; + border: 1px solid #2b3646; + border-radius: 10px; + box-shadow: 0 6px 28px rgba(0,0,0,.45); + color: #e5e7eb; + overflow: hidden; /* чтобы шапка/низ не вылезали */ +} + +/* Header as compact grid */ +.vars-popover .vars-head { + display: grid; + grid-template-columns: 1fr minmax(160px, 1.2fr) 120px auto auto auto; + gap: 8px; + align-items: center; + padding: 10px; + border-bottom: 1px solid #2b3646; +} +.vars-popover .vars-head > strong { + font-size: 14px; + color: var(--muted); +} +.vars-popover .vars-head input#vars-search, +.vars-popover .vars-head select#vars-scope { + width: 100%; + background: #0f141a; + border: 1px solid #2b3646; + color: #e5e7eb; + border-radius: 8px; + padding: 6px 8px; + box-sizing: border-box; +} +.vars-popover .vars-head .vars-braces { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: #a7b0bf; + white-space: nowrap; +} +.vars-popover .vars-head button { + background: #1f2937; + border: 1px solid #334155; + color: #e5e7eb; + padding: 6px 8px; + border-radius: 8px; + cursor: pointer; +} +.vars-popover .vars-head button:hover { background: #273246; } + +/* Info line */ +.vars-popover #vars-info { + padding: 8px 10px; + border-bottom: 1px solid #2b3646; + color: #a7b0bf; +} + +/* List area */ +.vars-popover #vars-list { + padding: 8px 0; + overflow: auto; + max-height: calc(100% - 110px); /* остаток высоты относительно контейнера */ +} + +/* Rows styling (override inline minimal grid by sizing columns and clamping) */ +.vars-popover #vars-list .row { + display: grid !important; + grid-template-columns: 220px 1fr !important; /* фиксируем ширину колонки макроса */ + gap: 8px !important; + padding: 8px 10px !important; + border-bottom: 1px solid #1f2b3b !important; + cursor: pointer !important; +} +.vars-popover #vars-list .row:hover { + background: rgba(96,165,250,.08); +} +.vars-popover #vars-list .row code { + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + color: #60a5fa !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; + max-width: 220px !important; + display: block !important; +} +.vars-popover #vars-list .row > div { + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; } /* Port hover affordance (no heavy effects) */ @@ -324,4 +785,304 @@ button:hover { background: #273246; } .drawflow .drawflow-node.node-err .box { border-color: #ef4444 !important; /* red */ box-shadow: 0 0 0 2px rgba(239,68,68,.35) !important; +} + +/* Sleep state — amber */ +.drawflow .drawflow-node.node-sleep .title-box, +.drawflow .drawflow-node.node-sleep .box { + border-color: #f59e0b !important; /* amber */ + box-shadow: 0 0 0 2px rgba(245,158,11,.35) !important; +} +/* LOD hover tooltip (shown only in compact/tiny via JS) */ +.lod-tooltip { + position: absolute; + pointer-events: none; + background: #10151c; + color: #e5e7eb; + border: 2px solid #3b82f6; + border-radius: 8px; + padding: 2px 6px; + white-space: nowrap; + font: 18px/1.2 Inter, system-ui, Arial, sans-serif; + box-shadow: 0 0 0 2px rgba(59,130,246,.20), 0 2px 6px rgba(0,0,0,.35); + transform: translate(-50%, -100%); + z-index: 1000; /* above nodes/edges but below menus */ +} +/* Снимаем скролл-бары с контейнера Drawflow, чтобы не перекрывать правую стрелку */ +#drawflow { + overflow: hidden !important; + position: relative; + z-index: 1; /* гарантируем, что канвас виден под HUD и над фоном */ + /* Растянем контейнер Drawflow на всю центральную колонку */ + width: 100%; + height: 100%; + display: block; +} + +/* Panels collapse controls and layout */ +#container.collapse-left { + grid-template-columns: 0 1fr 360px !important; +} +#container.collapse-right { + grid-template-columns: 260px 1fr 0 !important; +} +/* BOTH collapsed: высокий приоритет, чтобы не осталась «пустая колонка» */ +#container.collapse-left.collapse-right { + grid-template-columns: 0 1fr 0 !important; +} +/* removed explicit grid placement to avoid grid misplacement on some browsers */ + +/* Поверх HUD: панели выше, чтобы чипы уходили ПОД них */ +#sidebar, #inspector { + position: relative; + z-index: 10; +} +/* Inspector: extra bottom padding + no horizontal scroll to ensure OUTx block reachable */ +#inspector { + overflow-y: auto; + overflow-x: hidden; + padding-bottom: 240px; +} + +#container.collapse-left #sidebar { + /* Полностью исключаем сайдбар из потока при сворачивании, + чтобы он не создавал «чёрную накладку» и не перехватывал клики */ + display: none !important; + + /* Страховочные свойства на случай кеша/старых стилей */ + width: 0 !important; + padding: 0 !important; + border-right: 0 !important; + overflow: hidden !important; + pointer-events: none !important; + background: transparent !important; + visibility: hidden !important; +} +#container.collapse-right #inspector { + /* Полностью исключаем инспектор из потока и стэка, + чтобы гарантированно не перекрывал канвас (фикс «чёрной накладки») */ + display: none !important; + + /* Ниже — страховочные свойства для старых кешированных стилей */ + width: 0 !important; + padding: 0 !important; + border-left: 0 !important; + overflow: hidden !important; + pointer-events: none !important; + background: transparent !important; + visibility: hidden !important; +} + +/* На случай, если браузер продолжит хит‑тестить потомков — + дополнительно отключаем события на всём поддереве инспектора */ +#container.collapse-right #inspector * { + pointer-events: none !important; +} + +/* Edge toggle buttons — positioned relative to #container; coordinates set by JS (placeToggles) */ +.panel-toggle { + position: absolute; + top: 50%; + transform: translateY(-50%); /* vertical center */ + width: 22px; + height: 36px; + display: grid; + place-items: center; + border-radius: 8px; + background: #0f141a; + color: #e5e7eb; + border: 1px solid #334155; + box-shadow: 0 2px 6px rgba(0,0,0,.35); + z-index: 9000; /* below drawers/popovers (#run-drawer/#scheme-panel/#vars-popover), above HUD/chips */ + cursor: pointer; + user-select: none; + opacity: .95; + pointer-events: auto; /* ensure clickable over HUD layers */ +} +/* Left/Right offsets are set dynamically via JS, no static left/right here */ +.panel-toggle:hover { + background: #1f2937; + border-color: var(--accent-2); + box-shadow: 0 0 0 3px rgba(96,165,250,.22), 0 4px 10px rgba(0,0,0,.35); +} +/* Safety: hide any leftover debug overlay if it still exists in DOM */ +#dbg-hit { display: none !important; } + +/* Remove dotted/dashed focus outlines on controls (requested) */ +button:focus, +a.chip-btn:focus, +.chip-btn:focus, +.panel-toggle:focus { + outline: none !important; + box-shadow: none !important; +} +/* Logs UI: stronger selected row and readable details override */ +#logs-panel #logs-list .logs-row.selected { + border-left-width: 5px; /* stronger accent on selected */ +} +#logs-panel #logs-list .logs-row.selected .title { + font-weight: 700; + letter-spacing: .01em; +} +#logs-panel #logs-list .logs-row.selected .sub { + color: #cbd5e1; +} +/* Smooth collapse/expand of side panels via grid columns transition */ +#container { + transition: grid-template-columns .18s ease-in-out; +} + +/* Optional: soften perceived change on panel boxes themselves */ +#sidebar, +#inspector { + transition: opacity .18s ease-in-out; +} +#container.collapse-left #sidebar { opacity: 0; } +#container.collapse-right #inspector { opacity: 0; } +/* Smooth appear animations for overlay/panels (trigger on display: block) */ +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes fadeInRight { + from { opacity: 0; transform: translateX(8px); } + to { opacity: 1; transform: translateX(0); } +} +@keyframes fadeInDown { + from { opacity: 0; transform: translateY(-6px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Respect reduced motion */ +@media (prefers-reduced-motion: reduce) { + #container { + transition: none !important; + } + #sidebar, #inspector, + #vars-popover, #logs-panel, #scheme-panel { + transition: none !important; + animation: none !important; + } +} + +/* Vars popover: quick fade + slight upward motion on open (display becomes block in JS) */ +#vars-popover[style*="display: block"] { + animation: fadeInUp .16s ease-out; + will-change: opacity, transform; +} + +/* Logs panel: fade + slight leftward motion (panel is fixed on the right) */ +#logs-panel[style*="display: block"] { + animation: fadeInRight .16s ease-out; + will-change: opacity, transform; +} + +/* Mini-scheme panel: fade + slight downward motion (drops from header area) */ +#scheme-panel[style*="display: block"] { + animation: fadeInDown .16s ease-out; + will-change: opacity, transform; +} +/* Unify select look to match preset-select (header .top-input style) */ +select#f-provider, +select#tx-preset, +select#vars-scope, +select.v-mode, +select#pm-role { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + + background: #0f141a; + color: #e5e7eb; + border: 1px solid #334155; + border-radius: 8px; + height: 32px; + padding: 6px 30px 6px 8px; + box-sizing: border-box; + outline: none; + font: 12px/1 Inter, system-ui, Arial, sans-serif; + + transition: + border-color .12s ease, + box-shadow .12s ease, + background-color .12s ease, + color .12s ease; +} + +/* Hover and focus states consistent with .top-input */ +select#f-provider:hover, +select#tx-preset:hover, +select#vars-scope:hover, +select.v-mode:hover, +select#pm-role:hover { + background: #121820; + border-color: var(--accent-2); +} + +select#f-provider:focus, +select#tx-preset:focus, +select#vars-scope:focus, +select.v-mode:focus, +select#pm-role:focus { + border-color: var(--accent-2); + box-shadow: 0 0 0 3px rgba(96,165,250,.20); +} + +/* Compact width contexts: keep natural width unless container forces 100% */ +#inspector select#f-provider, +#inspector select#tx-preset, +#inspector select.v-mode, +#inspector select#pm-role { + width: 100%; +} + +/* Vars dock header already grid-fit; just unify visuals */ +.vars-popover .vars-head select#vars-scope { + height: 32px; +} + +/* Add a minimal dropdown arrow via background SVG, aligned right */ +select#f-provider, +select#tx-preset, +select#vars-scope, +select.v-mode, +select#pm-role { + background-image: + url("data:image/svg+xml;utf8,"); + background-repeat: no-repeat; + background-position: right 8px center; + background-size: 12px 12px; +} + +/* High-contrast for disabled */ +select#f-provider:disabled, +select#tx-preset:disabled, +select#vars-scope:disabled, +select.v-mode:disabled, +select#pm-role:disabled { + opacity: .6; + cursor: not-allowed; +} + +/* Respect reduced motion for focus rings */ +@media (prefers-reduced-motion: reduce) { + select#f-provider, + select#tx-preset, + select#vars-scope, + select.v-mode, + select#pm-role { + transition: none; + } +} +/* Header spacing: add safe gap after brand and small indent for wrapped action rows */ +header .brand { + margin-right: 12px; /* space after "НАДTAVERN" */ + padding-right: 2px; /* micro buffer to avoid optical collision */ +} +header .actions { + margin-left: 8px; /* indent when rows wrap under brand */ +} +@media (max-width: 860px) { + header .brand { margin-right: 10px; } + header .actions { margin-left: 6px; } } \ No newline at end of file diff --git a/static/editor.html b/static/editor.html index 8cfbc47..d4d37a1 100644 --- a/static/editor.html +++ b/static/editor.html @@ -3,8 +3,10 @@ - НадTavern — Визуальный редактор нод + НАДTAVERN + +