mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
26dc379af9
* 🐛 fix(agent-group): keep group membership lifecycle intact on transfer & delete A chat group's roster mixes two kinds of member: agents the group itself built (its synthetic supervisor, plus members made in the group builder) and standalone agents merely linked in. Only `duplicateGroup` knew the difference; delete, roster removal, group transfer and agent transfer each acted on every member alike, which cost: - deleting a group stranded its own supervisor/members forever (`agents` has no FK to `chat_groups`, and `virtual: true` hides them from every list), and the cleanup ran outside the delete's transaction - transferring an agent silently tore it out of every group, and for a supervisor left the group headless — the next read minted a blank replacement and the configured systemRole/model were gone - transferring a group dragged referenced members into another scope, and evicted them from every OTHER group they belonged to Put the judgement in one place (`resolveGroupMembershipType`) and have all four paths read it. A group transfer now leaves referenced members where they are and takes a clone instead, remapping the group's own topics/threads/messages onto it — `agent_id` is ON DELETE CASCADE, so a stale pointer would have taken the moved history with it. The async backfill applies the same remap on the same rows it rewrites. Also surfaced up front rather than after the fact: two read endpoints (`agent.getGroupMembershipImpact`, `group.listReferencedMembers`) so the UI can say what a move will do, and `role` is now a closed enum instead of free text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(test): use a role the closed set actually contains `moderator` predates `GROUP_MEMBER_ROLES` and is not a member of it, so these two cases stopped type-checking once `role` became an enum. `assistant` is the valid non-default role, so they still prove a non-default role round-trips rather than just re-asserting the default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(agent-group): gate referenced-member details on member visibility `listReferencedMembers` scoped only on the GROUP, so a caller who could see the group could read the title and avatar of a member whose owner had since flipped it back to `private` — a member the roster itself hides from them (`memberAgentVisibility`). The pre-transfer warning would have been the one surface that handed it out. Apply `agentOwnership()` as well, the same predicate `checkAgentsBeforeRemoval` and the roster reads use. A hidden member is now under-reported rather than exposed, which is the right direction: it is invisible to this caller everywhere else too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(agent-group): guard left-behind members for the drain's duration A slow-path group transfer registers the target-scope roster — members that travelled plus the clones — as the job's covered agents. The referenced members it left behind are not covered, and deliberately so: that junction is what the migration badge reads, and an agent that never moved should not advertise a migration. But being uncovered also left them unguarded. Until the drain reaches a topic, that topic's rows still carry `messages.agent_id = sourceAgentId`, and the column is ON DELETE CASCADE — so the owner deleting their own agent mid-drain silently took the moved group history with it, the very rows the remap was about to rescue. Guard the delete without touching the badge: `hasPendingRemapForSourceAgents` reads `payload.agentIdRemap` of pending transfer jobs, and `AgentModel.delete` now refuses on it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(agent-group): refuse to clone a member the caller cannot see A group transfer leaves referenced members behind and clones them into the target scope — title, systemRole, model, params and plugins — via `buildCopiedAgent`. When a member's owner had since flipped it back to `private`, that clone handed the caller a full copy of an agent the roster hides from them (`memberAgentVisibility`), and which `listReferencedMembers` had just been fixed to withhold. Moving the group was the way around both. Pre-existing rather than new — the old path MOVED that agent into the target scope outright, which is strictly worse — and only narrowed, not closed, by the owner-only guard: `buildWorkspaceWhere` constrains private rows to their author, so a workspace owner cannot see such a member either. Refuse, rather than clone a redacted shell (a member answering with a blank systemRole is a silent behavior change) or drop the member (leaving moved history pointed at an agent in the source scope, which `messages.agent_id`'s cascade turns back into data loss). The error names nobody: which member is hidden is the part being withheld. `copyToWorkspace` duplicates members the same way and gets the same guard. `duplicate` needs none — it re-links referenced members instead of copying them, so no config crosses a scope. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🔒 fix(agent-group): check member visibility on locked rows, not a snapshot Both guards ran against a read taken before the row lock, so an owner committing `visibility = 'private'` in between was checked as public and then cloned anyway — from that same stale snapshot. A narrow window, but it defeated the guard entirely when it hit. - transfer: the check moves BELOW the existing `FOR UPDATE` on the member agents, and the clone now reads the locked row rather than `memberRows` - copy: the check moves INSIDE the transaction and takes its own `FOR UPDATE` on the referenced members first, since it had no lock at all on them Once those rows are locked, a concurrent visibility update blocks until this transaction ends, so what is checked is what gets cloned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🔒 fix(agent-group): enforce single-group ownership, withhold hidden group ids Two guards that were resting on something weaker than they looked. `addAgentsToGroup` validated visibility but never rejected a `virtual` agent, so the "an owned member belongs to exactly one group" invariant that `resolveGroupMembershipType` leans on was enforced by the member picker's query rather than by the write. Sending a known group-built member or supervisor id would link it to a second group — and then deleting either group deletes an agent the other still uses. Reject it at the write. `getGroupMembershipImpact` set `groupVisible: false` for a group the caller cannot see, and withheld its title and avatar, but still returned its id — so the preflight disclosed the existence and identifier of another member's private group. Withhold the id with the rest. Done where the refs are built, so the transfer error's `groups` payload stops carrying them too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(agent-group): the invariant is one group, not zero The previous commit rejected every `virtual` agent at `addAgentsToGroup`, which breaks the group agent builder: it creates `virtual: true` and adds it through exactly this path, as that agent's first and only membership. Reject only a virtual agent already on ANOTHER group's roster — which is the actual invariant `resolveGroupMembershipType` needs, and the only case where deleting one group would destroy an agent another still lists. Adds the test that should have caught it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🔒 fix(agent-group): keep copy and transfer taking locks in the same order My own TOCTOU fix put the referenced-member `FOR UPDATE` at the top of `copyToWorkspace`'s transaction — ahead of the source-group lock. That inverted the order `transferToWorkspace` uses (group, then member agents), so a concurrent copy and transfer of the same group could each hold what the other wanted and deadlock, instead of meeting the pending-job guards that exist to serialize them. Move the agent lock below the group lock. Same protection, one order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🔒 fix(agent-group): keep builtin agents off group rosters Builtins (Inbox, the agent builders) are provisioned per user and carry `virtual: true`, exactly like a group's own members — so the membership rules classify one as group-OWNED the moment it lands on a roster. And `removeAgentsFromGroup` deletes owned members with no reserved-slug protection, unlike the group-delete path. Adding your Inbox to a group and then leaving the group deleted the Inbox. Narrowing the previous virtual-agent rejection to "already in another group" is what let a builtin through the door, since a builtin normally sits on no roster at all. Two layers, matching how `ChatGroupModel.delete` already handles this: - `addAgentsToGroup` refuses a reserved builtin slug outright - the removal delete gains the same reserved-slug backstop, for a row that reached the roster some other way Both regression tests verified failing first: without the backstop the Inbox row is gone; without the door check the add resolves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🔒 fix(agent-group): a builtin is never a group-owned member The previous commit closed the two paths that DELETE an owned member. It left the one that rehomes them: `transferToWorkspace` rewrites `userId`/`workspaceId` for every owned member, so a builtin on a roster would have someone's Inbox moved into another scope. Rather than a third call-site backstop, put the carve-out in the shared judgement — which is what this whole change is about. A reserved builtin slug now resolves to `referenced` ahead of every other rule, including `supervisor`, in both the TS resolver and its SQL twin. Every owned path inherits it: delete, roster removal, group transfer, agent transfer. Call sites that partition a roster now pass `slug`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🔨 chore(agent-group): drop the now-unused `not` import Left over from the roster-eviction delete this branch removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * 🐛 fix(agent-group): classify referenced members with the same inputs everywhere `listReferencedMembers` fed `resolveGroupMembershipType` a row without `slug`, so a builtin on a roster read as merely `virtual` — owned — and dropped out of the warning. The transfer path does pass `slug`, treats the same row as referenced, and clones it. The warning therefore omitted the one row the move was about to act on. Carry `slug` here too. The shared judgement only helps if every caller hands it the same facts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
103 lines
6.8 KiB
JSON
103 lines
6.8 KiB
JSON
{
|
||
"actionTag.category.agentSkill": "Навык агента",
|
||
"actionTag.category.command": "Команда",
|
||
"actionTag.category.projectSkill": "Навык проекта",
|
||
"actionTag.category.skill": "Навык",
|
||
"actionTag.category.tool": "Инструмент",
|
||
"actionTag.tooltip.agentSkill": "Загружает набор навыков из документов этого агента для запроса.",
|
||
"actionTag.tooltip.clickToView": "Нажмите, чтобы просмотреть детали",
|
||
"actionTag.tooltip.command": "Выполняет клиентскую slash-команду перед отправкой.",
|
||
"actionTag.tooltip.projectSkill": "Отправляется как вызов через слэш, чтобы CLI агента запускал соответствующий навык проекта.",
|
||
"actionTag.tooltip.skill": "Загружает для этого запроса переиспользуемый пакет навыков.",
|
||
"actionTag.tooltip.tool": "Помечает инструмент, который пользователь явно выбрал для этого запроса.",
|
||
"actions.expand.off": "Свернуть",
|
||
"actions.expand.on": "Развернуть",
|
||
"actions.typobar.off": "Скрыть панель форматирования",
|
||
"actions.typobar.on": "Показать панель форматирования",
|
||
"actions.typobar.title": "Инструмент форматирования",
|
||
"autoSave.failed": "Сохранение не удалось",
|
||
"autoSave.latest": "Загружена последняя версия",
|
||
"autoSave.retry": "Повторить",
|
||
"autoSave.saved": "Сохранено",
|
||
"autoSave.saving": "Автосохранение...",
|
||
"cancel": "Отмена",
|
||
"codemirror.copyFailed": "Не удалось скопировать",
|
||
"codemirror.copySuccess": "Код скопирован в буфер обмена",
|
||
"codemirror.selectLanguage": "Выбрать язык",
|
||
"codemirror.selectTheme": "Выбрать тему",
|
||
"codemirror.showLineNumbers": "Показать номера строк",
|
||
"codemirror.tabSize": "Размер табуляции",
|
||
"codemirror.theme": "Тема",
|
||
"codemirror.useTabs": "Использовать табуляцию",
|
||
"confirm": "Подтвердить",
|
||
"file.error": "Ошибка: {{message}}",
|
||
"file.preparing": "Подготовка к загрузке…",
|
||
"file.processing": "Обработка файла…",
|
||
"file.uploading": "Загрузка файла...",
|
||
"file.uploadingProgress": "Загрузка… {{progress}}%",
|
||
"image.broken": "Изображение повреждено",
|
||
"link.edit": "Редактировать ссылку",
|
||
"link.editLinkTitle": "Ссылка",
|
||
"link.editTextTitle": "Заголовок",
|
||
"link.open": "Открыть ссылку",
|
||
"link.placeholder": "Введите URL ссылки",
|
||
"link.unlink": "Удалить ссылку",
|
||
"markdown.cancel": "Отмена",
|
||
"markdown.confirm": "Преобразовать",
|
||
"markdown.parseMessage": "Преобразовать в формат Markdown. Существующее содержимое будет перезаписано. Вы уверены? (Окно закроется автоматически через 5 секунд)",
|
||
"markdown.parseTitle": "Форматировать как Markdown",
|
||
"math.placeholder": "Введите формулу в формате TeX",
|
||
"modifier.accept": "Оставить",
|
||
"modifier.acceptAll": "Оставить все",
|
||
"modifier.reject": "Отменить",
|
||
"modifier.rejectAll": "Отменить все",
|
||
"skillFrontmatter.edit": "Редактировать метаданные",
|
||
"skillFrontmatter.empty": "Нет метаданных",
|
||
"skillFrontmatter.invalid.descriptionInvalid": "Описание должно быть однострочным текстом.",
|
||
"skillFrontmatter.invalid.descriptionRequired": "Описание обязательно.",
|
||
"skillFrontmatter.invalid.mapping": "Фронтматтер должен быть YAML-отображением.",
|
||
"skillFrontmatter.invalid.nameInvalid": "Имя должно содержать только строчные буквы, цифры и дефисы.",
|
||
"skillFrontmatter.invalid.nameLocked": "Имя должно оставаться {{name}}. Переименуйте пакет навыков вместо этого.",
|
||
"skillFrontmatter.invalid.nameRequired": "Имя обязательно.",
|
||
"skillFrontmatter.invalid.required": "Фронтматтер обязателен.",
|
||
"skillFrontmatter.invalid.syntax": "Недопустимый синтаксис YAML.",
|
||
"skillFrontmatter.saveFailed": "Метаданные не были сохранены. Повторите попытку или продолжайте редактирование.",
|
||
"skillFrontmatter.title": "Метаданные навыка",
|
||
"slash.compact": "Сжать контекст",
|
||
"slash.goal": "Установить цель",
|
||
"slash.goal.desc": "Запланируйте и начните цель с редактируемыми критериями принятия",
|
||
"slash.h1": "Заголовок 1",
|
||
"slash.h2": "Заголовок 2",
|
||
"slash.h3": "Заголовок 3",
|
||
"slash.hr": "Разделитель",
|
||
"slash.newTopic": "Отправить в новую тему",
|
||
"slash.rewrite": "Переписать",
|
||
"slash.summarize": "Суммировать",
|
||
"slash.table": "Таблица",
|
||
"slash.tex": "Формула TeX",
|
||
"slash.translate": "Перевести",
|
||
"table.delete": "Удалить таблицу",
|
||
"table.deleteColumn": "Удалить столбец",
|
||
"table.deleteRow": "Удалить строку",
|
||
"table.insertColumnLeft": "Вставить {{count}} столбец(а) слева",
|
||
"table.insertColumnRight": "Вставить {{count}} столбец(а) справа",
|
||
"table.insertRowAbove": "Вставить {{count}} строку(и) выше",
|
||
"table.insertRowBelow": "Вставить {{count}} строку(и) ниже",
|
||
"typobar.blockquote": "Цитата",
|
||
"typobar.bold": "Жирный",
|
||
"typobar.bulletList": "Маркированный список",
|
||
"typobar.code": "Встроенный код",
|
||
"typobar.codeblock": "Блок кода",
|
||
"typobar.image": "Изображение",
|
||
"typobar.italic": "Курсив",
|
||
"typobar.link": "Ссылка",
|
||
"typobar.numberList": "Нумерованный список",
|
||
"typobar.redo": "Повторить",
|
||
"typobar.strikethrough": "Зачёркнутый",
|
||
"typobar.table": "Таблица",
|
||
"typobar.taskList": "Список задач",
|
||
"typobar.tex": "Формула TeX",
|
||
"typobar.underline": "Подчёркнутый",
|
||
"typobar.undo": "Отменить"
|
||
}
|