[feature][ui][ai-chat] Аватар-стек: спереди аватар AI-агента, за ним — аккаунт запустившего человека #300
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Проблема / мотивация
Сейчас у контента, написанного AI-агентом (комментарии, история страницы), основная аватарка — это человек, а участие агента показано маленьким фиолетовым текстовым бейджем
AI-agentрядом с именем. Иерархия неверная: действие совершил агент, а человек — тот, кто его запустил. Переворачиваем:Модель провенанса (ключ к отрисовке)
Провенанс-токен агента ВСЕГДА привязан к логину, и логин резолвится из токена (
sub) — это ровноcomment.creator. Вопрос лишь в том, ЧЬЯ это личность:apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts:73→generateAccessToken(user, sessionId, { actor:'agent', aiChatId }), гдеuser= владелец чата. Значитsub=человек,creator=человек. Коммент реально несётaiChatId(стампится черезagentSourceFields(provenance,'createdSource','aiChatId')), а у чата естьrole_id(ai-chat.service.ts:487). «Агент» здесь — это роль чата (ai_agent_roles.name+ опц.emoji), у неё нет логина и картинки.sub= аккаунт, под которым зашли (resolveMcpSessionConfig,apps/server/src/integrations/mcp/mcp-auth.helpers.ts:560). Единственный способ получить agent-метку снаружи — залогиниться как выделенныйis_agent-аккаунт (external MCP не может подсунутьactor:'agent'— этот claim ставит только сервер). Значитcreator= сам агент-аккаунт (name+avatarUrl), а отдельного человека за ним НЕТ. Наusersсвязи owner/created_by нет.Дискриминатор на клиенте — один: наличие
aiChatId(для истории —lastUpdatedAiChatId).Решение
1. Backend: нормализуем в две личности (клиент не ветвится)
На agent-контенте добавить два под-объекта. Коммент (
comment.repo.ts, рядом сwithCreator): резолв через left joincomments.aiChatId → ai_chats.role_id → ai_agent_roles (name, emoji).agent(перед)launcher(зад)aiChatId != null){ name: role.name, emoji: role.emoji, avatarUrl: null }creator(человек)ai_agent_roles.name/emojirole_id = null){ name: "AI agent", avatarUrl: null }creator(человек)aiChatId == null){ name: creator.name, avatarUrl: creator.avatarUrl }nullИмя/эмодзи из роли — это ровно то, чем приложение уже подписывает ассистента в окне чата (
ai-chat-window.tsx:882→assistantName={currentRole?.name};message-item.tsx:148→resolveAssistantName(...) ?? "AI agent"). В самих комментах/истории сейчас это НЕ выводится — фича это и добавляет.Важно: join
ai_agent_rolesдля подписи делать БЕЗ фильтраenabled/deleted_at(роль могли позже выключить/soft-delete, а подпись исторического контента должна сохраниться) — использоватьAiAgentRoleRepo.findById, НЕfindLiveEnabled.2. Backend: история страницы (тот же паттерн, другие колонки)
У истории модель иная —
lastUpdatedBy+ группаcontributors, а не одинcreator, но нормализация идентична. Вapps/server/src/database/repos/page/page-history.repo.tsдобавитьwithAgent(по аналогии сwithLastUpdatedBy/withContributors).createdSource === 'agent'lastUpdatedSource === 'agent'(вhistory-item.tsxужеisAgentEdit)aiChatIdlastUpdatedAiChatIdaiChatId → ai_chats.role_id → ai_agent_roleslastUpdatedAiChatId → ai_chats.role_id → ai_agent_rolescreatorlastUpdatedBylastUpdatedAiChatId/lastUpdatedByвIPageHistoryуже есть — не хватает только резолва роли (name/emoji).3. Frontend: компонент
AgentAvatarStack(вcomponents/ui/, один на комменты и историю)AgentGlyph— приоритет источника картинки агента:agent.avatarUrl(внешний MCP-аккаунт) →CustomAvatarс картинкой;agent.emoji(роль с эмодзи) →Avatarс эмодзи на фиолетовом кружке;IconSparklesна фиолетовом (тот же фиолет, что у нынешнего бейджа).Подпись: жирным
agent.name, при наличии — приглушённым· {launcher.name}. Текстовый бейджAI-AGENTубираем (заменён стеком); клик-переход в чат поaiChatIdпереносим на стек (логика изai-agent-badge.tsx). Tooltip: внутр. чат — «AI-агент «{роль}» от имени {человек}»; MCP — «AI-агент {имя}».История: группу
contributors(Avatar.Group) оставляем как есть;AgentAvatarStackвстаёт РОВНО на место текстового бейджа. Небольшая избыточность (lastUpdatedByможет встречаться и в contributors, и «сзади») допустима.Fallback'и:
launcher == null(MCP) → только передняя аватарка агента;createdSource !== 'agent'(иlastUpdatedSource !== 'agent') → прежнее поведение (одна аватарка человека).Принятые решения
AI-AGENT: заменить стеком (клик в чат сохранить на стеке).AiAgentBadge).launcher=null) — второго субъекта в модели нет.Затрагиваемые файлы
apps/server/src/database/repos/comment/comment.repo.ts(withAgent);apps/server/src/database/repos/page/page-history.repo.ts(withAgent); DTO/типы коммента.apps/client/src/features/comment/types/comment.types.ts(IComment.agent,IComment.launcher),apps/client/src/features/page-history/types/page.types.ts.apps/client/src/components/ui/agent-avatar-stack.tsx(+AgentGlyph); интеграция вcomment-list-item.tsx:122-140иhistory-item.tsx; удаление/сворачиваниеai-agent-badge.tsx(deep-link переносится).AgentAvatarStack(внутр. чат с ролью / без роли / MCP / non-agent); backend-тест наwithAgent(коммент и история).Критерии приёмки (DoD)
creator).aiChatId.createdSource !== 'agent'→ прежний одиночный аватар человека.agent/launcherиз подписанного провенанса (без спуфинга); роль резолвится черезfindById(не отфильтровывается поenabled/deleted_at).Вне scope (опционально, отдельные тикеты)
is_agent-аккаунт → человек-владелец (users.owner_user_id), чтобы показывать человека и за внешним MCP-агентом. Сейчас не нужно:launcher=nullчестно отражает модель.Постановка: рисовать «наоборот» — основная аватарка это AI-агент, а за ним аккаунт пользователя, который его запустил.