npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ykurilov/agent-chat-attachments

v0.75.0

Published

Вложения к сообщению: картинки, PDF и текстовые файлы. Отдельное расширение: приложению, которому вложения не нужны, они не нужны и в сборке. Без пакета чат остаётся текстовым, договор остальных пакетов не меняется.

Downloads

618

Readme

@ykurilov/agent-chat-attachments

Вложения к сообщению: картинки, PDF и текстовые файлы. Отдельное расширение: приложению, которому вложения не нужны, они не нужны и в сборке. Без пакета чат остаётся текстовым, договор остальных пакетов не меняется.

Серверу без экрана React не нужен: react и @ykurilov/agent-chat-react — необязательные зависимости (peer). Для @ykurilov/agent-chat-attachments/react оба должны стоять в приложении, как и для самого чата на экране.

// сервер
import { createAttachments, createDiskAttachmentStorage } from "@ykurilov/agent-chat-attachments";

const attachments = createAttachments({ storage: createDiskAttachmentStorage({ directory: "./data/attachments" }) });

createChatHandler({
  …,
  attachments,
  agent: { instructions: appInstructions },
});
// браузер
import { attachmentsClient } from "@ykurilov/agent-chat-attachments/react";
<AgentChat api="/api/chat" chatId={chatId} skin={tandemSkin} attachments={attachmentsClient} />;

