From 7e0a12a2c9ab624bcc5cae12dc17da8493edc2df Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 24 Sep 2026 18:02:27 +0300 Subject: [PATCH] =?UTF-8?q?:recycle:=20refactor:=20=D1=83=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2?= =?UTF-8?q?=D1=88=D0=B8=D0=B5=20=D1=83=D0=BF=D0=BE=D0=BC=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20Hub=20=D0=B8=20CustoAI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- Caddyfile | 2 +- apps/backend/Dockerfile.production | 2 +- apps/backend/chatballs/ai/agent_card_views.py | 2 +- .../ai/migrations/0008_aiagent_credential_mode.py | 6 +++--- .../ai/migrations/0012_agent_provider_integration.py | 2 +- .../ai/migrations/0015_backfill_channel_agents.py | 2 +- .../ai/migrations/0016_remove_credential_mode.py | 3 +-- apps/backend/chatballs/ai/models.py | 12 +++++------- apps/backend/chatballs/ai/provider/openai_http.py | 8 +++----- apps/backend/chatballs/ai/provider/openrouter.py | 2 +- apps/backend/chatballs/ai/provider/routing.py | 6 +++--- apps/backend/chatballs/ai/provider_selection.py | 6 +++--- apps/backend/chatballs/ai/test_provider_modes.py | 2 +- apps/backend/chatballs/ai/tests.py | 2 +- .../migrations/0005_enforce_policy_invariants.py | 2 +- apps/backend/chatballs/channels/models.py | 2 +- apps/backend/chatballs/channels/runtime.py | 2 +- apps/backend/chatballs/channels/selectors.py | 3 +-- apps/backend/chatballs/channels/services.py | 4 ++-- apps/backend/chatballs/conversations/models.py | 2 +- .../chatballs/conversations/test_vk_transport.py | 4 ++-- .../chatballs/conversations/transports/base.py | 2 +- .../events/management/commands/run_worker.py | 2 +- apps/backend/chatballs/identity/auth/invitations.py | 2 +- apps/backend/chatballs/identity/bootstrap.py | 2 +- .../backend/chatballs/identity/invitation_service.py | 6 +++--- .../0007_employee_roles_position_department.py | 4 ++-- .../migrations/0008_enforce_position_title.py | 2 +- .../identity/migrations/0015_organization_status.py | 2 +- .../identity/migrations/0016_channel_capabilities.py | 2 +- apps/backend/chatballs/identity/models.py | 2 +- apps/backend/chatballs/identity/setup.py | 2 +- apps/backend/chatballs/identity/tests.py | 4 ++-- apps/backend/chatballs/integrations/models.py | 4 ++-- apps/backend/chatballs/integrations/services.py | 4 ++-- apps/backend/chatballs/integrations/tests.py | 2 +- apps/backend/chatballs/platform/payloads.py | 2 +- .../chatballs/platform/provisioning_command.py | 4 ++-- .../chatballs/platform/provisioning_models.py | 4 ++-- .../chatballs/platform/provisioning_service.py | 4 ++-- apps/backend/chatballs/platform/validation.py | 2 +- apps/backend/chatballs/platform/views.py | 2 +- .../migrations/0005_platform_provisioning_grants.py | 4 ++-- .../tenancy/migrations/0022_demo_registry_guards.py | 2 +- .../0031_platform_provisioning_table_grants.py | 2 +- .../0032_app_role_ingress_and_bootstrap.py | 2 +- apps/backend/chatballs/tenancy/models.py | 4 ++-- apps/backend/chatballs/tenancy/routing.py | 4 ++-- apps/backend/chatballs/tenancy/test_storage_quota.py | 2 +- apps/backend/chatballs_backend/settings_base.py | 7 ++----- apps/internal-ui/src/features/agents/styles.css | 2 +- .../features/conversations/ConversationWorkspace.tsx | 2 +- apps/internal-ui/src/features/conversations/types.ts | 2 +- .../src/features/integrations/model.test.ts | 8 ++++---- apps/internal-ui/src/features/integrations/model.ts | 4 ++-- apps/internal-ui/src/shared/providers.ts | 4 ++-- apps/web-chat/src/styles.css | 4 ++-- apps/web-chat/vite.config.ts | 2 +- chatballs | 2 +- deploy/docker/frontend.Dockerfile | 2 +- packages/shared/src/callRtc.ts | 2 +- 62 files changed, 95 insertions(+), 104 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6f65256..13b5c11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,7 +33,7 @@ UI / дизайн: - Перед тем как написать свой элемент, искать существующий в `shared/` и в соседних фичах. Второй стандарт того же элемента — это дефект, а не свобода реализации. Никаких служебных записок в продукте: -- В продуктовом UI не должно быть служебной лексики. Запрещены на экране: коды спек и решений (SPEC-HUB-*, ADR-HUB-*, DG-*), номера правил (P1-P5 и любые другие), слова «инвариант», «констрейнт», «миграция», «scope» в техническом смысле, имена таблиц, полей и enum-значений БД и API (`allowCheckoutActions`, `Conversation.channel`, `PROTECT`, `OK`/`ERROR` как есть), ссылки на внутренние документы и любые пометки для разработчика. +- В продуктовом UI не должно быть служебной лексики. Запрещены на экране: коды спек и решений (SPEC-*, ADR-*, DG-*), номера правил (P1-P5 и любые другие), слова «инвариант», «констрейнт», «миграция», «scope» в техническом смысле, имена таблиц, полей и enum-значений БД и API (`allowCheckoutActions`, `Conversation.channel`, `PROTECT`, `OK`/`ERROR` как есть), ссылки на внутренние документы и любые пометки для разработчика. - Пользователю показывается следствие и способ исправить, а не внутреннее правило. «Коммерческие действия недоступны непродуктовому каналу. Назначьте продукт» — да. «Запрещено инвариантом P1» — нет. - Технические коды остаются в коде, комментариях, логах и API-ответах, но не в текстах интерфейса. - Это относится и к baseline-макетам: служебный текст, попавший в макет, не является основанием выводить его на экран. Макет реализуется, дополняется по указанию владельца, служебная лексика в реализацию не переносится. diff --git a/Caddyfile b/Caddyfile index d9e6762..8c8ba09 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,4 +1,4 @@ -# Caddyfile — единый HTTP/HTTPS public boundary Chatballs (ADR-HUB-0028 §gateway). +# Caddyfile — единый HTTP/HTTPS public boundary Chatballs (ADR-CHATBALLS-0028 §gateway). # Подставляется в release bundle и монтируется в контейнер gateway. # # Свежая установка не знает своего домена: человек поднимает докер на сервере и diff --git a/apps/backend/Dockerfile.production b/apps/backend/Dockerfile.production index db899d1..4db2cb8 100644 --- a/apps/backend/Dockerfile.production +++ b/apps/backend/Dockerfile.production @@ -22,7 +22,7 @@ COPY apps/backend /app/apps/backend COPY deploy/secrets/generate-instance-secrets.sh /usr/local/bin/chatballs-generate-secrets.sh RUN chmod 0755 /usr/local/bin/chatballs-generate-secrets.sh -# collectstatic в образе (ADR-HUB-0028): STATIC_ROOT испечён, runtime-шаг не нужен. +# collectstatic в образе (ADR-CHATBALLS-0028): STATIC_ROOT испечён, runtime-шаг не нужен. # Build-time secret нужен только чтобы settings загрузились в production-режиме; # collectstatic не обращается к БД/Redis/S3. whitenoise раздаёт static в runtime. # Build-time dummy values satisfy C04 runtime guards (distinct DB users in diff --git a/apps/backend/chatballs/ai/agent_card_views.py b/apps/backend/chatballs/ai/agent_card_views.py index e23980a..ba2240d 100644 --- a/apps/backend/chatballs/ai/agent_card_views.py +++ b/apps/backend/chatballs/ai/agent_card_views.py @@ -228,7 +228,7 @@ class AgentCardDeactivateView(_AgentCardStatusView): class AgentCardTestChatView(APIView): permission_classes = [HasCapability] - # Исполняет агента, а не изменяет канал: остаётся на ai.manage (ADR-HUB-0037 §9). + # Исполняет агента, а не изменяет канал: остаётся на ai.manage. required_capability = "ai.manage" def post(self, request: Request, agent_id: int) -> Response: diff --git a/apps/backend/chatballs/ai/migrations/0008_aiagent_credential_mode.py b/apps/backend/chatballs/ai/migrations/0008_aiagent_credential_mode.py index 6deb7fe..213b322 100644 --- a/apps/backend/chatballs/ai/migrations/0008_aiagent_credential_mode.py +++ b/apps/backend/chatballs/ai/migrations/0008_aiagent_credential_mode.py @@ -1,4 +1,4 @@ -# Generated for CustoAI / BYOK credential mode (ADR-HUB-0033 §4, SPEC-CHATBALLS-0024 §2). +# Историческое поле режима доступа к AI; удалено в 0016. from django.db import migrations, models @@ -14,8 +14,8 @@ class Migration(migrations.Migration): model_name='aiagent', name='credential_mode', field=models.CharField( - choices=[('CUSTOAI', 'CustoAI (Managed)'), ('BYOK', 'BYOK')], - default='CUSTOAI', + choices=[('BYOK', 'BYOK')], + default='BYOK', max_length=16, ), ), diff --git a/apps/backend/chatballs/ai/migrations/0012_agent_provider_integration.py b/apps/backend/chatballs/ai/migrations/0012_agent_provider_integration.py index daa52e6..b479fc0 100644 --- a/apps/backend/chatballs/ai/migrations/0012_agent_provider_integration.py +++ b/apps/backend/chatballs/ai/migrations/0012_agent_provider_integration.py @@ -1,4 +1,4 @@ -# SPEC-HUB-0027 §9, ADR-HUB-0037 §8 — этап 5, шаги 1-2. +# Провайдер LLM переезжает с канала на агента. # # BYOK-секрет переезжает с канала на агента. До сих пор credential_mode и model # жили на AIAgent, а provider_integration — на Channel: одно решение было diff --git a/apps/backend/chatballs/ai/migrations/0015_backfill_channel_agents.py b/apps/backend/chatballs/ai/migrations/0015_backfill_channel_agents.py index 7d08456..ff57091 100644 --- a/apps/backend/chatballs/ai/migrations/0015_backfill_channel_agents.py +++ b/apps/backend/chatballs/ai/migrations/0015_backfill_channel_agents.py @@ -15,7 +15,7 @@ def backfill_agents(apps, schema_editor): channel=channel, name=channel.name, status="DRAFT", - credential_mode="CUSTOAI", + credential_mode="BYOK", ) diff --git a/apps/backend/chatballs/ai/migrations/0016_remove_credential_mode.py b/apps/backend/chatballs/ai/migrations/0016_remove_credential_mode.py index 320eb1a..0b92d21 100644 --- a/apps/backend/chatballs/ai/migrations/0016_remove_credential_mode.py +++ b/apps/backend/chatballs/ai/migrations/0016_remove_credential_mode.py @@ -1,5 +1,4 @@ -# ADR-CHATBALLS-0042 §3: managed-режим CustoAI удалён вместе с тарифным контуром. -# BYOK — единственный режим; поле credential_mode больше не нужно. +# ADR-CHATBALLS-0042 §3: BYOK — единственный режим; поле credential_mode больше не нужно. from django.db import migrations diff --git a/apps/backend/chatballs/ai/models.py b/apps/backend/chatballs/ai/models.py index 1868691..b568867 100644 --- a/apps/backend/chatballs/ai/models.py +++ b/apps/backend/chatballs/ai/models.py @@ -6,7 +6,7 @@ from pgvector.django import VectorField from chatballs.tenancy.models import TenantRelationModel -# Один основной агент на канал обработки (ADR-HUB-0019, ADR-CHATBALLS-0023). +# Один основной агент на канал обработки (ADR-CHATBALLS-0023). DEFAULT_AI_MODEL = "anthropic/claude-sonnet-4.6" @@ -40,8 +40,6 @@ class AIAgentStatus(models.TextChoices): -# Managed-режим CustoAI удалён вместе с тарифным контуром (ADR-CHATBALLS-0042 §3): - # AI работает только через провайдера организации (AIAgent.provider_integration). @@ -151,7 +149,7 @@ class KnowledgeAttachment(TenantRelationModel): # Непредсказуемый идентификатор публичной ссылки скачивания (ADR-CHATBALLS-0023): - # агент может отдать ссылку клиенту в мессенджер, где нет аутентификации Hub. + # агент может отдать ссылку клиенту в мессенджер, где нет аутентификации установки. public_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False) @@ -193,7 +191,7 @@ class KnowledgeAttachment(TenantRelationModel): # Абсолютная ссылка скачивания: уходит клиентам в мессенджеры, поэтому - # строится от публичного адреса Hub, а не от request. + # строится от публичного адреса установки, а не от request. from django.urls import reverse @@ -343,7 +341,7 @@ class AIAgent(TenantRelationModel): channel = models.OneToOneField("channels.Channel", on_delete=models.CASCADE, related_name="ai_agent") - # BYOK-секрет организации (SPEC-HUB-0027 §9). Раньше жил на Channel, из-за + # BYOK-секрет организации. Раньше жил на Channel, из-за # чего credential_mode и model были на агенте, а секрет — на канале: одно @@ -494,7 +492,7 @@ class LlmInvocation(TenantRelationModel): tenant_relation_fields = ("channel",) - # Учёт по каналу (ADR-HUB-0019). + # Учёт по каналу. channel = models.ForeignKey("channels.Channel", on_delete=models.SET_NULL, null=True, blank=True, related_name="ai_invocations") diff --git a/apps/backend/chatballs/ai/provider/openai_http.py b/apps/backend/chatballs/ai/provider/openai_http.py index a318bc6..099a541 100644 --- a/apps/backend/chatballs/ai/provider/openai_http.py +++ b/apps/backend/chatballs/ai/provider/openai_http.py @@ -1,10 +1,8 @@ -"""Shared HTTP layer for OpenAI-compatible LLM providers (ADR-HUB-0033 §7, - -ADR-CHATBALLS-0034 §3). +"""Shared HTTP layer for OpenAI-compatible LLM providers (ADR-CHATBALLS-0034 §3). -The OpenRouter, generic Custom and CustoAI (Yandex AI Studio) providers all +The OpenRouter and generic Custom providers both speak the same Chat Completions shape: @@ -20,7 +18,7 @@ speak the same Chat Completions shape: -This module owns the HTTP transport and response parsing so the three adapters +This module owns the HTTP transport and response parsing so the adapters do not duplicate it. Adapters stay responsible for their own product semantics diff --git a/apps/backend/chatballs/ai/provider/openrouter.py b/apps/backend/chatballs/ai/provider/openrouter.py index 5b74637..84cd42f 100644 --- a/apps/backend/chatballs/ai/provider/openrouter.py +++ b/apps/backend/chatballs/ai/provider/openrouter.py @@ -8,7 +8,7 @@ class OpenRouterProvider(LLMProvider): OpenAI Chat Completions shape with usage.include=true (returns the actual USD cost in usage.cost). Delegates HTTP/parsing to the shared openai_http - layer (ADR-HUB-0033 §7, ADR-CHATBALLS-0034 §3); this adapter only carries the + layer (ADR-CHATBALLS-0034 §3); this adapter only carries the OpenRouter product semantics (cost reporting). Exercised with a real key; tests use the LocalProvider. """ diff --git a/apps/backend/chatballs/ai/provider/routing.py b/apps/backend/chatballs/ai/provider/routing.py index fbd2399..22fa768 100644 --- a/apps/backend/chatballs/ai/provider/routing.py +++ b/apps/backend/chatballs/ai/provider/routing.py @@ -5,7 +5,7 @@ Resolves an LLM provider and the effective model from the channel agent's own credentials, the integration is selected explicitly on `AIAgent`, and managed AI credits are not consumed. -Источник провайдера переехал с канала на агента (SPEC-HUB-0027 §9). Один +Источник провайдера переехал с канала на агента. Один релиз резолвер падает на `Channel.provider_integration` для записей, не попавших в data-миграцию; после удаления поля канала fallback уходит. @@ -14,7 +14,7 @@ the owner's choice is forbidden (ADR-CHATBALLS-0020:45). The integration MUST be the one the agent points at. This module also closes the as-built gap where the OpenRouter «Модель по -умолчанию» field was decorative (SPEC-HUB-0005:388, SPEC-CHATBALLS-0024 §4.3, §6): +умолчанию» field was decorative (SPEC-CHATBALLS-0024 §4.3, §6): for OpenRouter and Custom integrations the configured `default_model` is read at runtime and overrides `AIAgent.model`. """ @@ -114,7 +114,7 @@ def _channel_integration(channel) -> Integration: agent = getattr(channel, "ai_agent", None) integration = getattr(agent, "provider_integration", None) if agent else None if integration is None: - # Переходный fallback на один релиз (SPEC-HUB-0027 §9 шаг 3): записи, + # Переходный fallback на один релиз: записи, # не попавшие в data-миграцию, продолжают работать через канал. integration = getattr(channel, "provider_integration", None) if integration is None or not integration.secret: diff --git a/apps/backend/chatballs/ai/provider_selection.py b/apps/backend/chatballs/ai/provider_selection.py index 606d287..069ac85 100644 --- a/apps/backend/chatballs/ai/provider_selection.py +++ b/apps/backend/chatballs/ai/provider_selection.py @@ -1,7 +1,7 @@ -"""Выбор LLM-провайдера агента (SPEC-HUB-0027 §9, ADR-CHATBALLS-0042 §3). +"""Выбор LLM-провайдера агента (ADR-CHATBALLS-0042 §3). -Managed-режим CustoAI удалён вместе с тарифным контуром: агент работает только -через интеграцию организации (BYOK, ADR-CHATBALLS-0034). Функция ничего не пишет: +Агент работает только через интеграцию организации (BYOK, +ADR-CHATBALLS-0034). Функция ничего не пишет: возвращает разрешённую интеграцию и модель, вызывающий сервис ставит их на агента в одной транзакции. Агент без интеграции — валидное состояние черновика; активация без провайдера запрещена в set_agent_active. diff --git a/apps/backend/chatballs/ai/test_provider_modes.py b/apps/backend/chatballs/ai/test_provider_modes.py index bb92cd9..4a4047e 100644 --- a/apps/backend/chatballs/ai/test_provider_modes.py +++ b/apps/backend/chatballs/ai/test_provider_modes.py @@ -130,7 +130,7 @@ class ProviderModeTests(TestCase): class AgentProviderOwnershipTests(TestCase): - """SPEC-HUB-0027 §9 — провайдер живёт на агенте, канал не изменяется.""" + """Провайдер живёт на агенте, канал не изменяется.""" def setUp(self) -> None: bootstrap_owner(email="owner@example.com", password="temporary-password") diff --git a/apps/backend/chatballs/ai/tests.py b/apps/backend/chatballs/ai/tests.py index a9cc54f..b7e9b60 100644 --- a/apps/backend/chatballs/ai/tests.py +++ b/apps/backend/chatballs/ai/tests.py @@ -16,7 +16,7 @@ _MEDIA_ROOT = tempfile.mkdtemp(prefix="hub-test-media-") def make_channel_with_agent(organization, *, code, name, model="openai/gpt-4o-mini"): - """Канал обработки + его агент (ADR-HUB-0019/0023). Bootstrap не создаёт + """Канал обработки + его агент (ADR-CHATBALLS-0023). Bootstrap не создаёт каналы/агентов — в тестах их собирает этот helper.""" channel = Channel.objects.create(organization=organization, code=code, name=name) agent = AIAgent.objects.create( diff --git a/apps/backend/chatballs/channels/migrations/0005_enforce_policy_invariants.py b/apps/backend/chatballs/channels/migrations/0005_enforce_policy_invariants.py index 6aa2853..7d2f4bc 100644 --- a/apps/backend/chatballs/channels/migrations/0005_enforce_policy_invariants.py +++ b/apps/backend/chatballs/channels/migrations/0005_enforce_policy_invariants.py @@ -1,4 +1,4 @@ -# SPEC-HUB-0027 §3.2/§12, ADR-HUB-0037 §7 — этап 3. +# Проверка инвариантов политики канала. # # Приводит существующие каналы в соответствие P1-P2 и меняет дефолты модели, # которые сами по себе их нарушали: attribution и checkout были включены по diff --git a/apps/backend/chatballs/channels/models.py b/apps/backend/chatballs/channels/models.py index 831ff1b..6f3d848 100644 --- a/apps/backend/chatballs/channels/models.py +++ b/apps/backend/chatballs/channels/models.py @@ -1,6 +1,6 @@ from django.db import models -# Канал обработки — якорь AI-контекста (ADR-HUB-0019). Группа видимости и +# Канал обработки — якорь AI-контекста. Группа видимости и # ссылка на провайдер-интеграцию. Поведение AI (модель, инструкции, знания) # живёт на агенте канала (ADR-CHATBALLS-0023). diff --git a/apps/backend/chatballs/channels/runtime.py b/apps/backend/chatballs/channels/runtime.py index 1b3ea34..ab5a215 100644 --- a/apps/backend/chatballs/channels/runtime.py +++ b/apps/backend/chatballs/channels/runtime.py @@ -4,7 +4,7 @@ from chatballs.i18n import t def run_channel_turn(*, channel, message: str, history: list[dict] | None = None) -> ChatResult: - """One AI turn for a processing channel (ADR-HUB-0019/0023). + """One AI turn for a processing channel (ADR-CHATBALLS-0023). AI-поведение канала целиком определяет его агент. Без активного агента AI-ответа нет: вызывающий код (ingest/support) обрабатывает ProviderError diff --git a/apps/backend/chatballs/channels/selectors.py b/apps/backend/chatballs/channels/selectors.py index 892767a..88a0249 100644 --- a/apps/backend/chatballs/channels/selectors.py +++ b/apps/backend/chatballs/channels/selectors.py @@ -13,8 +13,7 @@ def _with_relations(queryset: QuerySet[Channel]) -> QuerySet[Channel]: ).prefetch_related( Prefetch("connections", queryset=Integration.objects.order_by("id")) ).annotate( - # Один агрегат на весь список: N+1 запросов на счётчик не допускается - # (SPEC-HUB-0027 §6.1). + # Один агрегат на весь список: N+1 запросов на счётчик не допускается. open_conversations_count=Count( "conversations", filter=Q(conversations__lifecycle=LifecycleState.OPEN), diff --git a/apps/backend/chatballs/channels/services.py b/apps/backend/chatballs/channels/services.py index 71db150..29f7f0c 100644 --- a/apps/backend/chatballs/channels/services.py +++ b/apps/backend/chatballs/channels/services.py @@ -1,4 +1,4 @@ -"""Операции над каналом обработки (SPEC-HUB-0027 §6, §7).""" +"""Операции над каналом обработки.""" from __future__ import annotations @@ -42,7 +42,7 @@ class ChannelHasReferences(Exception): class ConnectionAlreadyBound(Exception): - """Подключение принадлежит ровно одному каналу (ADR-HUB-0019).""" + """Подключение принадлежит ровно одному каналу.""" def __init__(self, *, integration: Integration) -> None: self.integration = integration diff --git a/apps/backend/chatballs/conversations/models.py b/apps/backend/chatballs/conversations/models.py index 28404fc..d63521a 100644 --- a/apps/backend/chatballs/conversations/models.py +++ b/apps/backend/chatballs/conversations/models.py @@ -7,7 +7,7 @@ from django.utils import timezone from chatballs.i18n import t from chatballs.tenancy.models import TenantRelationModel -# Минимальный домен диалогов (ADR-HUB-0001/0002/0003/0006). Состояние диалога +# Минимальный домен диалогов (ADR-CHATBALLS-0002/0003/0006). Состояние диалога # разделено на независимые оси; перехват оператором — атомарный. diff --git a/apps/backend/chatballs/conversations/test_vk_transport.py b/apps/backend/chatballs/conversations/test_vk_transport.py index cb50dfe..a9e52df 100644 --- a/apps/backend/chatballs/conversations/test_vk_transport.py +++ b/apps/backend/chatballs/conversations/test_vk_transport.py @@ -267,10 +267,10 @@ class VkOutboundTests(SimpleTestCase): with mock.patch.object(vk_send.vk_api, "call", api): with mock.patch.object(vk_send, "customer_language", return_value="ru"): sent = vk_send.send_call_invite( - _integration(), chat_id="77", user_id="77", text="Звонок", url="https://hub.test/calls/abc" + _integration(), chat_id="77", user_id="77", text="Звонок", url="https://chatballs.test/calls/abc" ) self.assertTrue(sent) - self.assertIn("https://hub.test/calls/abc", api.params("messages.send")["keyboard"]) + self.assertIn("https://chatballs.test/calls/abc", api.params("messages.send")["keyboard"]) def test_rejected_send_is_reported_as_failure(self) -> None: api = _Api(**{"messages.send": VkRejected("ВКонтакте отклонил запрос (7): access denied")}) diff --git a/apps/backend/chatballs/conversations/transports/base.py b/apps/backend/chatballs/conversations/transports/base.py index cc03b12..41edb60 100644 --- a/apps/backend/chatballs/conversations/transports/base.py +++ b/apps/backend/chatballs/conversations/transports/base.py @@ -119,7 +119,7 @@ def multipart_body( """Ручной multipart/form-data (в репо urllib, без requests).""" import uuid - boundary = "----hub" + uuid.uuid4().hex + boundary = "----chatballs" + uuid.uuid4().hex crlf = "\r\n" parts: list[bytes] = [] for name, value in fields.items(): diff --git a/apps/backend/chatballs/events/management/commands/run_worker.py b/apps/backend/chatballs/events/management/commands/run_worker.py index cd991e5..5c09cbe 100644 --- a/apps/backend/chatballs/events/management/commands/run_worker.py +++ b/apps/backend/chatballs/events/management/commands/run_worker.py @@ -110,7 +110,7 @@ class Command(BaseCommand): role = str(options["role"]) does_events = role in (ROLE_ALL, ROLE_EVENTS) does_polling = role in (ROLE_ALL, ROLE_POLLER) - self.stdout.write(f"Hub worker started (role={role})") + self.stdout.write(f"Chatballs worker started (role={role})") last_poll = 0.0 last_maintenance = 0.0 last_call_sweep = 0.0 diff --git a/apps/backend/chatballs/identity/auth/invitations.py b/apps/backend/chatballs/identity/auth/invitations.py index f48b938..2066001 100644 --- a/apps/backend/chatballs/identity/auth/invitations.py +++ b/apps/backend/chatballs/identity/auth/invitations.py @@ -81,7 +81,7 @@ class InvitationRegisterView(APIView): class InvitationAcceptView(APIView): - """Accept an organization invitation: OWNER (SPEC-HUB-0021 §8.2) or employee. + """Accept an organization invitation: OWNER or employee. Authenticated endpoint: the caller must already have a HumanUser account (created through sign-up / password setup). The token is read from the body; diff --git a/apps/backend/chatballs/identity/bootstrap.py b/apps/backend/chatballs/identity/bootstrap.py index b6458b6..26cc54d 100644 --- a/apps/backend/chatballs/identity/bootstrap.py +++ b/apps/backend/chatballs/identity/bootstrap.py @@ -41,7 +41,7 @@ def bootstrap_owner(*, email: str, password: str, full_name: str = "") -> Bootst support_group, _ = EmployeeGroup.objects.get_or_create( organization=organization, name="Поддержка" ) - # Каналы обработки и их агенты (ADR-HUB-0019) создаются через API каналов. + # Каналы обработки и их агенты создаются через API каналов. owner, created_owner = HumanUser.objects.get_or_create( email=HumanUser.objects.normalize_email(email), diff --git a/apps/backend/chatballs/identity/invitation_service.py b/apps/backend/chatballs/identity/invitation_service.py index ac2d538..7a1c21f 100644 --- a/apps/backend/chatballs/identity/invitation_service.py +++ b/apps/backend/chatballs/identity/invitation_service.py @@ -30,7 +30,7 @@ from chatballs.tenancy.ingress import invitation_route # Приглашение существующего пользователя в организацию: письмо отправляет # воркер по этому событию (identity.event_handlers). MEMBERSHIP_INVITATION_REQUESTED = "identity.membership_invitation_requested" -# Приглашение владельца из платформенного провижининга (SPEC-HUB-0021 §8.2): +# Приглашение владельца из платформенного провижининга: # учётной записи может ещё не быть, тогда человек создаёт её по ссылке. OWNER_INVITATION_REQUESTED = "organization.owner_invitation_requested" MEMBERSHIP_INVITATION_TTL = timedelta(days=7) @@ -240,7 +240,7 @@ def _invitation_for_token(token: str, *, accepted: bool) -> OrganizationInvitati @transaction.atomic def accept_invitation(*, token: str, user: HumanUser) -> AcceptedInvitation: - """Принять приглашение: владельца (SPEC-HUB-0021 §8.2) или сотрудника. + """Принять приглашение: владельца или сотрудника. Членство собирается из полей приглашения; для владельца организация ещё и активируется. Идемпотентно: повторное принятие того же токена не создаёт @@ -337,7 +337,7 @@ def _already_accepted_for( token: str, user: HumanUser ) -> AcceptedInvitation | None: """Idempotent re-accept: if this token was already accepted by the same user, - return the existing result instead of raising (SPEC-HUB-0021 §11/§15).""" + return the existing result instead of raising.""" invitation = _invitation_for_token(token, accepted=True) if invitation is None: return None diff --git a/apps/backend/chatballs/identity/migrations/0007_employee_roles_position_department.py b/apps/backend/chatballs/identity/migrations/0007_employee_roles_position_department.py index 2fd5e65..c3b98d8 100644 --- a/apps/backend/chatballs/identity/migrations/0007_employee_roles_position_department.py +++ b/apps/backend/chatballs/identity/migrations/0007_employee_roles_position_department.py @@ -1,4 +1,4 @@ -# ADR-HUB-0027 / SPEC-HUB-0018 фаза M1-M2 (этап 1): additive schema + backfill. +# Роли, должности и отделы сотрудников: additive schema + backfill. # Роли OWNER/ADMIN/EMPLOYEE, обязательная должность (пока nullable), основной отдел # и размещение владельца на уровне компании. Доступ существующих сотрудников не # расширяется: OPERATOR → EMPLOYEE, права сохраняются compatibility-адаптером. @@ -8,7 +8,7 @@ from django.db import migrations, models def operator_to_employee(apps, schema_editor): EmployeeProfile = apps.get_model("identity", "EmployeeProfile") - # OPERATOR больше не является системной ролью (ADR-HUB-0027). + # OPERATOR больше не является системной ролью. EmployeeProfile.objects.filter(role="OPERATOR").update(role="EMPLOYEE") # Владелец всегда на уровне компании: снимаем основной отдел. EmployeeProfile.objects.filter(role="OWNER").exclude(primary_department__isnull=True).update( diff --git a/apps/backend/chatballs/identity/migrations/0008_enforce_position_title.py b/apps/backend/chatballs/identity/migrations/0008_enforce_position_title.py index 3514b93..a18e2f2 100644 --- a/apps/backend/chatballs/identity/migrations/0008_enforce_position_title.py +++ b/apps/backend/chatballs/identity/migrations/0008_enforce_position_title.py @@ -1,4 +1,4 @@ -# ADR-HUB-0027 / SPEC-HUB-0018 фаза M2 (этап 1): после явного заполнения должностей +# После явного заполнения должностей # включается обязательность. Оставшиеся незаполненные значения нормализуются в ""; # непустая должность гарантируется application contract (SPEC-CHATBALLS-0016 §5). from django.db import migrations, models diff --git a/apps/backend/chatballs/identity/migrations/0015_organization_status.py b/apps/backend/chatballs/identity/migrations/0015_organization_status.py index d34fa4d..d3919fd 100644 --- a/apps/backend/chatballs/identity/migrations/0015_organization_status.py +++ b/apps/backend/chatballs/identity/migrations/0015_organization_status.py @@ -1,4 +1,4 @@ -# Generated for C06 tenant provisioning (SPEC-HUB-0021 §6/§8). +# Generated for tenant provisioning. # Adds Organization.status and backfills existing organizations to ACTIVE. from django.db import migrations, models diff --git a/apps/backend/chatballs/identity/migrations/0016_channel_capabilities.py b/apps/backend/chatballs/identity/migrations/0016_channel_capabilities.py index 19cfbc0..326cdfa 100644 --- a/apps/backend/chatballs/identity/migrations/0016_channel_capabilities.py +++ b/apps/backend/chatballs/identity/migrations/0016_channel_capabilities.py @@ -1,4 +1,4 @@ -# SPEC-HUB-0027 §5.1/§5.3, ADR-HUB-0037 §9 — этап 1. +# Права на управление каналами. # # Вводит `channels.view` / `channels.manage` и выдаёт их существующим профилям # доступа по текущим `ai.view` / `ai.manage`, чтобы никто не потерял доступ в diff --git a/apps/backend/chatballs/identity/models.py b/apps/backend/chatballs/identity/models.py index 52b0b00..e75c00a 100644 --- a/apps/backend/chatballs/identity/models.py +++ b/apps/backend/chatballs/identity/models.py @@ -102,7 +102,7 @@ class HumanUser(AbstractUser): return self.email class OrganizationStatus(models.TextChoices): - # SPEC-HUB-0021 §6/§8: PENDING_OWNER до принятия OWNER invitation, ACTIVE после. + # PENDING_OWNER до принятия OWNER invitation, ACTIVE после. ACTIVE = "ACTIVE", "Active" PENDING_OWNER = "PENDING_OWNER", "Pending owner" diff --git a/apps/backend/chatballs/identity/setup.py b/apps/backend/chatballs/identity/setup.py index 34fd52d..86c5518 100644 --- a/apps/backend/chatballs/identity/setup.py +++ b/apps/backend/chatballs/identity/setup.py @@ -7,7 +7,7 @@ Запись идёт по основному соединению процесса. Роль app вправе вставить организацию только пока их нет (политика tenancy/0032); дальше создавать -организации может только роль platform (SPEC-HUB-0021 §10). Так пароль +организации может только роль platform. Так пароль platform-роли не нужен процессу backend-app. """ diff --git a/apps/backend/chatballs/identity/tests.py b/apps/backend/chatballs/identity/tests.py index 9dd3b09..9902efb 100644 --- a/apps/backend/chatballs/identity/tests.py +++ b/apps/backend/chatballs/identity/tests.py @@ -770,7 +770,7 @@ class ThrottlingTests(TestCase): class EmployeeModelInvariantTests(TestCase): - """ADR-HUB-0027 / SPEC-CHATBALLS-0016 §5,§7 — инварианты модели сотрудника после + """SPEC-CHATBALLS-0016 §5,§7 — инварианты модели сотрудника после миграции этапа 1: роли OWNER/ADMIN/EMPLOYEE, обязательная должность, размещение владельца на уровне компании и ровно один владелец на организацию.""" @@ -823,7 +823,7 @@ class EmployeeModelInvariantTests(TestCase): class EmployeeGovernanceTests(TestCase): - """ADR-HUB-0027 этап 2 / SPEC-CHATBALLS-0016 §8,§12: административная иерархия + """SPEC-CHATBALLS-0016 §8,§12: административная иерархия OWNER/ADMIN/EMPLOYEE, target-aware управление и передача владения.""" def setUp(self) -> None: diff --git a/apps/backend/chatballs/integrations/models.py b/apps/backend/chatballs/integrations/models.py index 014a78d..a38e805 100644 --- a/apps/backend/chatballs/integrations/models.py +++ b/apps/backend/chatballs/integrations/models.py @@ -3,7 +3,7 @@ from django.db import models from chatballs.identity.crypto import EncryptedCharField # Интеграции: провайдеры (LLM) и подключения (боты/виджеты). ADR-CHATBALLS-0020. -# Привязка подключения к каналу обработки появляется в M1 (ADR-HUB-0019). +# Привязка подключения к каналу обработки появляется в M1. class IntegrationKind(models.TextChoices): @@ -63,7 +63,7 @@ class Integration(models.Model): voice_messages_enabled = models.BooleanField(default=True) audio_calls_enabled = models.BooleanField(default=True) video_calls_enabled = models.BooleanField(default=True) - # Подключение (бот/виджет) привязано к каналу обработки (ADR-HUB-0019, M2). + # Подключение (бот/виджет) привязано к каналу обработки. channel = models.ForeignKey("channels.Channel", on_delete=models.SET_NULL, null=True, blank=True, related_name="connections") # Курсор Long Polling (marker MAX / offset Telegram). poll_marker = models.CharField(max_length=64, blank=True) diff --git a/apps/backend/chatballs/integrations/services.py b/apps/backend/chatballs/integrations/services.py index a0b6199..28b1271 100644 --- a/apps/backend/chatballs/integrations/services.py +++ b/apps/backend/chatballs/integrations/services.py @@ -30,7 +30,7 @@ class IntegrationInput: name: str secret: str | None = None # None = не менять при update config: dict = field(default_factory=dict) - channel_id: int | None = None # канал обработки для подключения (ADR-HUB-0019) + channel_id: int | None = None # канал обработки для подключения is_active: bool | None = None @@ -120,7 +120,7 @@ def _normalized_config(provider: str, config: dict) -> dict: except OutboundUrlRejected as error: raise ValidationError({"config": t("settings.proxy_url_rejected", error=error)}) from error # LLM-провайдеры (OpenRouter, Custom) хранят модель по умолчанию свободным текстом. - # Для OpenRouter поле исторически декоративно (SPEC-HUB-0005:388); для Custom оно + # Для OpenRouter поле исторически декоративно; для Custom оно # читается в рантайме (ADR-CHATBALLS-0034 §4). Версионирование модели — дорожка ADR-0034. if provider in (IntegrationProvider.OPENROUTER, IntegrationProvider.CUSTOM, IntegrationProvider.DEMO): default_model = str(config.get("defaultModel", config.get("default_model", ""))).strip() diff --git a/apps/backend/chatballs/integrations/tests.py b/apps/backend/chatballs/integrations/tests.py index 357bfad..4e97b84 100644 --- a/apps/backend/chatballs/integrations/tests.py +++ b/apps/backend/chatballs/integrations/tests.py @@ -510,7 +510,7 @@ class OpenRouterProviderProxyTests(TestCase): provider = OpenRouterProvider(api_key="sk-test", base_url="https://openrouter.ai/api/v1", proxy_url="http://proxy:8080") - # После рефакторинга общий HTTP-слой живёт в openai_http (ADR-HUB-0033 §7): + # После рефакторинга общий HTTP-слой живёт в openai_http: # мокаем именно его build_opener. diff --git a/apps/backend/chatballs/platform/payloads.py b/apps/backend/chatballs/platform/payloads.py index b5fcab1..1e5f2d7 100644 --- a/apps/backend/chatballs/platform/payloads.py +++ b/apps/backend/chatballs/platform/payloads.py @@ -16,7 +16,7 @@ def provisioning_result_payload( owner_state: str, ) -> dict[str, Any]: """Response shape for POST /api/v1/organizations. Never includes secrets, - tokens or invitation plaintext (SPEC-HUB-0021 §12/§13).""" + tokens or invitation plaintext.""" return { "organization": { "publicId": str(organization.public_id), diff --git a/apps/backend/chatballs/platform/provisioning_command.py b/apps/backend/chatballs/platform/provisioning_command.py index df6e479..725639d 100644 --- a/apps/backend/chatballs/platform/provisioning_command.py +++ b/apps/backend/chatballs/platform/provisioning_command.py @@ -26,7 +26,7 @@ class ProvisioningCommand: tax_profile: dict[str, Any] | None = None def significant_fields(self) -> dict[str, Any]: - """Canonical normalized values for request_hash (SPEC-HUB-0021 §11): + """Canonical normalized values for request_hash: transport metadata is excluded. Keys are sorted for a stable hash.""" data = asdict(self) data["owner_email"] = data["owner_email"].strip().lower() @@ -53,7 +53,7 @@ class ProvisioningResult: def is_replay(provisioning: OrganizationProvisioning, command: ProvisioningCommand) -> bool: """COMPLETED/WAITING_FOR_OWNER with a matching request hash is an idempotent - replay; a differing hash is a conflict (SPEC-HUB-0021 §11).""" + replay; a differing hash is a conflict.""" return provisioning.request_hash == command.request_hash() diff --git a/apps/backend/chatballs/platform/provisioning_models.py b/apps/backend/chatballs/platform/provisioning_models.py index 13437eb..66c0fa4 100644 --- a/apps/backend/chatballs/platform/provisioning_models.py +++ b/apps/backend/chatballs/platform/provisioning_models.py @@ -21,10 +21,10 @@ class ProvisioningStatus(models.TextChoices): class OrganizationProvisioning(models.Model): - """Platform-owned process record (SPEC-HUB-0021 §6). Idempotency key is + """Platform-owned process record. Idempotency key is globally unique; the same key + payload returns the existing result, while a differing payload is a conflict. failure_details_safe must never contain - secrets, tokens or provider credentials (SPEC-HUB-0021 §6/§13).""" + secrets, tokens or provider credentials.""" idempotency_key = models.CharField(max_length=160, unique=True) request_hash = models.CharField(max_length=128) diff --git a/apps/backend/chatballs/platform/provisioning_service.py b/apps/backend/chatballs/platform/provisioning_service.py index e789c7b..7b2b183 100644 --- a/apps/backend/chatballs/platform/provisioning_service.py +++ b/apps/backend/chatballs/platform/provisioning_service.py @@ -46,7 +46,7 @@ def provision_organization( command: ProvisioningCommand, operator: PlatformOperator, ) -> ProvisioningResult: - """Single write boundary for tenant provisioning (SPEC-HUB-0021 §4). + """Single write boundary for tenant provisioning. Coordinates identity, audit and outbox in one transaction (тариф удалён, ADR-CHATBALLS-0042 §2: организация создаётся без подписки). Tenant-owned rows are written under set_local_tenant(new_org.id) @@ -201,7 +201,7 @@ def _provision_pending_owner( command: ProvisioningCommand, context: TenantContext, ) -> None: - # No password/dummy user is created (SPEC-HUB-0021 §8.2). An OWNER invitation + # No password/dummy user is created. An OWNER invitation # is issued; the organization stays PENDING_OWNER until it is accepted. issued = issue_invitation( organization=org, diff --git a/apps/backend/chatballs/platform/validation.py b/apps/backend/chatballs/platform/validation.py index 38a0ea0..0caba9d 100644 --- a/apps/backend/chatballs/platform/validation.py +++ b/apps/backend/chatballs/platform/validation.py @@ -16,7 +16,7 @@ _REQUIRED_FIELDS = ( def parse_provisioning_body( body: Any, *, idempotency_key: str, source: str ) -> tuple[ProvisioningCommand, str | None]: - """Map the API body (SPEC-HUB-0021 §12) to a ProvisioningCommand. Returns + """Map the API body to a ProvisioningCommand. Returns (command, error_message). Validation is intentionally explicit (no DRF serializers), mirroring identity/access_payloads.py.""" if not isinstance(body, dict): diff --git a/apps/backend/chatballs/platform/views.py b/apps/backend/chatballs/platform/views.py index cefb6a2..652d491 100644 --- a/apps/backend/chatballs/platform/views.py +++ b/apps/backend/chatballs/platform/views.py @@ -19,7 +19,7 @@ _IDEMPOTENCY_HEADER = "Idempotency-Key" class OrganizationProvisionView(APIView): - """POST /api/v1/organizations (SPEC-HUB-0021 §12). + """POST /api/v1/organizations. The first Platform API adapter: machine-to-machine provisioning via an opaque platform token. Does not reuse tenant HasCapability (no membership diff --git a/apps/backend/chatballs/tenancy/migrations/0005_platform_provisioning_grants.py b/apps/backend/chatballs/tenancy/migrations/0005_platform_provisioning_grants.py index ffc4557..bd5ef1d 100644 --- a/apps/backend/chatballs/tenancy/migrations/0005_platform_provisioning_grants.py +++ b/apps/backend/chatballs/tenancy/migrations/0005_platform_provisioning_grants.py @@ -1,4 +1,4 @@ -# C06 tenant provisioning DB boundary (SPEC-HUB-0021 §10, ADR-CHATBALLS-0029 §6). +# C06 tenant provisioning DB boundary (ADR-CHATBALLS-0029 §6). # # Provisioning runs on the platform connection (chatballs_runtime_platform) and must # both INSERT a new Organization and create tenant-owned rows (departments, the @@ -27,7 +27,7 @@ PLATFORM_POLICY = "chatballs_platform_tenant_provisioning" def apply_grants(apps, schema_editor): # 1. Platform role may create and update organizations (no RLS on this table; # it has no organization_id column). DELETE stays with the schema/migration - # role per SPEC-HUB-0021 §14 (hard delete as compensation is forbidden). + # role (hard delete as compensation is forbidden). schema_editor.execute( "GRANT INSERT, UPDATE ON identity_organization TO chatballs_runtime_platform" ) diff --git a/apps/backend/chatballs/tenancy/migrations/0022_demo_registry_guards.py b/apps/backend/chatballs/tenancy/migrations/0022_demo_registry_guards.py index b7a87bb..f638491 100644 --- a/apps/backend/chatballs/tenancy/migrations/0022_demo_registry_guards.py +++ b/apps/backend/chatballs/tenancy/migrations/0022_demo_registry_guards.py @@ -1,7 +1,7 @@ # Реестр демо-данных (identity_demodataset, identity_demorecord): tenant-таблицы # с organization_id, RLS и гранты по образцу tenancy/0021. Платформенная роль # получает те же права: мастер первого запуска ставит демо-набор в очередь на -# соединении platform (SPEC-HUB-0021 §10). +# соединении platform. from django.db import migrations TABLES = ("identity_demodataset", "identity_demorecord") diff --git a/apps/backend/chatballs/tenancy/migrations/0031_platform_provisioning_table_grants.py b/apps/backend/chatballs/tenancy/migrations/0031_platform_provisioning_table_grants.py index a28c697..481be09 100644 --- a/apps/backend/chatballs/tenancy/migrations/0031_platform_provisioning_table_grants.py +++ b/apps/backend/chatballs/tenancy/migrations/0031_platform_provisioning_table_grants.py @@ -2,7 +2,7 @@ # после tenancy/0003, и ни одна миграция не выдала на них прав runtime-роли # platform. В деплое backend-platform работает ролью chatballs_platform, поэтому # уже проверка токена падала «permission denied for table platform_platformtoken», -# а вместе с ней — весь POST /api/v1/organizations (SPEC-HUB-0021 §12). Тесты +# а вместе с ней — весь POST /api/v1/organizations. Тесты # этого не видели: они ходят в базу владельцем кластера. # # Здесь роль platform получает DML на платформенные таблицы (без DELETE: токены diff --git a/apps/backend/chatballs/tenancy/migrations/0032_app_role_ingress_and_bootstrap.py b/apps/backend/chatballs/tenancy/migrations/0032_app_role_ingress_and_bootstrap.py index 7c5522f..6251043 100644 --- a/apps/backend/chatballs/tenancy/migrations/0032_app_role_ingress_and_bootstrap.py +++ b/apps/backend/chatballs/tenancy/migrations/0032_app_role_ingress_and_bootstrap.py @@ -10,7 +10,7 @@ # нет»: проверку делает SECURITY DEFINER-функция от chatballs_schema (иначе # политика ссылалась бы на свою же таблицу и Postgres отказал бы за рекурсию). # После первой организации INSERT для app закрыт навсегда — создавать следующие -# по-прежнему может только роль platform (SPEC-HUB-0021 §10). +# по-прежнему может только роль platform. from django.db import migrations INGRESS_VIEWS = ( diff --git a/apps/backend/chatballs/tenancy/models.py b/apps/backend/chatballs/tenancy/models.py index 058bb61..8c0e91f 100644 --- a/apps/backend/chatballs/tenancy/models.py +++ b/apps/backend/chatballs/tenancy/models.py @@ -57,7 +57,7 @@ class OrganizationStorageUsage(models.Model): """Authoritative storage_bytes usage counter for one organization. ``bytes_used`` is the committed total; ``reserved_bytes`` covers in-flight - uploads whose final size is not yet known (SPEC-HUB-0022 §10 reserve/finalize). + uploads whose final size is not yet known (reserve/finalize). The effective usage against the quota is ``bytes_used + reserved_bytes``. """ @@ -88,7 +88,7 @@ class OrganizationStorageUsage(models.Model): class StorageReservation(models.Model): """An in-flight storage_bytes reservation keyed by an idempotency token, so a - multi-step upload (SPEC-HUB-0022 §10) can reserve the expected size, finalize + multi-step upload can reserve the expected size, finalize the actual size once the object is persisted, and release the reservation on failure. A single reservation tracks one upload lifecycle. """ diff --git a/apps/backend/chatballs/tenancy/routing.py b/apps/backend/chatballs/tenancy/routing.py index be23d42..1323a80 100644 --- a/apps/backend/chatballs/tenancy/routing.py +++ b/apps/backend/chatballs/tenancy/routing.py @@ -1,7 +1,7 @@ """Принудительный выбор соединения БД для блока кода. -Алиас ``platform`` (роль platform, права на создание организаций — -SPEC-HUB-0021) есть только у процессов, которым он нужен: у платформенной +Алиас ``platform`` (роль platform, права на создание +организаций) есть только у процессов, которым он нужен: у платформенной поверхности и у воркера, захватывающего outbox всех организаций (см. ``settings_database.build_databases``). У backend-app его нет: каталоги входа и мастер первого запуска работают ролью app (tenancy/0032). diff --git a/apps/backend/chatballs/tenancy/test_storage_quota.py b/apps/backend/chatballs/tenancy/test_storage_quota.py index 6cb8a3e..6f6ebf8 100644 --- a/apps/backend/chatballs/tenancy/test_storage_quota.py +++ b/apps/backend/chatballs/tenancy/test_storage_quota.py @@ -13,7 +13,7 @@ from chatballs.testing import system_tenant_context class StorageReserveFinalizeReleaseTests(TestCase): - """SPEC-HUB-0022 §10: reserve/finalize/release — идемпотентный технический + """Reserve/finalize/release — идемпотентный технический учёт занятого места. Лимитов больше нет (ADR-CHATBALLS-0042 §2): сервис никогда не отказывает по квоте.""" diff --git a/apps/backend/chatballs_backend/settings_base.py b/apps/backend/chatballs_backend/settings_base.py index a2e51eb..e692a9b 100644 --- a/apps/backend/chatballs_backend/settings_base.py +++ b/apps/backend/chatballs_backend/settings_base.py @@ -208,9 +208,6 @@ CHATBALLS_AI_TURN_DEADLINE_SECONDS = int( ) # Модель расшифровки голосовых (OpenAI-совместимый /audio/transcriptions). -# Managed-провайдер CustoAI удалён (ADR-CHATBALLS-0042 §3): AI — только через -# интеграцию организации (BYOK). - # Long-poll hold-time мессенджеров (сек). Держим малым: опрос идёт по # подключениям последовательно в одном процессе, и hold-time каждого из них # складывается в задержку приёма у остальных. Ответы AI от этого больше не @@ -267,7 +264,7 @@ CHATBALLS_STORAGE_BACKEND, MEDIA_ROOT, STORAGES = build_storage_settings( testing=TESTING, ) -# Публичный адрес Hub: абсолютные ссылки, уходящие клиентам (download вложений). +# Публичный адрес установки: абсолютные ссылки, уходящие клиентам (download вложений). CHATBALLS_PUBLIC_BASE_URL = os.environ.get("CHATBALLS_PUBLIC_BASE_URL", "http://localhost:8000") # Публичные порталы поддержки размещаются на отдельных хостах. Базовый домен @@ -323,7 +320,7 @@ if ( or CHATBALLS_CALL_CONNECT_GRACE_SECONDS <= 0 or CHATBALLS_CALL_RECONNECT_GRACE_SECONDS <= 0 ): - raise ImproperlyConfigured("HUB call token TTL values must be positive") + raise ImproperlyConfigured("Call token TTL values must be positive") # ICE-серверы для WebRTC (SPEC-CHATBALLS-0013 §10): direct-first через STUN, TURN как # fallback. Формат URL через запятую (stun:host:port / turn:host:3478?transport=udp). diff --git a/apps/internal-ui/src/features/agents/styles.css b/apps/internal-ui/src/features/agents/styles.css index 8e692c7..3c36755 100644 --- a/apps/internal-ui/src/features/agents/styles.css +++ b/apps/internal-ui/src/features/agents/styles.css @@ -633,7 +633,7 @@ background: var(--n-9); } -/* Блокер «провайдер не выбран» (кадр G5, ADR-HUB-0017). */ +/* Блокер «провайдер не выбран» (кадр G5). */ .agent-blocker { display: flex; diff --git a/apps/internal-ui/src/features/conversations/ConversationWorkspace.tsx b/apps/internal-ui/src/features/conversations/ConversationWorkspace.tsx index 576fe0f..93aa87a 100644 --- a/apps/internal-ui/src/features/conversations/ConversationWorkspace.tsx +++ b/apps/internal-ui/src/features/conversations/ConversationWorkspace.tsx @@ -53,7 +53,7 @@ import { useDialogKeyboardNav } from "./useDialogKeyboardNav"; import { useIncomingMessageSound } from "./useIncomingMessageSound"; import { t } from "../../i18n"; -// Общий workspace диалогов (SPEC-HUB-0010 §8.2). Видимость inbox решает +// Общий workspace диалогов. Видимость inbox решает // backend по группам (ADR-CHATBALLS-0043); страница параметризуется заголовком, // placeholder поиска и правой панелью через render-prop. Список и история — // серверные окна: ни то, ни другое целиком не запрашивается. diff --git a/apps/internal-ui/src/features/conversations/types.ts b/apps/internal-ui/src/features/conversations/types.ts index 6cd0199..712712d 100644 --- a/apps/internal-ui/src/features/conversations/types.ts +++ b/apps/internal-ui/src/features/conversations/types.ts @@ -1,4 +1,4 @@ -// Общие типы workspace диалогов (sales + support). SPEC-HUB-0010 §8.2: +// Общие типы workspace диалогов (sales + support): // общий conversation workspace, не отдельная реализация под каждый отдел. export type DialogMode = "ai" | "closed" | "operator" | "wait"; diff --git a/apps/internal-ui/src/features/integrations/model.test.ts b/apps/internal-ui/src/features/integrations/model.test.ts index c238372..788a206 100644 --- a/apps/internal-ui/src/features/integrations/model.test.ts +++ b/apps/internal-ui/src/features/integrations/model.test.ts @@ -10,11 +10,11 @@ describe("webWidgetSnippet", () => { }); it("builds the embed snippet from the current origin and channel code", async () => { - vi.stubGlobal("window", { location: { origin: "https://hub.example.com" } }); + vi.stubGlobal("window", { location: { origin: "https://chatballs.example.com" } }); const { webWidgetSnippet } = await import("./model"); expect(webWidgetSnippet("wgt_demo")).toBe( - ``, + ``, ); }); @@ -28,11 +28,11 @@ describe("webWidgetSnippet", () => { }); it("uses the origin verbatim with a custom port", async () => { - vi.stubGlobal("window", { location: { origin: "https://hub.example.com:8443" } }); + vi.stubGlobal("window", { location: { origin: "https://chatballs.example.com:8443" } }); const { webWidgetSnippet } = await import("./model"); expect(webWidgetSnippet("wgt_demo")).toBe( - ``, + ``, ); }); }); diff --git a/apps/internal-ui/src/features/integrations/model.ts b/apps/internal-ui/src/features/integrations/model.ts index 27608b0..2b96b18 100644 --- a/apps/internal-ui/src/features/integrations/model.ts +++ b/apps/internal-ui/src/features/integrations/model.ts @@ -109,7 +109,7 @@ export const fetchLlmProviders = () => response.items.filter((item) => item.kind === "LLM_PROVIDER") ); -// Разрешённые домены Web-виджета (SPEC-HUB-0010 §7.1). Пустой список в проде +// Разрешённые домены Web-виджета. Пустой список в проде // запрещает все origin'ы, поэтому домены вводятся руками и обязательны. // Принимаем три формы, которые понимает backend (webchat.services.origin_allowed): // `example.com`, `*.example.com` и `https://example.com:8443`; `localhost` — для разработки. @@ -135,7 +135,7 @@ export const formatAllowedOrigins = (origins: string[]): string => origins.join( export const invalidAllowedOrigin = (origins: string[]): string | undefined => origins.find((item) => !ORIGIN_RULE.test(item)); -// Публичный домен Hub для встраивания Web-виджета (SPEC-CHATBALLS-0003 §3). +// Публичный домен установки для встраивания Web-виджета (SPEC-CHATBALLS-0003 §3). // Один frontend-образ работает на любом домене (ADR-CHATBALLS-0028 §runtime frontend): // сниппет генерируется от текущего origin в рантайме, а не от build-time аргумента. export function webWidgetSnippet(widgetKey: string): string { diff --git a/apps/internal-ui/src/shared/providers.ts b/apps/internal-ui/src/shared/providers.ts index d1b0550..1eca696 100644 --- a/apps/internal-ui/src/shared/providers.ts +++ b/apps/internal-ui/src/shared/providers.ts @@ -4,8 +4,8 @@ import { t } from "../i18n"; Общий словарь для всего приложения — раньше он лежал в демо-данных диалогов, и продуктовые экраны каналов зависели от файла с примерами. - Провайдеры различаются точкой и коротким лейблом на своих токенах - (ADR-HUB-0013), а не бренд-логотипами. */ + Провайдеры различаются точкой и коротким лейблом на своих токенах, + а не бренд-логотипами. */ export type ProviderKey = "EMAIL" | "MAX" | "TG" | "VK" | "WEB"; diff --git a/apps/web-chat/src/styles.css b/apps/web-chat/src/styles.css index 460b7e5..5a69118 100644 --- a/apps/web-chat/src/styles.css +++ b/apps/web-chat/src/styles.css @@ -14,8 +14,8 @@ body, На основной странице виджета (встраиваемого) эти переменные не нужны, но экран аудиозвонка из @chatballs/ui (audio-call.css) использует var(--font-sans) и радиусы, которых в web-chat нет. Значения — системный font stack Ant Design и токены - радиусов из baseline tokens/typography.css + tokens/effects.css (ADR-HUB-0013: - внешние веб-шрифты не подключаются). + радиусов из baseline tokens/typography.css + tokens/effects.css (внешние + веб-шрифты не подключаются). */ :root { --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; diff --git a/apps/web-chat/vite.config.ts b/apps/web-chat/vite.config.ts index 4a8da16..1df60bc 100644 --- a/apps/web-chat/vite.config.ts +++ b/apps/web-chat/vite.config.ts @@ -3,7 +3,7 @@ import { defineConfig, loadEnv } from "vite"; export default defineConfig(({ mode }) => ({ plugins: [react()], - // Панель отдаётся с домена hub под путём /chat/ (nginx). + // Панель отдаётся с домена установки под путём /chat/ (nginx). base: "/chat/", build: { rollupOptions: { diff --git a/chatballs b/chatballs index 5ce2c33..ea228dc 100644 --- a/chatballs +++ b/chatballs @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# chatballs — единый deployment entrypoint Chatballs (ADR-HUB-0028 §CLI). +# chatballs — единый deployment entrypoint Chatballs (ADR-CHATBALLS-0028 §CLI). # Команды этапа 1: doctor, deploy, status, logs. # (install/setup-url/update/backup/restore/rollback — этап 2.) # diff --git a/deploy/docker/frontend.Dockerfile b/deploy/docker/frontend.Dockerfile index 627b5dd..5a4b885 100644 --- a/deploy/docker/frontend.Dockerfile +++ b/deploy/docker/frontend.Dockerfile @@ -14,7 +14,7 @@ COPY apps/internal-ui ./apps/internal-ui COPY apps/web-chat ./apps/web-chat COPY packages ./packages -# Build-time домен не привязывается (ADR-HUB-0028 §10): один образ работает на любом +# Build-time домен не привязывается (ADR-CHATBALLS-0028 §10): один образ работает на любом # домене; сниппет/WS/API выводятся от текущего origin в рантайме. ARG VITE_API_BASE_URL= ENV VITE_API_BASE_URL=${VITE_API_BASE_URL} diff --git a/packages/shared/src/callRtc.ts b/packages/shared/src/callRtc.ts index 024c5dc..1456e4e 100644 --- a/packages/shared/src/callRtc.ts +++ b/packages/shared/src/callRtc.ts @@ -1,5 +1,5 @@ /** - * Клиент P2P-звонка: собственный WebSocket signaling Hub + RTCPeerConnection + * Клиент P2P-звонка: собственный WebSocket signaling + RTCPeerConnection * (SPEC-CHATBALLS-0013 §9–10). Общий для internal-ui (оператор) и web-chat (клиент). * * Роли детерминированы: offer всегда создаёт STAFF (инициатор), CUSTOMER