mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 09:14:58 +03:00
🚚 chore: переименование Chatbolls → Chatballs во всём проекте
Бренд, API-схема, TOTP issuer, письма, тексты бота уведомлений, загрузчик виджета (window.ChatballsChat, типы postMessage), ключи localStorage, id/параметры виджета помощи, health-service, README, скрипты, демо (пароль Chatballs-Demo-2026). Макеты в design/baseline не тронуты — это файлы владельца. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
22e0a12a16
commit
dbd7ae3150
28 files changed
+51
-44
No files matched your search
@@ -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 <URL репозитория> chatbolls
|
||||
cd chatbolls/code/custocrm
|
||||
git clone <URL репозитория> chatballs
|
||||
cd chatballs/code/custocrm
|
||||
.\scripts\start.ps1
|
||||
```
|
||||
|
||||
Linux / macOS:
|
||||
|
||||
```bash
|
||||
git clone <URL репозитория> chatbolls
|
||||
cd chatbolls/code/custocrm
|
||||
git clone <URL репозитория> chatballs
|
||||
cd chatballs/code/custocrm
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
@@ -45,7 +45,7 @@ cd chatbolls/code/custocrm
|
||||
Демо ставится в вашу организацию и удаляется целиком одной кнопкой: **Настройки →
|
||||
Демо-данные**. Там же — учётные записи демо-сотрудников из разных групп, чтобы
|
||||
посмотреть систему их глазами (пароль общий и намеренно публичный —
|
||||
`Chatbolls-Demo-2026`). Ваши данные при удалении не затрагиваются: сид ведёт
|
||||
`Chatballs-Demo-2026`). Ваши данные при удалении не затрагиваются: сид ведёт
|
||||
реестр созданных записей и удаляет ровно их.
|
||||
|
||||
Для разработки то же доступно из командной строки:
|
||||
|
||||
@@ -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 <no-reply@edevs.tech>")
|
||||
DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "Chatballs <no-reply@edevs.tech>")
|
||||
|
||||
# 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")
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Демо-провайдер: живой AI без ключей и сети.
|
||||
|
||||
Нужен, чтобы человек, развернувший Chatbolls за минуту, увидел агента в
|
||||
Нужен, чтобы человек, развернувший Chatballs за минуту, увидел агента в
|
||||
работе до подключения BYOK-провайдера. Отвечает детерминированно по знаниям,
|
||||
которые runtime положил в системные сообщения («Отвечай только на основе этих
|
||||
знаний»): выбирает предложения с наибольшим пересечением слов с вопросом.
|
||||
|
||||
@@ -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"
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
{
|
||||
"type": "RELEASE_PUBLISHED",
|
||||
"level": "SUCCESS",
|
||||
"title": "Обновление Chatbolls: голосовые сообщения",
|
||||
"title": "Обновление Chatballs: голосовые сообщения",
|
||||
"body": "Операторы могут отправлять голосовые в Telegram и MAX, входящие расшифровываются одной кнопкой.",
|
||||
"targetRoute": "chat",
|
||||
"audience": "ALL",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"_comment": "Демо «Ателье Норд» (дизайн-базлайн v2, каст кадров A1/A2 и A–H). Ставится в организацию установщика: организация и её владелец здесь не создаются. Пароль всех демо-сотрудников один — см. demoPassword.",
|
||||
"demoPassword": "Chatbolls-Demo-2026",
|
||||
"demoPassword": "Chatballs-Demo-2026",
|
||||
"showcaseAccounts": [
|
||||
"elena",
|
||||
"svetlana",
|
||||
|
||||
@@ -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 минут. Если вы не запрашивали сброс, просто проигнорируйте это письмо."
|
||||
),
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# одним тегом:
|
||||
# <script src="https://hub.edevs.tech/chat-widget.js"
|
||||
# data-widget-key="wgt_public_key" async></script>
|
||||
# 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: ""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AuthenticatedUser, SessionUser } from "../types";
|
||||
|
||||
const preferenceKey = "chatbolls.organizationPublicId";
|
||||
const preferenceKey = "chatballs.organizationPublicId";
|
||||
|
||||
export function activateOrganization(
|
||||
identity: AuthenticatedUser,
|
||||
|
||||
@@ -69,7 +69,7 @@ export function OrganizationLogoField({
|
||||
tone="danger"
|
||||
icon="trash"
|
||||
title="Удалить логотип?"
|
||||
description="Вместо него в меню снова будет показан знак Chatbolls."
|
||||
description="Вместо него в меню снова будет показан знак Chatballs."
|
||||
actions={(
|
||||
<>
|
||||
<Button variant="secondary" onClick={() => setConfirmingRemoval(false)}>
|
||||
|
||||
@@ -94,7 +94,7 @@ export function AuthResetPassword({ onDone }: { onDone: () => void }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthFrame title="Новый пароль" subtitle="Задайте новый пароль для входа в Chatbolls." logo="shield" width={420} note={backLink}>
|
||||
<AuthFrame title="Новый пароль" subtitle="Задайте новый пароль для входа в Chatballs." logo="shield" width={420} note={backLink}>
|
||||
<form className="auth-card" onSubmit={submit}>
|
||||
{error && <div className="auth-error"><span className="auth-error-dot">!</span><span>{error}</span></div>}
|
||||
<label className="field-label">Новый пароль</label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
const SCRIPT_ID = "chatbolls-help-widget-loader";
|
||||
const SCRIPT_ID = "chatballs-help-widget-loader";
|
||||
|
||||
export function PortalWebWidget({ widgetKey }: { widgetKey: string }) {
|
||||
useEffect(() => {
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("supportHref", () => {
|
||||
supportAvailable: true,
|
||||
supportWidgetKey: "wgt_foxray_support",
|
||||
supportChannelCode: "foxray-support",
|
||||
})).toBe("https://foxray.example/account?from=help&chatbollsSupportWidget=wgt_foxray_support");
|
||||
})).toBe("https://foxray.example/account?from=help&chatballsSupportWidget=wgt_foxray_support");
|
||||
});
|
||||
|
||||
it("does not build an unauthenticated fallback without a channel", () => {
|
||||
|
||||
@@ -7,7 +7,7 @@ export function supportHref(product: HelpProduct): string | null {
|
||||
if (!product.siteUrl || !product.supportWidgetKey) return null;
|
||||
try {
|
||||
const url = new URL(product.siteUrl);
|
||||
url.searchParams.set("chatbollsSupportWidget", product.supportWidgetKey);
|
||||
url.searchParams.set("chatballsSupportWidget", product.supportWidgetKey);
|
||||
return url.toString();
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -31,7 +31,7 @@ const STEPS: Step[] = [
|
||||
];
|
||||
|
||||
function storageKey(user: SessionUser): string {
|
||||
return `chatbolls.launch.${user.organizationPublicId}.hidden`;
|
||||
return `chatballs.launch.${user.organizationPublicId}.hidden`;
|
||||
}
|
||||
|
||||
function isHidden(user: SessionUser): boolean {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Icon } from "./icons";
|
||||
// сотрудникам подсказки не показываются, это решает вызывающая сторона).
|
||||
// Закрытие запоминается на устройстве по ключу `id`.
|
||||
|
||||
const STORAGE_PREFIX = "chatbolls.hint.";
|
||||
const STORAGE_PREFIX = "chatballs.hint.";
|
||||
|
||||
function isDismissed(id: string): boolean {
|
||||
try {
|
||||
|
||||
@@ -26,7 +26,7 @@ function requestSupportToken(): Promise<string> {
|
||||
if (
|
||||
event.source !== window.parent
|
||||
|| event.origin !== HOST_ORIGIN
|
||||
|| data.type !== "chatbolls-chat-token-response"
|
||||
|| data.type !== "chatballs-chat-token-response"
|
||||
|| data.instanceId !== INSTANCE_ID
|
||||
|| data.requestId !== requestId
|
||||
) return;
|
||||
@@ -37,7 +37,7 @@ function requestSupportToken(): Promise<string> {
|
||||
}
|
||||
window.addEventListener("message", receive);
|
||||
window.parent.postMessage({
|
||||
type: "chatbolls-chat-token-request",
|
||||
type: "chatballs-chat-token-request",
|
||||
instanceId: INSTANCE_ID,
|
||||
requestId,
|
||||
widgetKey: WIDGET_KEY,
|
||||
|
||||
Generated
+7
@@ -49,6 +49,7 @@
|
||||
"@edevs/ui": "0.1.0",
|
||||
"antd": "^5.26.0",
|
||||
"js-yaml": "^5.2.1",
|
||||
"lucide": "^1.41.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -2589,6 +2590,12 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lucide": {
|
||||
"version": "1.41.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide/-/lucide-1.41.0.tgz",
|
||||
"integrity": "sha512-fIXP7JzC1vehdqOrMhwpbcXzX6CvUMuoDf16HY1FmC4sDBeuwrH2A0c6QmLn3wnSvaMchpEyM+JWijyTvOs3kw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env sh
|
||||
# Локальный запуск Chatbolls (Linux/macOS): canonical compose.yaml + dev override.
|
||||
# Локальный запуск Chatballs (Linux/macOS): canonical compose.yaml + dev override.
|
||||
# Параметры не нужны: владельца и демо-данные создаёт мастер первого запуска
|
||||
# в браузере (http://localhost). Режим поставки: ./scripts/start.sh [cloud|self-hosted]
|
||||
set -eu
|
||||
|
||||
Reference in new issue
Block a user