Что умеет

  • Типы: картинки (PNG, JPEG, WebP, GIF), PDF, текстовые файлы (.txt, .md, .csv, .json и подобные). Тип определяется по содержимому, а не по имени и не по тому, что назвал браузер. Всё остальное отклоняется понятным отказом.

  • Типы можно сузить: createAttachments({ acceptedMediaTypes: ["image/png", "image/jpeg", "image/webp"] }). Файл другого типа отклоняется отказом 415 unsupported_type до ужатия, разбора и записи (и при загрузке, и в ingestFile, и при импорте). Файл, загруженный раньше, при отправке сообщения тоже отклоняется: «подойдут только PNG, JPEG, WebP». Поле ввода получает список из GET …/attachments/settings и файл чужого типа (в том числе вставленный из буфера) отклоняет сразу, без загрузки. Тип, которого SDK не распознаёт (например image/jpg), — ошибка при запуске.

  • Картинки уходят только моделям, которые их видят у выбранного провайдера (modelAcceptsImages(profile, providerId) из ядра: acceptsImagesByProvider профиля, иначе acceptsImages). Если модель не видит, человек узнаёт об этом до отправки, а модель получает в запросе пометку «картинка не показана».

  • Модель без зрения смотрит картинку инструментом (imagesForBlindModels, по умолчанию выключено). Вместо картинки в сообщении — постоянная плашка с номером вложения, а инструмент приложения читает картинку этого чата и отдаёт её зрячей модели:

    const attachments = createAttachments({
      storage,
      acceptedMediaTypes: ["image/png", "image/jpeg", "image/webp"],
      imagesForBlindModels: { toolName: "look_at_attachment" },
      // свой текст: marker: ({ id, name, mediaType, authorId }) => `[Картинка от Юрия «${name}», вложение ${id}. Посмотреть: look_at_attachment.]`
    });
    
    // инструмент приложения (agent.tools — функция от выбранной модели, в ней есть chatId):
    const image = await attachments.readChatImage({ chatId: model.chatId, attachmentId });
    if (!image) return { error: "В этом чате нет такой картинки" }; // чужой чат, не картинка, неверный номер
    // image: { id, name, mediaType, authorId, bytes }

    Плашка по умолчанию: [Картинка «shot.png», вложение <id>. Посмотреть: look_at_attachment.]. Она собирается только из неизменной записи вложения, поэтому одинакова байт в байт на каждом ходе и после смены модели: кэш модели не сбивается. Свой marker тоже должен зависеть только от входа (без дат и счётчиков). Зрячей паре картинка уходит как раньше; always: true — плашка всем моделям. К инструкции вложений дописывается постоянная строка про плашку и инструмент. Поле ввода в этом режиме не предупреждает «модель не видит картинки». Сам инструмент (какая модель смотрит, сколько стоит) пишет приложение. readChatImage проверяет только чат: картинку отдаст, кто бы в этом чате её ни загрузил. Если в чате несколько людей и кому-то нельзя смотреть чужие картинки, проверку автора (image.authorId) делает приложение в своём инструменте. Без always смена пары «модель + провайдер» между зрячей и слепой меняет историю (плашка ↔ картинка) и сбрасывает кэш модели; нужен ровный кэш при смене моделей — ставьте always: true.

  • Только нужные типы (acceptedMediaTypes). Решает тип, с которым файл пришёл: ужатый в JPEG большой PNG при ["image/png"] проходит. Он записан в поле sourceMediaType записи вложения — своё хранилище должно его сохранять. Файл чужого типа, загруженный до сужения списка, при отправке получает отказ unsupported_type с текстом «Такой файл прикрепить нельзя: подойдут только PNG, JPEG, WebP.» (список — по настройке). Импорт чата (importChat) вместо картинки неподходящего типа пишет текст «[картинка: имя]».

  • isAttachmentId(value) — номер вложения ли это (UUID): проверка до похода в хранилище.

  • Большие фото уменьшаются при загрузке: картинка поворачивается по EXIF и ужимается до 2000×2000 пикселей и ~4,5 МБ в base64 (запас ниже 5 МБ строгих провайдеров). Хранится и показывается уменьшенная копия, оригинал не сохраняется. Картинки, загруженные до этого, ужимаются при сборке запроса (последние 20 решений запоминаются, повторно не перекодируются). Нечитаемая картинка и картинка больше 60 млн пикселей при загрузке — отказ 422 unreadable, в старой истории — пометка «картинка пропущена». У большой анимации (GIF, WebP) остаётся первый кадр в PNG или JPEG; имя файла не меняется. Обработку делает sharp: готовые сборки для macOS, Linux (glibc и Alpine/musl) и Windows ставятся сами, компилятор не нужен. В Docker ставьте зависимости внутри образа (pnpm install), а не копируйте node_modules с Mac: сборка sharp у каждой системы своя.

  • PDF и текст: текст достаётся на сервере при загрузке один раз и идёт в запрос блоком <attachment name="…" type="…">…</attachment> любой модели. PDF без текста (скан) отклоняется с объяснением.

  • Лимиты (createAttachments({ limits })): 10 файлов на сообщение, 20 МБ на файл, 25 000 токенов текста на файл (лишнее обрезается с пометкой), 100 000 токенов текста на сообщение. На человека: не больше 30 неотправленных файлов и 200 МБ (maxUnsentFiles, maxUnsentBytes, ответ 429 too_many_unsent); одновременных загрузок не больше 8 на сервер и 3 на человека (maxParallelUploads, maxParallelUploadsPerUser, ответ 429 busy). Файл с ошибкой в лимит не входит. Поле ввода грузит по два файла сразу, остальные ждут в очереди; на busy повторяет само. Значения по умолчанию — defaultAttachmentLimits.

  • Ужатие при загрузке идёт по очереди: разом не больше половины ядер процессора (maxParallelImageFits), остальные ждут.

  • Старые большие картинки (загруженные до того, как сервер стал их уменьшать) уменьшаются один раз и держатся в памяти: до 128 МБ уменьшенных копий (imageCacheBytes, 0 — не хранить), сверх — уходят давно не нужные.

  • Разбор PDF идёт в отдельном потоке с потолком памяти и сроком: зависший или «раздутый» файл обрывается, сервер живёт дальше. Сбор текста останавливается, как только набран потолок символов. Текстовые логи с цветовыми кодами терминала принимаются как текст.

  • Хранилище файлов заменяемое (AttachmentStorage): из коробки createDiskAttachmentStorage и createMemoryAttachmentStorage. Своё (например, S3) проверяется общим набором describeAttachmentStorageContract из @ykurilov/agent-chat-attachments/storage-contract. Схема базы чата не меняется, миграции не нужны. Метод removeAllOfChat(chatId) удаляет все файлы чата: его вызывает удаление чата из списка чатов.

  • Безопасность: файл привязан к чату и владельцу, чужой и несуществующий файл отвечают одинаково (404). Адрес в хранилище — серверный UUID, имя файла в путь не попадает. Файлы отдаются с nosniff и Content-Security-Policy: sandbox. Ключи провайдеров в браузер не уходят.

  • Кэш модели: вложения — часть сообщения человека, извлечённый текст не меняется от хода к ходу, поэтому начало запроса остаётся постоянным.

  • Сжатие истории: сводка называет прикреплённые файлы по имени, оригиналы остаются в хранилище.

  • Загруженные, но не отправленные файлы убираются сами через 24 часа (проверка раз в 15 минут), вместе с недописанными остатками на диске. Файл, который пролежал дольше и не был отправлен, возвращается в поле с пометкой «загрузите снова». Файл считается прикреплённым только после того, как сообщение записано; если отправка отклонена, текст и файлы остаются в поле.

  • Импорт старой переписки: attachments.importFile({ ownerId, chatId, bytes, mediaType, filename? }) сохраняет картинку из старой системы; его зовёт service.importChat ядра сам, если вложения подключены к чату.

  • Инструкция агенту про вложения (attachmentsInstructions) ядро добавляет само, вручную дописывать не нужно (дубль не появится).

  • Служебный формат <attachment …> (attachments.serviceBlocks): если модель повторит текст файла этим блоком в ответе, ядро его вырежет до экрана и записи.

Адреса

Под адресом чата (/api/chat): GET /attachments/settings (лимиты и допустимые типы для браузера), POST /:chatId/attachments (поле формы file), GET и DELETE /:chatId/attachments/:id (удалить можно только неотправленный файл).

Со своим скином

Composer получает необязательное attachments (ComposerAttachments): скрепка, перетаскивание и вставка файлов, превью с удалением, notice с готовой надписью. UserMessage получает необязательное attachments (MessageAttachmentsView): миниатюры картинок и плашки файлов. Нет свойства — вложений нет, ничего про них не рисовать. Надписи пакета — russianAttachmentsLabels, английские — englishAttachmentsLabels, свои передаются в createAttachmentsClient(labels).