Vortholm — документация

Загрузка файлов

Один запрос, до пятидесяти файлов, подпапки, поддерживаемые форматы и что означают счётчики в ответе.

Файлы кладутся в конкретную папку базы знаний — ту, чей идентификатор вернул /mine:

POST /api/admin/library-folders/<id папки>/files

Тело — обычная форма multipart/form-data. Поле называется files; их может быть несколько в одном запросе.

curl -X POST "https://portal.example.com/api/admin/library-folders/$FOLDER/files" \
  -H "Authorization: Bearer sk_forge_..." \
  -F "files=@договор-2026-014.pdf"

За один запрос — не больше пятидесяти файлов. Пятьдесят первый портал отклонит целиком, а не отрежет.

curl -X POST "https://portal.example.com/api/admin/library-folders/$FOLDER/files" \
  -H "Authorization: Bearer sk_forge_..." \
  -F "files=@акт-1.pdf" \
  -F "files=@акт-2.pdf" \
  -F "files=@реестр.xlsx"

Подпапки

Чтобы файлы легли не в корень папки, а в подпапки, добавьте поле relativePaths — массив строк той же длины, что и список файлов, в том же порядке. Пустая строка означает корень.

curl -X POST "https://portal.example.com/api/admin/library-folders/$FOLDER/files" \
  -H "Authorization: Bearer sk_forge_..." \
  -F 'relativePaths=["2026/01","2026/02",""]' \
  -F "files=@январь.xlsx" \
  -F "files=@февраль.xlsx" \
  -F "files=@сводная.xlsx"

Поле принимается и как повторяющееся поле формы, и как строка с JSON-массивом. Длина обязана совпадать с числом файлов, иначе весь запрос отклоняется. Путь проверяется: выйти за пределы папки через .. нельзя.

Форматы

Список ниже — то, что портал принимает на загрузку. Что именно он потом сумеет прочитать внутри файла — отдельный вопрос, и про него написано в разделе «Файлы».

Документы: .pdf, .docx, .doc, .xlsx, .xls, .pptx, .ppt, .odt, .ods, .odp, .txt, .csv, .md, .html, .htm, .json, .eml, .srt.

Изображения: .png, .jpg, .jpeg.

Аудио и видео: .wav, .mp3, .ogg, .flac, .m4a, .webm, .weba, .mp4, .mkv, .mov — при условии, что перевод записей в текст на этом портале включён. Если он выключен, такие файлы не загружаются, но и ошибкой это не считается: они возвращаются в поле skippedFiles ответа, а остальные файлы запроса принимаются.

Расширение должно соответствовать содержимому: для двоичных форматов портал проверяет первые байты файла и отклоняет запрос при несовпадении. Текстовые форматы (.txt, .csv, .md, .html, .htm, .json, .eml, .srt) эту проверку не проходят — по первым байтам их не отличить.

Письма в формате .eml — единственный случай, когда тип надо указать явно: без этого библиотека, которой вы отправляете запрос, скорее всего подставит application/octet-stream, и файл будет отклонён.

curl -X POST "https://portal.example.com/api/admin/library-folders/$FOLDER/files" \
  -H "Authorization: Bearer sk_forge_..." \
  -F "files=@письмо.eml;type=message/rfc822"

Размеры

Один документ — до 200 МБ. Для аудио и видео действует отдельный предел, он задаётся при установке портала; спросите его у администратора, если собираетесь грузить длинные записи.

Ответ

Успех — код 201:

{
  "data": {
    "uploaded": 2,
    "deduplicated": 1,
    "replaced": 0,
    "alreadyInFolder": 0,
    "files": [
      {
        "id": "b1f2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "filename": "акт-1.pdf",
        "relativePath": "",
        "deduplicated": false,
        "alreadyInFolder": false
      }
    ],
    "skippedFiles": []
  }
}

Что означают счётчики:

СчётчикЧто произошло
uploadedФайл новый: такого содержимого у портала не было, он добавлен в папку
deduplicatedТочно такое содержимое портал уже хранил. Новой копии не появится, папка сошлётся на имеющийся файл
alreadyInFolderЭто же содержимое уже лежит в этой папке по этому пути. Ничего не изменилось
replacedВ папке по этому пути уже был файл с таким именем, но с другим содержимым. Старый заменён новым

Счётчики пересекаются: файл, который уже лежал в папке и чьё содержимое портал уже знал, увеличит и alreadyInFolder, и deduplicated. Сумма четырёх чисел не равна числу отправленных файлов. Результат для каждого файла смотрите в массиве files.

Порядок элементов files не обязан совпадать с порядком файлов в запросе. Сопоставляйте по паре «relativePath + filename», а не по номеру в массиве.

Поле skippedFiles — список из пар filename и reason для файлов, которые портал принял к рассмотрению, но не сохранил. Сегодня туда попадают только медиафайлы при выключенном переводе записей в текст.

Что происходит дальше

Загрузка возвращает 201 сразу, но файл в этот момент ещё не готов к работе: портал обрабатывает его в фоне. Пока обработка не закончилась, файл виден в списке, но задачи его содержимое ещё не используют. Состояние каждого файла видно в поле indexStatus списка файлов и на странице папки в администраторской части. Что означают состояния — в справочнике статусов.

Записи (аудио и видео) проходят более длинный путь: сначала перевод в текст, и только потом обработка получившегося текста.

Если что-то пошло не так

  • 400. Запрос отклонён целиком, ни один файл не сохранён. Причина — в error.details.reason, таблица причин.
  • 403. Папка не входит в список этого ключа. Сверьтесь с ответом /mine.
  • Файл принят, но задачи его не видят. Скорее всего обработка ещё идёт или закончилась ошибкой — посмотрите indexStatus. Повторная попытка запрашивается переобработкой папки.
  • Загрузили новую версию, а в папке старая. Замена по имени срабатывает только при совпадении имени и пути. Файл с тем же именем, отправленный с другим relativePaths, ляжет рядом как отдельный.

На этой странице