sync: UI animations select styling TLS verify flag via proxy second line brand spacing
This commit is contained in:
@@ -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)"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "<unrepresentable>"
|
||||
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 = "<resp.text decode error>"
|
||||
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): <unavailable>")
|
||||
try:
|
||||
body_text = resp.text
|
||||
resp_text = resp.text
|
||||
except Exception:
|
||||
body_text = "<resp.text decode error>"
|
||||
resp_text = "<resp.text decode error>"
|
||||
print("Body Text:")
|
||||
print(body_text)
|
||||
print(resp_text)
|
||||
print("===== RawForward RESPONSE END =====")
|
||||
except Exception:
|
||||
resp_text = "<resp.text decode error>"
|
||||
|
||||
# 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)}
|
||||
|
||||
|
||||
|
||||
@@ -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 "<masked>"
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
1034
editor.html
1034
editor.html
File diff suppressed because it is too large
Load Diff
@@ -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 \"Красиво\"",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
http:190.185.109.251:9525:wajrym:oxX9qp
|
||||
http:127.0.0.1:8080::
|
||||
false
|
||||
@@ -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,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a7b0bf' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
|
||||
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; }
|
||||
}
|
||||
1811
static/editor.html
1811
static/editor.html
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,18 @@
|
||||
loop_max_iters: 1000,
|
||||
loop_time_budget_ms: 10000,
|
||||
clear_var_store: true,
|
||||
// New: default HTTP timeout for upstream requests (seconds)
|
||||
http_timeout_sec: 60,
|
||||
// New (v1): стратегия извлечения текста для [[OUTx]] (глобальная по умолчанию)
|
||||
// auto | deep | openai | gemini | claude | jsonpath
|
||||
text_extract_strategy: 'auto',
|
||||
// Используется при стратегии jsonpath (dot-нотация, поддержка индексов: a.b.0.c)
|
||||
text_extract_json_path: '',
|
||||
// Разделитель при объединении массива результатов
|
||||
text_join_sep: '\n',
|
||||
// v2: коллекция пресетов извлечения текста, управляется в "Запуск"
|
||||
// [{ id, name, strategy, json_path, join_sep }]
|
||||
text_extract_presets: [],
|
||||
};
|
||||
|
||||
function getPipelineMeta() {
|
||||
@@ -28,16 +40,45 @@
|
||||
|
||||
function updatePipelineMeta(p) {
|
||||
if (!p || typeof p !== 'object') return;
|
||||
const keys = ['id','name','parallel_limit','loop_mode','loop_max_iters','loop_time_budget_ms','clear_var_store'];
|
||||
const keys = [
|
||||
'id','name','parallel_limit','loop_mode','loop_max_iters','loop_time_budget_ms','clear_var_store','http_timeout_sec',
|
||||
'text_extract_strategy','text_extract_json_path','text_join_sep','text_join_sep','text_join_SEP',
|
||||
// v2 presets collection
|
||||
'text_extract_presets'
|
||||
];
|
||||
for (const k of keys) {
|
||||
if (Object.prototype.hasOwnProperty.call(p, k) && p[k] !== undefined && p[k] !== null && (k === 'clear_var_store' ? true : p[k] !== '')) {
|
||||
if (k === 'parallel_limit' || k === 'loop_max_iters' || k === 'loop_time_budget_ms') {
|
||||
const v = parseInt(p[k], 10);
|
||||
if (!Number.isNaN(v) && v > 0) _pipelineMeta[k] = v;
|
||||
} else if (k === 'http_timeout_sec') {
|
||||
const fv = parseFloat(p[k]);
|
||||
if (!Number.isNaN(fv) && fv > 0) _pipelineMeta[k] = fv;
|
||||
} else if (k === 'clear_var_store') {
|
||||
_pipelineMeta[k] = !!p[k];
|
||||
} else {
|
||||
_pipelineMeta[k] = String(p[k]);
|
||||
// спец-обработка коллекции пресетов
|
||||
if (k === 'text_extract_presets') {
|
||||
try {
|
||||
const arr = Array.isArray(p[k]) ? p[k] : [];
|
||||
_pipelineMeta[k] = arr
|
||||
.filter(it => it && typeof it === 'object')
|
||||
.map(it => ({
|
||||
id: String((it.id ?? '') || ('p' + Date.now().toString(36) + Math.random().toString(36).slice(2))),
|
||||
name: String(it.name ?? 'Preset'),
|
||||
strategy: String(it.strategy ?? 'auto'),
|
||||
json_path: String(it.json_path ?? ''),
|
||||
join_sep: String(it.join_sep ?? '\n'),
|
||||
}));
|
||||
} catch (_) {
|
||||
_pipelineMeta[k] = [];
|
||||
}
|
||||
} else if (k.toLowerCase() === 'text_join_sep') {
|
||||
// нормализация ключа join separator (допускаем разные написания)
|
||||
_pipelineMeta['text_join_sep'] = String(p[k]);
|
||||
} else {
|
||||
_pipelineMeta[k] = String(p[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,11 +101,24 @@
|
||||
const wantIds = {}; // drawflow id -> желаемый/финальный nX
|
||||
const isValidNid = (s) => typeof s === 'string' && /^n\d+$/i.test(s.trim());
|
||||
|
||||
// Helper: вернуть исключительно «живые» данные ноды из редактора (если доступны).
|
||||
// Это исключает расхождения между DOM.__data и editor.getNodeFromId(..).data.
|
||||
function mergedNodeData(df, el, dfid) {
|
||||
try {
|
||||
const nid = parseInt(dfid, 10);
|
||||
const n = (w.editor && typeof w.editor.getNodeFromId === 'function') ? w.editor.getNodeFromId(nid) : null;
|
||||
if (n && n.data) return n.data;
|
||||
} catch (_) {}
|
||||
if (df && df.data) return df.data;
|
||||
// как последний fallback — DOM.__data (почти не используется после этого изменения)
|
||||
return (el && el.__data) ? el.__data : {};
|
||||
}
|
||||
|
||||
// Первый проход: резервируем существующие валидные _origId
|
||||
for (const id in dfNodes) {
|
||||
const df = dfNodes[id];
|
||||
const el = document.querySelector(`#node-${id}`);
|
||||
const datacopySrc = el && el.__data ? el.__data : (df.data || {});
|
||||
const datacopySrc = mergedNodeData(df, el, id);
|
||||
const tmp = typeof w.applyNodeDefaults === 'function'
|
||||
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
||||
: (JSON.parse(JSON.stringify(datacopySrc)));
|
||||
@@ -95,11 +149,22 @@
|
||||
for (const id in dfNodes) {
|
||||
const df = dfNodes[id];
|
||||
const el = document.querySelector(`#node-${id}`);
|
||||
const datacopySrc = el && el.__data ? el.__data : (df.data || {});
|
||||
const datacopySrc = mergedNodeData(df, el, id);
|
||||
const datacopy = typeof w.applyNodeDefaults === 'function'
|
||||
? w.applyNodeDefaults(df.name, JSON.parse(JSON.stringify(datacopySrc)))
|
||||
: (JSON.parse(JSON.stringify(datacopySrc)));
|
||||
try { datacopy._origId = idMap[id]; } catch (e) {}
|
||||
|
||||
// Спец-обработка SetVars: гарантированно берём свежие variables из живых данных редактора
|
||||
try {
|
||||
if (String(df.name) === 'SetVars') {
|
||||
const nid = parseInt(id, 10);
|
||||
const nLive = (w.editor && typeof w.editor.getNodeFromId === 'function') ? w.editor.getNodeFromId(nid) : null;
|
||||
const v = nLive && nLive.data && Array.isArray(nLive.data.variables) ? nLive.data.variables : (Array.isArray(datacopy.variables) ? datacopy.variables : []);
|
||||
datacopy.variables = v.map(x => ({ ...(x || {}) })); // глубокая копия
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
nodes.push({
|
||||
id: idMap[id],
|
||||
type: df.name,
|
||||
@@ -205,6 +270,12 @@
|
||||
loop_max_iters: (typeof meta.loop_max_iters === 'number' ? meta.loop_max_iters : 1000),
|
||||
loop_time_budget_ms: (typeof meta.loop_time_budget_ms === 'number' ? meta.loop_time_budget_ms : 10000),
|
||||
clear_var_store: (typeof meta.clear_var_store === 'boolean' ? meta.clear_var_store : true),
|
||||
http_timeout_sec: (typeof meta.http_timeout_sec === 'number' ? meta.http_timeout_sec : 60),
|
||||
text_extract_strategy: (meta.text_extract_strategy || 'auto'),
|
||||
text_extract_json_path: (meta.text_extract_json_path || ''),
|
||||
text_join_sep: (meta.text_join_sep || '\n'),
|
||||
// v2: persist presets
|
||||
text_extract_presets: (Array.isArray(meta.text_extract_presets) ? meta.text_extract_presets : []),
|
||||
nodes
|
||||
};
|
||||
}
|
||||
@@ -225,6 +296,12 @@
|
||||
loop_max_iters: (p && typeof p.loop_max_iters === 'number') ? p.loop_max_iters : 1000,
|
||||
loop_time_budget_ms: (p && typeof p.loop_time_budget_ms === 'number') ? p.loop_time_budget_ms : 10000,
|
||||
clear_var_store: (p && typeof p.clear_var_store === 'boolean') ? p.clear_var_store : true,
|
||||
http_timeout_sec: (p && typeof p.http_timeout_sec === 'number') ? p.http_timeout_sec : 60,
|
||||
text_extract_strategy: (p && typeof p.text_extract_strategy === 'string') ? p.text_extract_strategy : 'auto',
|
||||
text_extract_json_path: (p && typeof p.text_extract_json_path === 'string') ? p.text_extract_json_path : '',
|
||||
text_join_sep: (p && typeof p.text_join_sep === 'string') ? p.text_join_sep : '\n',
|
||||
// v2: presets from pipeline.json
|
||||
text_extract_presets: (p && Array.isArray(p.text_extract_presets)) ? p.text_extract_presets : [],
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user