Files
lobehub__lobehub/locales/ru-RU/tool.json
Rdmclin2 c4314f0252 🐛 fix(messenger): stop losing oversize images to a silent download link (#18608)
* 🐛 fix(messenger): stop losing oversize images to a silent download link

- WeChat: the two CDN legs that move the media bytes shared the 15s budget
  sized for a small JSON call. A server far from the CDN is aborted mid-upload,
  the attachment counts as failed, and the picture goes out as a download link
  with nothing pointing at a timeout. They now get 60s — the same distinction
  Telegram's sender already draws between JSON calls and byte transfers.
- Every failure in the attachment chain (download, re-encode, upload) collapsed
  into the same silent `undefined` behind a `debug()` call, so a deployed
  server could degrade every image in a push without recording which step gave
  up. Downloads, compression and WeChat uploads now report the step and the
  reason, so "it sent a link again" stops being the only evidence.
- An oversize image is a real choice — a recompressed JPEG that shows inline,
  or the untouched original behind a download link — so the sender makes it
  instead of the server. `link` skips the download and re-encode entirely, and
  the choice rides the WeChat replay queue so a queued push replays with the
  same decision rather than the default.
- Push modal: the file and every decision about it now share one region, with
  the size and the limit read as the file's own metadata; an oversize file and
  an oversize image no longer use two different shapes for one fact.

* 🌐 chore(i18n): translate the oversize-attachment strings

Also carries the translation backlog `bun run i18n` picked up from earlier
merges (chat / models / setting / tool / verify / selfLearning), which the
daily workflow had not run yet.

* 🔒 fix(messenger): keep presigned URL query strings out of the logs

Addresses the Codex review on #18608.

P1 — the attachment download failure paths wrote the complete `fetchUrl` at
warn level. A presigned storage URL carries `X-Amz-Credential` and
`X-Amz-Signature` in its query string, logs outlive the signature they would
leak, and this loader is reachable from `botMessage`, whose schema accepts a
caller-supplied URL.

`redactUrlForLog` reduces a URL to origin + path — enough to say which object
failed, nothing after it. Applied to every site in this chain that logs a URL,
not only the two the review pointed at: the two `console.warn` calls, the
`content-length` and byte-cap `debug()` lines in the same loader, and
`resolveSafeUrl` / `fetchPublicUrl`'s own traces, which take the raw
caller-supplied string.

* ✅ fix(test): make the publicUrlFetch mocks partial

`redactUrlForLog` joined `publicUrlFetch`, and ten suites mock that module with
a factory naming only `fetchPublicUrl`. A `vi.mock` factory REPLACES the module,
so every unnamed export becomes undefined — five tests across `attachmentBudget`
and `telegram/sendAttachments` died on "No redactUrlForLog export is defined on
the mock" the moment they walked a download-failure path.

Each factory now spreads `vi.importActual` first, which is what vitest's own
error message recommends and what keeps the next export added to
`publicUrlFetch` from breaking suites that never cared about it.

* ♻️ refactor(messenger): carry the degradation reason in the result, not in prints

The previous commit reached for `console.warn` / `console.error` in the helpers
to make a silent degradation visible. Wrong lever:

- level inflation — an image sharp cannot decode is routine input, and it was
  landing in the error stream;
- unbounded volume — up to two lines per attachment, so one push of ten and a
  few retries buries the log (and Vercel bills and samples it);
- prose, not signal — no push, user or operation to join the lines to;
- the tests that came with it asserted on spied console output, i.e. on log
  wording.

The reason is data now. `prepareAttachmentsForBudget` returns `degradations`
(`compression-failed` / `not-compressible` / `source-unavailable` /
`strategy-link` / `unverifiable-size`), and `sendWechatAttachments` returns
`{ failures, undelivered }` where `failures.detail` carries the iLink `errmsg` —
the part that actually diagnoses a refused upload. Every per-attachment event is
back on `debug()`, matching the rest of the server.

Exactly one line is printed, at the delivery boundary that knows whose push it
is, and only when something degraded against the sender's wishes — a deliberate
`link` choice is not a failure:

  [messenger:wechat] 2 attachment(s) sent as a download link —
    hi.png: source-unavailable; a.png: upload-failed (invalid media size)

Volume drops from O(attachments) to O(pushes) and the error level is left for
actual faults. Tests assert the returned reasons; `redactUrlForLog` gets its own
unit tests instead of being checked through a console spy.

* 🐛 fix(messenger): stop offering a compression the server will refuse

Addresses the Codex review on #18608.

P2 — an image past `MAX_COMPRESSION_SOURCE_BYTES` (100MB) is never recompressed:
`prepareAttachmentsForBudget` refuses to buffer a source that large and sends a
download link instead, whatever the sender picked. The modal still offered
"compress it" for such a file and spelled out a re-encode underneath it, so the
option behaved exactly like "send a link" while promising otherwise.

The cap moves to `@lobechat/const` beside the budget table, for the same reason
that table is there: the modal and the sender have to read one number or they
tell the sender different stories. The decision itself is now
`resolveOversizePlan`, a pure function next to `resolveAttachmentType` — an
oversize image only earns a choice when the server will actually attempt the
re-encode, and everything else states the fallback it really gets.

* 🔒 fix(messenger): sanitize logged names, and stop claiming a link was delivered

Addresses the Codex review on #18608.

P2 — `summarizeDegradations` copied a user-controlled filename verbatim into a
persistent log line. A newline in it forges a whole additional entry that a
reader cannot tell from a real one; other control characters corrupt whatever
consumes the stream. `sanitizeForLog` flattens controls and the Unicode line
separators, collapses whitespace and caps the length. The reason string gets the
same treatment — it carries platform error text, which is no more trusted. The
name is sanitized rather than dropped: without it the line cannot say WHICH
attachment degraded, which is the only reason it is written.

P2 — the same line announced attachments "sent as a download link", but it runs
before the link legs, either of which can throw, and `source-unavailable` /
`over-budget-no-link` have no `fetchUrl` to build a link from at all — those are
requeued. It now says they could not be sent as files, which is the part
actually settled at that point. It still runs before the sends on purpose, so a
push that dies mid-delivery leaves its reasons behind.
2026-08-23 15:05:03 +08:00

234 lines
22 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"agentGroupManagement.executeTask.agent": "Выполнить агентом",
"agentGroupManagement.executeTask.cancelled": "Отменено",
"agentGroupManagement.executeTask.completed": "Задача выполнена",
"agentGroupManagement.executeTask.cost": "Стоимость",
"agentGroupManagement.executeTask.failed": "Ошибка выполнения задачи",
"agentGroupManagement.executeTask.interrupted": "Прервано",
"agentGroupManagement.executeTask.intervention.taskPlaceholder": "Пожалуйста, укажите подробное описание задачи, которую должен выполнить агент...",
"agentGroupManagement.executeTask.intervention.timeout": "Максимальное время выполнения",
"agentGroupManagement.executeTask.intervention.timeoutUnit": "минут",
"agentGroupManagement.executeTask.intervention.unknownAgent": "Неизвестный агент",
"agentGroupManagement.executeTask.processing": "Обработка...",
"agentGroupManagement.executeTask.steps": "Этапы выполнения",
"agentGroupManagement.executeTask.task": "Содержание задачи",
"agentGroupManagement.executeTask.thread": "ID потока",
"agentGroupManagement.executeTask.timeout": "Время выполнения истекло",
"agentGroupManagement.executeTask.tokens": "Использование токенов",
"agentGroupManagement.executeTasks.intervention.instructionPlaceholder": "Подробная инструкция для агента по выполнению этой задачи...",
"agentGroupManagement.executeTasks.intervention.titlePlaceholder": "Название задачи...",
"agentGroupManagement.executeTasks.results": "Результаты",
"agentMarketplace.category.businessStrategy": "Бизнес и стратегия",
"agentMarketplace.category.contentCreation": "Создание контента",
"agentMarketplace.category.creatorEconomy": "Экономика создателей",
"agentMarketplace.category.designCreative": "Дизайн и креатив",
"agentMarketplace.category.engineering": "Инженерия",
"agentMarketplace.category.financeLegal": "Финансы и право",
"agentMarketplace.category.learningResearch": "Обучение и исследования",
"agentMarketplace.category.marketing": "Маркетинг",
"agentMarketplace.category.operations": "Операции",
"agentMarketplace.category.peopleHR": "Персонал и HR",
"agentMarketplace.category.personalLife": "Личная жизнь",
"agentMarketplace.category.productManagement": "Управление продуктом",
"agentMarketplace.category.salesCustomer": "Продажи и клиенты",
"agentMarketplace.inspector.moreCategories_one": "+{{count}}",
"agentMarketplace.inspector.moreCategories_other": "+{{count}}",
"agentMarketplace.inspector.pickCount_one": "{{count}} агент",
"agentMarketplace.inspector.pickCount_other": "{{count}} агентов",
"agentMarketplace.picker.empty": "Шаблоны отсутствуют.",
"agentMarketplace.picker.failedToLoad": "Не удалось загрузить шаблоны. Пожалуйста, попробуйте позже.",
"agentMarketplace.picker.summary": "Доступно шаблонов: {{filtered}} из {{total}}.",
"agentMarketplace.render.alreadyInLibraryTag": "Уже в библиотеке",
"agentMarketplace.render.alreadyInLibrary_one": "{{count}} уже в библиотеке",
"agentMarketplace.render.alreadyInLibrary_other": "{{count}} уже в библиотеке",
"askUserQuestion.customOption.placeholder": "Или напишите свой вариант…",
"askUserQuestion.escape.back": "Вернуться к вариантам",
"askUserQuestion.escape.enter": "Или введите напрямую",
"askUserQuestion.escape.placeholder": "Введите ваш ответ здесь…",
"askUserQuestion.multiSelectTag": "(множественный выбор)",
"askUserQuestion.recommendedTag": "Рекомендуется",
"askUserQuestion.skip": "Пропустить",
"askUserQuestion.submit": "Отправить",
"askUserQuestion.supplement.enter": "Дополнительные заметки",
"askUserQuestion.supplement.placeholder": "Добавьте контекст к выбранным ответам…",
"claudeCode.askUserQuestion.customOption.placeholder": "Или напишите свой вариант…",
"claudeCode.askUserQuestion.escape.back": "Вернуться к вариантам",
"claudeCode.askUserQuestion.escape.enter": "Или введите напрямую",
"claudeCode.askUserQuestion.escape.placeholder": "Введите ваш ответ здесь…",
"claudeCode.askUserQuestion.multiSelectTag": "(множественный выбор)",
"claudeCode.askUserQuestion.recommendedTag": "Рекомендуется",
"claudeCode.askUserQuestion.skip": "Пропустить",
"claudeCode.askUserQuestion.submit": "Отправить",
"claudeCode.askUserQuestion.supplement.enter": "Дополнительные заметки",
"claudeCode.askUserQuestion.supplement.placeholder": "Добавьте контекст к выбранным ответам…",
"claudeCode.askUserQuestion.timeExpired": "Время истекло — используется вариант 1 для каждого вопроса.",
"claudeCode.askUserQuestion.timeRemaining": "Оставшееся время: {{time}} · на вопросы без ответа по истечении времени будет выбран вариант 1.",
"codeInterpreter-legacy.error": "Ошибка выполнения",
"codeInterpreter-legacy.executing": "Выполнение...",
"codeInterpreter-legacy.files": "Файлы:",
"codeInterpreter-legacy.output": "Вывод:",
"codeInterpreter-legacy.returnValue": "Возвращаемое значение:",
"codeInterpreter.error": "Ошибка выполнения",
"codeInterpreter.executing": "Выполнение...",
"codeInterpreter.files": "Файлы:",
"codeInterpreter.output": "Вывод:",
"codeInterpreter.returnValue": "Возвращаемое значение:",
"connector.actionFailed": "Операция не удалась, пожалуйста, попробуйте снова",
"connector.add.title": "Добавить пользовательский коннектор",
"connector.deleteAccountConfirmContent": "Это удаляет коннектор и его авторизацию из вашей учетной записи. Любой агент, который его использует, должен будет авторизоваться заново.",
"connector.deleteAgentConfirmContent": "Этот коннектор принадлежит агенту «{{agent}}». Удаление его также удалит этот инструмент у данного агента.",
"connector.manageOnlyCreator": "Только создатель или владелец рабочего пространства может управлять этим коннектором",
"connector.thisAgent": "этот агент",
"dalle.autoGenerate": "Автогенерация",
"dalle.downloading": "Ссылки на изображения, сгенерированные DALL·E3, действительны только в течение 1 часа, кэширование изображений локально...",
"dalle.generate": "Сгенерировать",
"dalle.generating": "Генерация...",
"dalle.images": "Изображения:",
"dalle.prompt": "Запрос",
"lobe-agent.actions.add": "Добавить",
"lobe-agent.actions.clearCompleted": "Очистить выполненные",
"lobe-agent.actions.placeholder": "Введите задачу...",
"lobe-agent.addTodo.placeholder": "Добавить задачу...",
"lobe-agent.clearTodos.cleared": "{{count}} элемент(ов) очищено",
"lobe-agent.clearTodos.clearedCompleted": "{{count}} выполненный(ых) элемент(ов) очищено",
"lobe-agent.clearTodos.clearedCompleted_one": "{{count}} выполненный элемент очищен",
"lobe-agent.clearTodos.clearedCompleted_other": "{{count}} выполненных элементов очищено",
"lobe-agent.clearTodos.cleared_one": "{{count}} элемент очищен",
"lobe-agent.clearTodos.cleared_other": "{{count}} элементов очищено",
"lobe-agent.clearTodos.header": "Очистить задачи",
"lobe-agent.clearTodos.label": "Выберите, что очистить:",
"lobe-agent.clearTodos.noItems": "Нет элементов для очистки",
"lobe-agent.clearTodos.option.all": "Очистить все элементы (включая ожидающие)",
"lobe-agent.clearTodos.option.completed": "Очистить только выполненные элементы",
"lobe-agent.clearTodos.remaining": "{{count}} элемент(ов) осталось",
"lobe-agent.clearTodos.remaining_one": "{{count}} элемент остался",
"lobe-agent.clearTodos.remaining_other": "{{count}} элементов осталось",
"lobe-agent.completeTodos.completed": "{{count}} элемент(ов) выполнено",
"lobe-agent.completeTodos.completed_one": "{{count}} элемент выполнен",
"lobe-agent.completeTodos.completed_other": "{{count}} элементов выполнено",
"lobe-agent.createPlan.context.label": "Контекст (необязательно)",
"lobe-agent.createPlan.context.placeholder": "Фон, ограничения, соображения...",
"lobe-agent.createPlan.description.label": "Описание",
"lobe-agent.createPlan.description.placeholder": "Краткое описание плана",
"lobe-agent.createPlan.goal.label": "Цель",
"lobe-agent.createPlan.goal.placeholder": "Чего вы хотите достичь?",
"lobe-agent.createTodos.created": "{{count}} задача(и) создана(ы)",
"lobe-agent.createTodos.created_one": "{{count}} задача создана",
"lobe-agent.createTodos.created_other": "{{count}} задач создано",
"lobe-agent.createTodos.total": "Всего: {{count}} элемент(ов)",
"lobe-agent.createTodos.total_one": "Всего: {{count}} элемент",
"lobe-agent.createTodos.total_other": "Всего: {{count}} элементов",
"lobe-agent.removeTodos.removed": "{{count}} элемент(ов) удалено",
"lobe-agent.removeTodos.removed_one": "{{count}} элемент удален",
"lobe-agent.removeTodos.removed_other": "{{count}} элементов удалено",
"lobe-agent.status.done": "{{count}} выполнено",
"lobe-agent.status.pending": "{{count}} ожидает",
"lobe-agent.todoItem.placeholder": "Введите задачу...",
"lobe-agent.todoList.empty": "Список задач пуст",
"lobe-agent.todoList.items": "{{count}} элемент(ов)",
"lobe-agent.todoList.items_one": "{{count}} элемент",
"lobe-agent.todoList.items_other": "{{count}} элементов",
"lobe-agent.todoList.title": "Список задач",
"lobe-agent.updateTodos.updated": "Список задач обновлен",
"lobe-knowledge-base.readKnowledge.meta.chars": "Количество символов",
"lobe-knowledge-base.readKnowledge.meta.lines": "Количество строк",
"localFiles.editFile.ambiguous": "Этот текст появляется {{times}} раз(а) в файле. Изменение будет отклонено — необходимо достаточно контекста, чтобы идентифицировать одно вхождение, или выбрать «заменить все».",
"localFiles.editFile.newString": "Заменить на",
"localFiles.editFile.oldString": "Найти",
"localFiles.editFile.replaceAll": "Заменить все вхождения",
"localFiles.editFile.replaceFirst": "Заменить только первое вхождение",
"localFiles.file": "Файл",
"localFiles.folder": "Папка",
"localFiles.globFiles.pattern": "Шаблон",
"localFiles.grepContent.glob": "Фильтр файлов",
"localFiles.grepContent.pattern": "Шаблон поиска",
"localFiles.grepContent.type": "Тип файла",
"localFiles.listFiles.emptyDirectory": "Эта папка пуста",
"localFiles.moveFiles.itemsMoved": "Перемещено: {{count}} элемент(ов):",
"localFiles.moveFiles.itemsMoved_one": "Перемещён {{count}} элемент:",
"localFiles.moveFiles.itemsMoved_other": "Перемещено {{count}} элементов:",
"localFiles.moveFiles.itemsToMove": "К перемещению: {{count}} элемент(ов):",
"localFiles.moveFiles.itemsToMove_one": "К перемещению: {{count}} элемент:",
"localFiles.moveFiles.itemsToMove_other": "К перемещению: {{count}} элементов:",
"localFiles.open": "Открыть",
"localFiles.openFile": "Открыть файл",
"localFiles.openFolder": "Открыть папку",
"localFiles.outOfScope.requestedPaths": "Запрошенные пути",
"localFiles.outOfScope.warning": "Предупреждение: Следующие пути находятся за пределами настроенной рабочей директории. Пожалуйста, подтвердите, что вы хотите разрешить доступ.",
"localFiles.outOfScope.workingDirectory": "Рабочая директория",
"localFiles.read.more": "Показать больше",
"localFiles.readFile": "Прочитать файл",
"localFiles.readFile.lineRange": "Строки {{start}} - {{end}}",
"localFiles.readFileError": "Не удалось прочитать файл, проверьте правильность пути",
"localFiles.readFiles": "Прочитать файлы",
"localFiles.readFilesError": "Не удалось прочитать файлы, проверьте правильность пути",
"localFiles.searchFiles.keywords": "Ключевые слова",
"localFiles.securityBlacklist.warning": "Предупреждение безопасности: Эта операция была отмечена правилами безопасности и требует вашего явного подтверждения.",
"localFiles.writeFile.characters": "символов",
"localFiles.writeFile.preview": "Предпросмотр содержимого",
"localFiles.writeFile.truncated": "усечено",
"search.emptyResult": "Результаты не найдены, измените ключевые слова и попробуйте снова",
"search.genAiMessage": "Создать сообщение агента",
"search.includedTooltip": "Текущие результаты поиска будут включены в контекст беседы",
"search.keywords": "Ключевые слова:",
"search.scoreTooltip": "Оценка релевантности; чем выше значение, тем ближе результат к запросу",
"search.searchBar.button": "Поиск",
"search.searchBar.placeholder": "Ключевые слова",
"search.searchBar.tooltip": "Это обновит результаты поиска и создаст новое сообщение с кратким содержанием",
"search.searchCategory.placeholder": "Категория поиска",
"search.searchCategory.title": "Категория поиска:",
"search.searchCategory.value.files": "Файлы",
"search.searchCategory.value.general": "Общее",
"search.searchCategory.value.images": "Изображения",
"search.searchCategory.value.it": "Информационные технологии",
"search.searchCategory.value.map": "Карты",
"search.searchCategory.value.music": "Музыка",
"search.searchCategory.value.news": "Новости",
"search.searchCategory.value.science": "Наука",
"search.searchCategory.value.social_media": "Социальные сети",
"search.searchCategory.value.videos": "Видео",
"search.searchEngine.placeholder": "Поисковая система",
"search.searchEngine.title": "Поисковая система:",
"search.searchResult": "Количество поисков:",
"search.searchTimeRange.title": "Период времени:",
"search.searchTimeRange.value.anytime": "В любое время",
"search.searchTimeRange.value.day": "За день",
"search.searchTimeRange.value.month": "За месяц",
"search.searchTimeRange.value.week": "За неделю",
"search.searchTimeRange.value.year": "За год",
"search.summary": "Резюме",
"search.viewMoreResults": "Показать ещё {{results}} результатов",
"securityBlacklist.awsCredentials": "Доступ к учетным данным AWS может привести к утечке ключей доступа к облаку",
"securityBlacklist.browserCredentials": "Доступ к хранилищу учетных данных браузера может привести к утечке паролей",
"securityBlacklist.chownSystemDirs": "Изменение владельца системных директорий опасно",
"securityBlacklist.ddDiskWrite": "Запись случайных данных на дисковые устройства может уничтожить данные",
"securityBlacklist.directMemoryAccess": "Прямой доступ к памяти чрезвычайно опасен",
"securityBlacklist.disableFirewall": "Отключение брандмауэра подвергает систему атакам",
"securityBlacklist.dockerConfig": "Чтение конфигурации Docker может привести к утечке учетных данных реестра",
"securityBlacklist.envFiles": "Чтение файлов .env может привести к утечке конфиденциальных данных и API-ключей",
"securityBlacklist.etcPasswd": "Изменение /etc/passwd может заблокировать доступ к системе",
"securityBlacklist.forkBomb": "Форк-бомба может привести к сбою системы",
"securityBlacklist.formatPartition": "Форматирование системных разделов уничтожит данные",
"securityBlacklist.gcpCredentials": "Чтение учетных данных GCP может привести к утечке ключей сервисных аккаунтов",
"securityBlacklist.gitCredentials": "Чтение файла учетных данных Git может привести к утечке токенов доступа",
"securityBlacklist.historyFiles": "Чтение файлов истории может раскрыть конфиденциальные команды и учетные данные",
"securityBlacklist.kernelParams": "Изменение параметров ядра без должного понимания может привести к сбою системы",
"securityBlacklist.kubeConfig": "Чтение конфигурации Kubernetes может привести к утечке учетных данных кластера",
"securityBlacklist.npmrc": "Чтение файла токена npm может привести к утечке учетных данных реестра пакетов",
"securityBlacklist.removeSystemPackages": "Удаление критически важных системных пакетов может нарушить работу системы",
"securityBlacklist.rmForceRecursive": "Принудительное рекурсивное удаление без указания цели слишком опасно",
"securityBlacklist.rmHomeDir": "Рекурсивное удаление домашней директории чрезвычайно опасно",
"securityBlacklist.rmRootDir": "Рекурсивное удаление корневой директории уничтожит систему",
"securityBlacklist.sshConfig": "Изменение конфигурации SSH может заблокировать доступ",
"securityBlacklist.sshPrivateKeys": "Чтение приватных ключей SSH может поставить под угрозу безопасность системы",
"securityBlacklist.sudoers": "Изменение файла sudoers без должной проверки опасно",
"securityBlacklist.suidShells": "Установка SUID на оболочки или интерпретаторы представляет угрозу безопасности",
"updateArgs.duplicateKeyError": "Ключ поля должен быть уникальным",
"updateArgs.form.add": "Добавить элемент",
"updateArgs.form.key": "Ключ поля",
"updateArgs.form.value": "Значение поля",
"updateArgs.formValidationFailed": "Ошибка проверки формы, проверьте формат параметров",
"updateArgs.keyRequired": "Ключ поля не может быть пустым",
"updateArgs.stringifyError": "Не удалось сериализовать параметры, проверьте их формат"
}