diff --git a/README.md b/README.md index 2999c6d..9aca47c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Chatbolls +# Chatballs Canonical implementation workspace for `hub.edevs.tech`. @@ -10,16 +10,16 @@ Canonical implementation workspace for `hub.edevs.tech`. Windows (PowerShell): ```powershell -git clone chatbolls -cd chatbolls/code/custocrm +git clone chatballs +cd chatballs/code/custocrm .\scripts\start.ps1 ``` Linux / macOS: ```bash -git clone chatbolls -cd chatbolls/code/custocrm +git clone chatballs +cd chatballs/code/custocrm ./scripts/start.sh ``` @@ -45,7 +45,7 @@ cd chatbolls/code/custocrm Демо ставится в вашу организацию и удаляется целиком одной кнопкой: **Настройки → Демо-данные**. Там же — учётные записи демо-сотрудников из разных групп, чтобы посмотреть систему их глазами (пароль общий и намеренно публичный — -`Chatbolls-Demo-2026`). Ваши данные при удалении не затрагиваются: сид ведёт +`Chatballs-Demo-2026`). Ваши данные при удалении не затрагиваются: сид ведёт реестр созданных записей и удаляет ровно их. Для разработки то же доступно из командной строки: diff --git a/apps/backend/hub_backend/settings_base.py b/apps/backend/hub_backend/settings_base.py index f6067c7..524462b 100644 --- a/apps/backend/hub_backend/settings_base.py +++ b/apps/backend/hub_backend/settings_base.py @@ -146,7 +146,7 @@ EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "587")) EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "") EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "") EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "true").lower() == "true" -DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "Chatbolls ") +DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "Chatballs ") # Base URL of the internal UI, used to build links inside transactional emails. INTERNAL_UI_BASE_URL = os.environ.get("INTERNAL_UI_BASE_URL", "http://localhost:5173") diff --git a/apps/backend/hub_backend/urls_app.py b/apps/backend/hub_backend/urls_app.py index 862fd43..3e963a7 100644 --- a/apps/backend/hub_backend/urls_app.py +++ b/apps/backend/hub_backend/urls_app.py @@ -8,7 +8,7 @@ urlpatterns = [ path("chat-widget.js", WidgetLoaderView.as_view(), name="chat-widget-loader"), path( "api/v1/schema/", - get_schema_view(title="Chatbolls API", version="0.1.0"), + get_schema_view(title="Chatballs API", version="0.1.0"), name="openapi-schema", ), path("api/v1/auth/", include("hub_platform.identity.auth_urls")), diff --git a/apps/backend/hub_platform/ai/provider/demo.py b/apps/backend/hub_platform/ai/provider/demo.py index 7241ae9..5845faa 100644 --- a/apps/backend/hub_platform/ai/provider/demo.py +++ b/apps/backend/hub_platform/ai/provider/demo.py @@ -1,6 +1,6 @@ """Демо-провайдер: живой AI без ключей и сети. -Нужен, чтобы человек, развернувший Chatbolls за минуту, увидел агента в +Нужен, чтобы человек, развернувший Chatballs за минуту, увидел агента в работе до подключения BYOK-провайдера. Отвечает детерминированно по знаниям, которые runtime положил в системные сообщения («Отвечай только на основе этих знаний»): выбирает предложения с наибольшим пересечением слов с вопросом. diff --git a/apps/backend/hub_platform/api/permissions.py b/apps/backend/hub_platform/api/permissions.py index 42640b0..1607a5d 100644 --- a/apps/backend/hub_platform/api/permissions.py +++ b/apps/backend/hub_platform/api/permissions.py @@ -29,9 +29,9 @@ class HasCapability(BasePermission): class CloudDeliveryOnly(BasePermission): - """Guard tenant APIs that exist only in the managed Chatbolls Cloud.""" + """Guard tenant APIs that exist only in the managed Chatballs Cloud.""" - message = "This operation is available only in Chatbolls Cloud" + message = "This operation is available only in Chatballs Cloud" def has_permission(self, request: Request, view: APIView) -> bool: return settings.CUS_DELIVERY_MODE == "CLOUD" diff --git a/apps/backend/hub_platform/health/views.py b/apps/backend/hub_platform/health/views.py index 02d28ed..98f2ff8 100644 --- a/apps/backend/hub_platform/health/views.py +++ b/apps/backend/hub_platform/health/views.py @@ -7,7 +7,7 @@ from django.http import JsonResponse def live(request): surface = settings.CUS_RUNTIME_SURFACE return JsonResponse( - {"status": "ok", "service": f"chatbolls-{surface}", "surface": surface} + {"status": "ok", "service": f"chatballs-{surface}", "surface": surface} ) diff --git a/apps/backend/hub_platform/identity/auth/totp_utils.py b/apps/backend/hub_platform/identity/auth/totp_utils.py index b9c5cd8..892e4a0 100644 --- a/apps/backend/hub_platform/identity/auth/totp_utils.py +++ b/apps/backend/hub_platform/identity/auth/totp_utils.py @@ -8,7 +8,7 @@ import time from hub_platform.identity.models import HumanUser TOTP_SESSION_KEY = "identity_pending_totp_user_id" -TOTP_ISSUER = "Chatbolls" +TOTP_ISSUER = "Chatballs" TOTP_PERIOD_SECONDS = 30 diff --git a/apps/backend/hub_platform/identity/demo_seed/data/operations.json b/apps/backend/hub_platform/identity/demo_seed/data/operations.json index aabe067..2ce580f 100644 --- a/apps/backend/hub_platform/identity/demo_seed/data/operations.json +++ b/apps/backend/hub_platform/identity/demo_seed/data/operations.json @@ -118,7 +118,7 @@ { "type": "RELEASE_PUBLISHED", "level": "SUCCESS", - "title": "Обновление Chatbolls: голосовые сообщения", + "title": "Обновление Chatballs: голосовые сообщения", "body": "Операторы могут отправлять голосовые в Telegram и MAX, входящие расшифровываются одной кнопкой.", "targetRoute": "chat", "audience": "ALL", diff --git a/apps/backend/hub_platform/identity/demo_seed/data/organization.json b/apps/backend/hub_platform/identity/demo_seed/data/organization.json index 8c27158..fe61b72 100644 --- a/apps/backend/hub_platform/identity/demo_seed/data/organization.json +++ b/apps/backend/hub_platform/identity/demo_seed/data/organization.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, "_comment": "Демо «Ателье Норд» (дизайн-базлайн v2, каст кадров A1/A2 и A–H). Ставится в организацию установщика: организация и её владелец здесь не создаются. Пароль всех демо-сотрудников один — см. demoPassword.", - "demoPassword": "Chatbolls-Demo-2026", + "demoPassword": "Chatballs-Demo-2026", "showcaseAccounts": [ "elena", "svetlana", diff --git a/apps/backend/hub_platform/identity/emails.py b/apps/backend/hub_platform/identity/emails.py index d46a70e..ae32efe 100644 --- a/apps/backend/hub_platform/identity/emails.py +++ b/apps/backend/hub_platform/identity/emails.py @@ -16,10 +16,10 @@ def _password_setup_url(user: HumanUser) -> str: def send_initial_access_email(user: HumanUser) -> None: setup_url = _password_setup_url(user) send_mail( - subject="Первичный доступ к Chatbolls", + subject="Первичный доступ к Chatballs", message=( f"Здравствуйте, {user.full_name or user.email}.\n\n" - "Для вас создана учётная запись Chatbolls. Чтобы задать пароль первичного доступа, " + "Для вас создана учётная запись Chatballs. Чтобы задать пароль первичного доступа, " "перейдите по ссылке:\n" f"{setup_url}\n\n" "Ссылка действует 30 минут. Если вы не ожидали это письмо, обратитесь к владельцу организации." @@ -32,10 +32,10 @@ def send_initial_access_email(user: HumanUser) -> None: def send_password_reset_email(user: HumanUser) -> None: reset_url = _password_setup_url(user) send_mail( - subject="Восстановление доступа к Chatbolls", + subject="Восстановление доступа к Chatballs", message=( f"Здравствуйте, {user.full_name or user.email}.\n\n" - "Вы запросили сброс пароля для Chatbolls. Чтобы задать новый пароль, перейдите по ссылке:\n" + "Вы запросили сброс пароля для Chatballs. Чтобы задать новый пароль, перейдите по ссылке:\n" f"{reset_url}\n\n" "Ссылка действует 30 минут. Если вы не запрашивали сброс, просто проигнорируйте это письмо." ), diff --git a/apps/backend/hub_platform/identity/setup.py b/apps/backend/hub_platform/identity/setup.py index 730cc26..49ab9d6 100644 --- a/apps/backend/hub_platform/identity/setup.py +++ b/apps/backend/hub_platform/identity/setup.py @@ -115,7 +115,7 @@ def complete_setup(data: SetupInput) -> SetupResult: # Блокировка от гонки двух вкладок: второй запрос дождётся первого и # увидит созданную организацию. with connections[INSTANCE_DB_ALIAS].cursor() as cursor: - cursor.execute("SELECT pg_advisory_xact_lock(hashtext('chatbolls.instance_setup'))") + cursor.execute("SELECT pg_advisory_xact_lock(hashtext('chatballs.instance_setup'))") if Organization.objects.exists(): raise SetupAlreadyCompleted() diff --git a/apps/backend/hub_platform/identity/test_seed_demo.py b/apps/backend/hub_platform/identity/test_seed_demo.py index 0ba9578..eb2a0df 100644 --- a/apps/backend/hub_platform/identity/test_seed_demo.py +++ b/apps/backend/hub_platform/identity/test_seed_demo.py @@ -104,7 +104,7 @@ class DemoDatasetTests(TestCase): self.assertEqual(memberships.count(), 7) self.assertEqual(memberships.filter(blocked_at__isnull=False).count(), 1) anna = HumanUser.objects.get(email="a.kim@atelie-nord.ru") - self.assertTrue(anna.check_password("Chatbolls-Demo-2026")) + self.assertTrue(anna.check_password("Chatballs-Demo-2026")) self.assertTrue(HumanUser.objects.get(email="k.volkov@atelie-nord.ru").totp_enabled) # Агенты: активный с AI, черновик без AI, выключенный канал. @@ -198,7 +198,7 @@ class DemoDatasetApiTests(TestCase): self.assertEqual([a["email"] for a in accounts], ["e.kuznetsova@atelie-nord.ru", "s.petrova@atelie-nord.ru", "i.saveliev@atelie-nord.ru"]) self.assertEqual(accounts[1]["groups"], ["Операторы"]) self.assertEqual(accounts[2]["groups"], ["Поддержка"]) - self.assertTrue(all(a["password"] == "Chatbolls-Demo-2026" for a in accounts)) + self.assertTrue(all(a["password"] == "Chatballs-Demo-2026" for a in accounts)) self.assertEqual(self.client.post("/api/v1/company/demo/").status_code, 400) removing = self.client.delete("/api/v1/company/demo/") @@ -212,7 +212,7 @@ class DemoDatasetApiTests(TestCase): self.client.post("/api/v1/company/demo/") self._dispatch_pending() employee = APIClient() - employee.login(username="s.petrova@atelie-nord.ru", password="Chatbolls-Demo-2026") + employee.login(username="s.petrova@atelie-nord.ru", password="Chatballs-Demo-2026") self.assertEqual(employee.post("/api/v1/company/demo/").status_code, 403) self.assertEqual(employee.delete("/api/v1/company/demo/").status_code, 403) diff --git a/apps/backend/hub_platform/notifications/binding.py b/apps/backend/hub_platform/notifications/binding.py index bef5bd7..d4e0c1a 100644 --- a/apps/backend/hub_platform/notifications/binding.py +++ b/apps/backend/hub_platform/notifications/binding.py @@ -24,8 +24,8 @@ logger = logging.getLogger(__name__) CODE_TTL = timedelta(minutes=10) NOTIFIER_PURPOSE = "notifications" -CONFIRMATION_TEXT = "Готово! Уведомления Chatbolls подключены. Отключить можно в профиле." -HINT_TEXT = "Это сервисный бот уведомлений Chatbolls. Чтобы подключить уведомления, откройте профиль в Chatbolls и нажмите «Подключить»." +CONFIRMATION_TEXT = "Готово! Уведомления Chatballs подключены. Отключить можно в профиле." +HINT_TEXT = "Это сервисный бот уведомлений Chatballs. Чтобы подключить уведомления, откройте профиль в Chatballs и нажмите «Подключить»." # База deep-link по провайдеру: и TG, и MAX поддерживают ?start=<код>. _DEEP_LINK_BASE = { diff --git a/apps/backend/hub_platform/support_portals/addressing.py b/apps/backend/hub_platform/support_portals/addressing.py index c43f552..e72fb84 100644 --- a/apps/backend/hub_platform/support_portals/addressing.py +++ b/apps/backend/hub_platform/support_portals/addressing.py @@ -46,7 +46,7 @@ def clean_portal_domains(portal) -> None: ) if portal.custom_domain == portal.hosted_domain: raise ValidationError( - {"custom_domain": "Свой домен должен отличаться от адреса Chatbolls"} + {"custom_domain": "Свой домен должен отличаться от адреса Chatballs"} ) collision = portal.__class__.objects.exclude(pk=portal.pk).filter( models.Q(hosted_domain=portal.custom_domain) diff --git a/apps/backend/hub_platform/support_portals/domain_services.py b/apps/backend/hub_platform/support_portals/domain_services.py index 63d64e6..b660ac7 100644 --- a/apps/backend/hub_platform/support_portals/domain_services.py +++ b/apps/backend/hub_platform/support_portals/domain_services.py @@ -67,7 +67,7 @@ def verify_custom_domain(portal: SupportPortal) -> SupportPortal: ) from error if settings.CUS_HELP_PUBLIC_IPV4 not in addresses: raise ValidationError( - {"customDomain": "A-запись домена указывает не на сервер Chatbolls"} + {"customDomain": "A-запись домена указывает не на сервер Chatballs"} ) expected = domain_verification_value(portal) diff --git a/apps/backend/hub_platform/support_portals/tests/test_management_api.py b/apps/backend/hub_platform/support_portals/tests/test_management_api.py index d519c40..337d138 100644 --- a/apps/backend/hub_platform/support_portals/tests/test_management_api.py +++ b/apps/backend/hub_platform/support_portals/tests/test_management_api.py @@ -203,7 +203,7 @@ class SupportPortalManagementTests(SupportPortalTestCase): ) self.assertEqual(response.status_code, 400, response.content) - self.assertIn("указывает не на сервер Chatbolls", str(response.json())) + self.assertIn("указывает не на сервер Chatballs", str(response.json())) @override_settings(CUS_APP_PRIMARY_HOSTS=["app.customer.example"]) def test_custom_domain_cannot_shadow_application_host(self) -> None: diff --git a/apps/backend/hub_platform/webchat/loader.py b/apps/backend/hub_platform/webchat/loader.py index 21988c3..a0f7eb6 100644 --- a/apps/backend/hub_platform/webchat/loader.py +++ b/apps/backend/hub_platform/webchat/loader.py @@ -2,7 +2,7 @@ # одним тегом: # -# Authenticated host регистрирует async token provider через ChatbollsChat.init; +# Authenticated host регистрирует async token provider через ChatballsChat.init; # Product Support Token никогда не попадает в URL или data-*. # Лоадер рисует launcher и открывает панель в изолированном iframe (/chat/). # @@ -27,7 +27,7 @@ LOADER_JS = r""" var panelUrl = origin + "/chat/?" + entryQuery + "&instanceId=" + encodeURIComponent(instanceId); var open = false, frame = null, unread = false, callActive = false, tokenProvider = null; - var api = window.ChatbollsChat = window.ChatbollsChat || {}; + var api = window.ChatballsChat = window.ChatballsChat || {}; window.CustoCRMChat = api; // legacy alias для уже встроенных хостов api._instances = api._instances || []; api._providers = api._providers || {}; @@ -118,17 +118,17 @@ LOADER_JS = r""" play(notification); } if (d.type === "edevs-chat-activity" && d.kind === "call") setCallActive(Boolean(d.active)); - if (d.type === "chatbolls-chat-token-request" && tokenProvider) { + if (d.type === "chatballs-chat-token-request" && tokenProvider) { Promise.resolve().then(tokenProvider).then(function (token) { frame.contentWindow.postMessage({ - type: "chatbolls-chat-token-response", + type: "chatballs-chat-token-response", instanceId: instanceId, requestId: d.requestId, token: String(token || "") }, origin); }).catch(function () { frame.contentWindow.postMessage({ - type: "chatbolls-chat-token-response", + type: "chatballs-chat-token-response", instanceId: instanceId, requestId: d.requestId, token: "" diff --git a/apps/internal-ui/src/auth/session.ts b/apps/internal-ui/src/auth/session.ts index 1d93c2a..a6789b2 100644 --- a/apps/internal-ui/src/auth/session.ts +++ b/apps/internal-ui/src/auth/session.ts @@ -1,6 +1,6 @@ import type { AuthenticatedUser, SessionUser } from "../types"; -const preferenceKey = "chatbolls.organizationPublicId"; +const preferenceKey = "chatballs.organizationPublicId"; export function activateOrganization( identity: AuthenticatedUser, diff --git a/apps/internal-ui/src/features/administration/OrganizationLogoField.tsx b/apps/internal-ui/src/features/administration/OrganizationLogoField.tsx index 26bf824..b16c18b 100644 --- a/apps/internal-ui/src/features/administration/OrganizationLogoField.tsx +++ b/apps/internal-ui/src/features/administration/OrganizationLogoField.tsx @@ -69,7 +69,7 @@ export function OrganizationLogoField({ tone="danger" icon="trash" title="Удалить логотип?" - description="Вместо него в меню снова будет показан знак Chatbolls." + description="Вместо него в меню снова будет показан знак Chatballs." actions={( <>