mirror of
https://github.com/lobehub/lobehub.git
synced 2026-09-20 04:56:13 +08:00
c4314f0252
* 🐛 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.
192 lines
17 KiB
JSON
192 lines
17 KiB
JSON
{
|
||
"anchor.filter": "Считается практикой",
|
||
"anchor.noCanon": "нет канона — охват не может быть оценен",
|
||
"anchor.noLayers": "нет уровней — уроки не сгруппированы по уровню",
|
||
"anchor.outOfScope": "Не учитывается",
|
||
"anchor.subtitle": "стандарт, по которому он учится, установлен при создании направления",
|
||
"anchor.title": "Его якорь",
|
||
"charts.count": "{{count}} уроков",
|
||
"charts.early": "начато с {{rate}}%",
|
||
"charts.learned": "Усвоено на данный момент",
|
||
"charts.learnedSub": "{{runs}} практик",
|
||
"charts.passRate": "Сделано правильно",
|
||
"charts.recentAvg": "среднее за последние {{count}}",
|
||
"charts.refLine": "линия 90%",
|
||
"charts.run1": "практика 1",
|
||
"charts.runN": "практика {{n}}",
|
||
"charts.streak": "{{count}} практик подряд без ошибок",
|
||
"charts.tooFew": "Графики появляются после второй практики.",
|
||
"create.adjust.action": "Применить корректировку",
|
||
"create.adjust.adjusting": "Корректировка…",
|
||
"create.adjust.blockAction": "Корректировать с помощью ИИ",
|
||
"create.adjust.failed": "Не удалось скорректировать якорь. Ваш текущий черновик остался без изменений.",
|
||
"create.adjust.generatingCountdown": "Корректировка якоря… осталось примерно {{time}}",
|
||
"create.adjust.help": "Опишите, что следует изменить. Текущий якорь и все, что вы не упомянете, будут сохранены.",
|
||
"create.adjust.placeholder.canonEntries": "например, Используйте признанные принципы из более авторитетной структуры…",
|
||
"create.adjust.placeholder.domainFilter": "например, Включите делегированное планирование и сортировку информации, но только для указанного лица…",
|
||
"create.adjust.placeholder.layers": "например, Сделайте это настоящей прогрессией навыков, а не этапами рабочего процесса…",
|
||
"create.adjust.placeholder.outOfScope": "например, Исключите общее управление проектами и решения, принимаемые от имени лица…",
|
||
"create.adjust.placeholder.rationale": "например, Четко скажите, в чем я стану лучше, без повторного перечисления слоев…",
|
||
"create.adjust.title": "Корректировать с помощью ИИ",
|
||
"create.anchor.addCanon": "Добавить",
|
||
"create.anchor.addLayer": "Добавить",
|
||
"create.anchor.canon": "Канон",
|
||
"create.anchor.canonHint": "признанные принципы, по которым он будет оцениваться",
|
||
"create.anchor.canonSource": "источник (книга / структура)",
|
||
"create.anchor.canonStatement": "общий принцип — почему эта ошибка повторяется",
|
||
"create.anchor.canonTitle": "принцип",
|
||
"create.anchor.layerDesc": "одна строка о том, что охватывает этот уровень",
|
||
"create.anchor.layerTitle": "уровень",
|
||
"create.anchor.layers": "Уровни",
|
||
"create.anchor.layersFrom": "из {{ref}}",
|
||
"create.anchor.layersInvented": "нет подходящей канонической модели — уровни предложены на основе задания",
|
||
"create.anchor.noCanon": "Канон пока отсутствует — он все равно будет учиться, но охват не может быть оценен.",
|
||
"create.anchor.noLayers": "Уровни пока отсутствуют — уроки не будут сгруппированы по уровню.",
|
||
"create.back": "Назад",
|
||
"create.briefHelp": "Опишите, чему вы хотите, чтобы он научился, и что не должно учитываться. Это предложение становится тестом для определения, какие разговоры подпитывают этот опыт — мы определим название из него.",
|
||
"create.briefPlaceholder": "например, я хочу, чтобы он стал лучше в управлении инцидентами — от симптомов до коренной причины и предотвращения. Дискуссии по дизайну и общие вопросы по эксплуатации не учитываются.",
|
||
"create.cancel": "Отмена",
|
||
"create.confirm": "Создать",
|
||
"create.failed": "Не удалось создать. Попробуйте снова.",
|
||
"create.field.brief": "Краткое направление",
|
||
"create.field.domainFilter": "Что считается практикой",
|
||
"create.field.outOfScope": "Что не учитывается",
|
||
"create.field.outOfScopePlaceholder": "например, обсуждения дизайна, общие вопросы и ответы…",
|
||
"create.field.rationalePlaceholder": "Как это воспринимается в этом направлении и где это улучшится…",
|
||
"create.field.title": "Название",
|
||
"create.generate": "Создать",
|
||
"create.generating": "Создание области знаний…",
|
||
"create.generatingAlmostDone": "Все еще работаем — скоро будет готово…",
|
||
"create.generatingCanon": "Поиск признанных принципов…",
|
||
"create.generatingCountdown": "Осталось примерно {{time}}",
|
||
"create.generatingLayers": "Создание четкой прогрессии…",
|
||
"create.generatingScope": "Уточнение, что считается практикой…",
|
||
"create.modalTitle": "Создать область знаний",
|
||
"create.next": "Далее",
|
||
"create.regenerate": "Пересоздать",
|
||
"create.reviewHelp": "Вот якорь, который он извлек из вашего задания. Исправьте все, что неверно — фильтр определяет, из каких разговоров он учится; уровни и канон определяют, где прикрепляются уроки и что означает «охват».",
|
||
"domain.delete": "Удалить",
|
||
"domain.deleteConfirm.content": "Его {{habits}} привычки и {{runs}} записи практик в этом направлении будут удалены. Это действие нельзя отменить.",
|
||
"domain.deleteConfirm.ok": "Удалить",
|
||
"domain.deleteConfirm.title": "Удалить «{{name}}»?",
|
||
"domain.deleteFailed": "Не удалось удалить направление. Попробуйте снова.",
|
||
"domain.deleted": "Направление удалено.",
|
||
"domain.more": "Еще",
|
||
"domains.meta": "{{habits}} привычек · {{runs}} практик",
|
||
"domains.title": "Направления",
|
||
"domains.word.fresh": "только начинается",
|
||
"domains.word.recurring": "{{count}} повторяющихся",
|
||
"domains.word.shaky": "{{count}} нестабильных",
|
||
"domains.word.stable": "все устойчиво",
|
||
"empty.desc": "Направьте его на то, что он делает часто, и правила будут расти из повторяющейся реальной практики.",
|
||
"empty.title": "Превратите реальную работу в знания",
|
||
"experience.emptyDesc": "Уроки появятся здесь, когда он будет практиковаться или когда вы будете его учить.",
|
||
"experience.emptyTitle": "В этом направлении пока ничего не изучено",
|
||
"experience.notFound": "Это направление больше не существует",
|
||
"experience.subtitle": "Все {{count}} уроков в «{{name}}», ничего не объединено",
|
||
"experience.title": "Все уроки",
|
||
"habit.action.correct": "Исправить это",
|
||
"habit.action.forget": "Забыть это",
|
||
"habit.action.source": "Посмотреть, как он это использовал",
|
||
"habit.action.teachAgain": "Научить снова",
|
||
"habit.hint.freshNone": "еще не было возможности проверить",
|
||
"habit.hint.freshOne": "использовано один раз",
|
||
"habit.hint.recurring": "ошибка {{bad}} из последних {{total}} раз · последний раз в «{{topic}}»",
|
||
"habit.hint.recurringNoTopic": "ошибка {{bad}} из последних {{total}} раз",
|
||
"habit.hint.shaky": "ошибка один раз из последних {{total}} · в «{{topic}}»",
|
||
"habit.hint.shakyNoTopic": "ошибка один раз из последних {{total}}",
|
||
"habit.hint.stable": "{{count}} раз, все правильно",
|
||
"habit.hint.taughtPending": "вы научили это; ожидается следующая практика",
|
||
"habit.recentTip.none": "еще не проверено",
|
||
"habit.recentTip.pass": "правильно",
|
||
"habit.recentTip.title": "последние {{count}}: {{list}}",
|
||
"habit.recentTip.violation": "неправильно",
|
||
"habit.taughtTag": "вы научили",
|
||
"habit.teach.done": "Научено — он перенесет это в следующий разговор.",
|
||
"habit.teach.failed": "Не удалось сохранить. Попробуйте снова.",
|
||
"habit.teach.forgot": "Забыто. Он больше не будет практиковать это.",
|
||
"habit.teach.placeholderCorrect": "Что не так? Скажите своими словами — он включит это в урок…",
|
||
"habit.teach.placeholderRecurring": "Скажите, когда это применимо и когда абсолютно нет…",
|
||
"habit.teach.send": "Научить",
|
||
"habits.search": "Искать привычки",
|
||
"habits.summary": "{{stable}} сформировано · {{shaky}} нестабильных · {{recurring}} повторяющихся · {{fresh}} новых",
|
||
"habits.title": "Его привычки",
|
||
"habits.viewAll": "Все {{count}} уроков",
|
||
"headline.detail.recurring": "«{{title}}» продолжает идти неправильно",
|
||
"headline.detail.shaky": "{{count}} привычек все еще нестабильны",
|
||
"headline.multi.ok": "Во всех {{domains}} направлениях все, что он выучил, стало привычкой.",
|
||
"headline.single.fresh": "{{name}}: {{runs}} практик, {{count}} уроков захвачено, ни один не проверен.",
|
||
"headline.single.notPracticed": "{{name}} установлено как направление — ничего не практиковалось.",
|
||
"headline.single.recurring": "{{name}}: {{runs}} практик, привычки в основном устойчивы.",
|
||
"headline.single.shaky": "{{name}}: {{runs}} практик, большинство привычек устойчивы.",
|
||
"headline.single.stable": "{{name}}: {{runs}} практик, все, что он выучил, стало привычкой.",
|
||
"headline.subline": "{{habits}} привычек · {{runs}} практик · уроки вступают в силу сразу после их изучения; исправьте все, что видите неверным",
|
||
"headline.sublineMulti": "{{domains}} направлений · {{habits}} привычек · уроки вступают в силу сразу после их изучения; исправьте все, что видите неверным",
|
||
"nav.newDomain": "Начать новое направление",
|
||
"nav.teach": "Научить его одному",
|
||
"nav.warmup": "Просмотреть прошлые разговоры",
|
||
"preview.failed": "Не удалось загрузить этот урок",
|
||
"preview.loading": "загрузка…",
|
||
"preview.moreEvidence": "+{{count}} дополнительных записей",
|
||
"preview.open": "Открыть полный урок",
|
||
"profile.counts": "{{count}} привычек",
|
||
"profile.fresh": "{{count}} новых",
|
||
"profile.recurring": "{{count}} повторяющихся",
|
||
"profile.shaky": "{{count}} нестабильных",
|
||
"profile.title": "Его профиль в этом направлении",
|
||
"profile.word.fresh": "только начинается",
|
||
"profile.word.mostlyStable": "в основном устойчиво",
|
||
"profile.word.stable": "устойчиво",
|
||
"profile.word.unstable": "иногда срывается",
|
||
"profile.word.weak": "слабое место",
|
||
"rules.detail.domainUnavailable": "Контекст домена не удалось загрузить",
|
||
"rules.detail.examples": "Где это было протестировано",
|
||
"rules.detail.eyebrow": "Правило {{code}}",
|
||
"rules.detail.meta": "{{hits}} использований в {{runs}} практиках",
|
||
"rules.detail.noExamples": "Пока нет записанных примеров",
|
||
"rules.detail.noExamplesDesc": "Это правило еще не совпало с последующей практикой, поэтому недостаточно доказательств, чтобы судить, работает ли оно.",
|
||
"rules.detail.notFound": "Это правило больше не существует",
|
||
"rules.detail.openSource": "Открытый источник · {{title}}",
|
||
"rules.detail.outcome.pass": "Сработало",
|
||
"rules.detail.outcome.violation": "Пропущено",
|
||
"rules.detail.retry": "Повторить",
|
||
"rules.section.breaks": "Что ломается",
|
||
"rules.section.correct": "Лучший подход",
|
||
"rules.section.dont": "Не превращайте это в",
|
||
"rules.section.good": "Что работает",
|
||
"rules.section.how": "Как использовать",
|
||
"rules.section.limits": "Когда это не применимо",
|
||
"rules.section.rule": "Правило",
|
||
"rules.section.why": "Почему",
|
||
"rules.section.works": "Почему это работает",
|
||
"rules.section.wrong": "Неправильный подход",
|
||
"taught.arc.pending": "еще не было возможности использовать",
|
||
"taught.arc.recurring": "снова сорвалось",
|
||
"taught.arc.shaky": "идет то так, то иначе",
|
||
"taught.arc.used": "использовано ✓",
|
||
"taught.showAll": "Показать все {{count}}",
|
||
"taught.title": "Вы научили его {{count}} вещам",
|
||
"teachNew.domain": "Направление",
|
||
"teachNew.help": "Скажите своими словами. Это станет уроком с тегом «вы научили», вступающим в силу с следующего разговора.",
|
||
"teachNew.placeholder": "например, При проверке журналиста, сначала проверьте его историю публикаций, а затем социальные аккаунты…",
|
||
"tier.fresh": "Только что выучено",
|
||
"tier.freshSub": "еще не проверено",
|
||
"tier.recurring": "Повторяющееся",
|
||
"tier.recurringSub": "знает правило, но продолжает нарушать — стоит научить снова",
|
||
"tier.shaky": "Нестабильное",
|
||
"tier.shakySub": "иногда правильно, иногда неправильно",
|
||
"tier.stable": "Сформировано",
|
||
"tier.stableSub": "чисто недавно — ничего не нужно проверять",
|
||
"title": "Саморазвивающийся",
|
||
"warmup.dismiss": "ОК",
|
||
"warmup.done": "Обзор завершен — {{learned}} уроков усвоено и уже используется. Каждая практика с этого момента будет их тестировать; все нестабильное появится здесь.",
|
||
"warmup.failed": "Не удалось начать обзор. Попробуйте снова.",
|
||
"warmup.idle": "У него было {{count}} связанных разговоров. Пусть он сначала их просмотрит, чтобы усвоить то, что уже знает — уроки вступают в силу сразу.",
|
||
"warmup.idleNone": "Пока нет прошлых разговоров для обзора. Он начнет учиться с следующего подходящего.",
|
||
"warmup.namesSep": "”, “",
|
||
"warmup.running": "Просмотр {{count}} прошлых разговоров… {{learned}} уроков на данный момент",
|
||
"warmup.start": "Просмотреть {{count}} прошлых разговоров",
|
||
"warmup.title": "«{{name}}» готов",
|
||
"warmup.titleMulti": "«{{names}}» — {{count}} направлений готовы"
|
||
}
|