Read in:
Русский

updateNotes: программное редактирование заметок

updateNotes — GraphQL-мутация для программного изменения содержимого заметок. За один вызов выполняет четыре типа операций: создание или полная замена (upsert), атомарный find-and-replace (patch), изменение ключей frontmatter (meta), скрытие заметки (hide). Изменения применяются сразу — отдельный вызов commitNotes не нужен.

Используйте updateNotes, когда точно знаете, что именно нужно изменить. Для полной синхронизации хранилища из плагина Obsidian подходит pushNotes + commitNotes.


Сигнатура мутации

mutation UpdateNotes($input: UpdateNotesInput!) {
  updateNotes(input: $input) {
    ... on UpdateNotesSuccessPayload { paths }
    ... on UpdateNotesHashMismatchPayload { path actualHash }
    ... on UpdateNotesPatchNotFoundPayload { path find }
    ... on ErrorPayload { message }
  }
}

Переменные:

{
  "input": {
    "changes": [ /* один или несколько объектов NoteChangeInput */ ]
  }
}

Типы операций

Каждый элемент changes — объект NoteChangeInput с одним из четырёх полей.

upsert — создать или полностью заменить заметку

{
  "upsert": {
    "path": "inbox.md",
    "content": "# Inbox\n\n- item"
  }
}

Если заметка не существует — создаёт её. Если существует — полностью заменяет содержимое.

Опциональный expectedHash: sha256 текущего содержимого заметки, в кодировке base64url. Если заметка изменилась с момента чтения — мутация вернёт UpdateNotesHashMismatchPayload вместо записи.

patch — атомарный find-and-replace внутри заметки

{
  "patch": {
    "path": "todo.md",
    "find": "- [ ] купить молоко",
    "replace": "- [x] купить молоко"
  }
}

Сервер читает текущее содержимое, заменяет единственное вхождение find на replace и сохраняет — всё в одной транзакции.

Правила:

  • find должна встречаться в заметке ровно один раз. Если строка не найдена или встречается несколько раз — мутация вернёт UpdateNotesPatchNotFoundPayload с path и find.
  • find — точная строка, не регулярное выражение.
  • Если find совпадает с replace — заметка не изменяется, новая версия не создаётся и событие изменения не поднимается: вебхуки не срабатывают, повторное встраивание не запускается.
  • Пустой replace допустим — удаляет найденную строку.

Опциональный expectedHash работает так же, как для upsert.

meta — изменить ключи frontmatter

{
  "meta": {
    "path": "todo.md",
    "set": { "status": "done", "priority": 2 },
    "unset": ["due"]
  }
}

Меняет ключи верхнего уровня во frontmatter заметки — без отправки всей заметки и без точного текста старого значения.

Правила:

  • set добавляет отсутствующий ключ и заменяет значение существующего. Значение — строка, число, логическое значение, список, объект или null.
  • unset удаляет ключи. Удалить ключ, которого в заметке нет, — не ошибка.
  • Только ключи верхнего уровня: "og.title" — это ключ с именем og.title, а не title внутри og. Чтобы поменять вложенное значение, задайте весь ключ верхнего уровня.
  • Остальные ключи сохраняют строку, порядок, кавычки и комментарии. Текст заметки не меняется. Новые ключи добавляются после последнего ключа, по алфавиту.
  • Заметка без frontmatter получает блок --- в начале; текст под ним остаётся байт в байт прежним.
  • Если ключу задают ту же строку, число или логическое значение, что уже стоит, строка остаётся как была, и новая версия заметки не создаётся.
  • Строка, которая начинается с перевода строки или содержит строку из одних дефисов вроде ---, записывается в двойных кавычках одной строкой, "one\n---\ntwo": так она сохраняет значение и не закрывает frontmatter раньше времени.
  • Строка ... завершает YAML-документ: ключи после неё лежат вне frontmatter, trip2g не читает их как meta, и правка их не трогает.
  • JSON не отличает 1.0 от 1, поэтому целое число записывается без дробной части: 1.0 станет 1; чтобы сохранить 1.0, передайте строку "1.0".
  • Заметка должна существовать. Если её нет или frontmatter нельзя править по ключам (невалидный YAML, flow-запись {...}, ключи с отступом от начала строки, YAML-алиас вида *name, ключ объявлен дважды, блок без закрывающей ---), мутация вернёт ErrorPayload.
  • Перед сохранением отредактированная заметка читается заново. Если какой-то ключ, кроме названных, прочитается иначе или названный ключ не получит своё значение, ничего не сохраняется и мутация вернёт ErrorPayload.
  • set и unset не могут быть пустыми одновременно, один ключ не может быть в обоих, а ключ не может быть пустым или содержать перевод строки и другие управляющие символы.

Опциональный expectedHash работает так же, как для patch.

hide — скрыть заметку

{
  "hide": {
    "path": "draft.md"
  }
}

Помечает заметку как скрытую, не затрагивая содержимое. Заметка остаётся в хранилище, но перестаёт отдаваться читателям. Скрытие записывается на админа: на вошедшего админа, на админа, создавшего API-ключ, или на админа, создавшего вебхук, который выдал токен.


Переключение чекбоксов (patch-сценарий)

Patch — стандартный механизм для переключения задач прямо со страницы.

Как это работает:

  1. В frontmatter заметки добавьте allow_task_toggle: true. Рендерер добавит атрибут data-line к элементам чеклиста — в нём хранится исходная строка markdown.
  2. Читатель кликает на чекбокс. Фронтенд читает data-line элемента и вызывает updateNotes:
    • find: исходная строка, например - [ ] купить молоко
    • replace: строка с изменённым статусом, например - [x] купить молоко (или обратное для снятия отметки)
  3. При успехе интерфейс обновляется оптимистично.

Требование уникальности. Если одинаковый текст задачи встречается в заметке несколько раз — patch вернёт UpdateNotesPatchNotFoundPayload. Фронтенд должен показать сообщение «нельзя переключить — повторяющийся текст задачи», а не молча упасть.

Включите переключение чекбоксов через frontmatter:

allow_task_toggle: true

Чтобы включить для всех заметок сразу, используйте frontmatter-патч:

* → { allow_task_toggle: true }

Защита от конфликтов: expectedHash

expectedHash — sha256-хеш текущего содержимого заметки в кодировке base64url.

При передаче хеша:

  • Сервер проверяет хеш перед применением изменения.
  • Если содержимое изменилось с момента чтения — мутация возвращает UpdateNotesHashMismatchPayload с path и актуальным actualHash.
  • Повторите операцию: перечитайте заметку, используйте новый хеш.

Для переключения чекбоксов прямо из интерфейса expectedHash обычно не нужен — строка find сама служит защитой. Если заметка изменилась и строки больше нет, patch вернёт PatchNotFound вместо затирания новых изменений.

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


Пакетные операции

Отправьте несколько изменений за один вызов:

{
  "input": {
    "changes": [
      {
        "patch": {
          "path": "todo.md",
          "find": "- [ ] купить молоко",
          "replace": "- [x] купить молоко"
        }
      },
      {
        "upsert": {
          "path": "log/2026-05-18.md",
          "content": "# Лог\n\nЗадача выполнена."
        }
      }
    ]
  }
}

Каждое изменение обрабатывается независимо. На первой ошибке обработка останавливается и возвращается соответствующий payload — предыдущие изменения уже применены. Пустые объекты NoteChangeInput {} пропускаются без ошибки.


Типы ответов

Payload Значение Что делать
UpdateNotesSuccessPayload Все изменения применены Читайте paths — список затронутых заметок; в updated[].warnings — предупреждения каждой заметки после сохранения
UpdateNotesHashMismatchPayload Заметка изменилась с момента чтения Перечитайте заметку, повторите с actualHash
UpdateNotesPatchNotFoundPayload Строка find не найдена или встречается несколько раз Покажите ошибку пользователю, при необходимости перечитайте заметку
ErrorPayload Ошибка валидации или системная ошибка Залогируйте message, исправьте входные данные

Авторизация

updateNotes принимает три вида доступа. Они проверяются по порядку, срабатывает первый найденный.

1. Вход администратора — полный доступ, паттерны записи не применяются. Подходит любой из способов, которыми trip2g узнаёт администратора:

  • сессия в браузере (cookie) — так работают страницы-приложения на том же сайте, см. SPA поверх trip2g;

  • персональный токен администратора в заголовке:

    Authorization: Bearer t2g_...
    
  • тот же персональный токен в адресе: ?token=t2g_....

2. API-ключ — доступ на запись ограничен паттернами записи ключа:

X-Api-Key: your-api-key

Если целевой путь не покрыт паттернами ключа, мутация вернёт ErrorPayload.

3. Короткоживущий токен из вебхука — для webhook-агентов, см. ниже:

Authorization: Bearer eyJhbGc...

Без любого из трёх запрос отклоняется с ошибкой missing X-API-Key in request header. Так же устроены hideNotes, uploadNoteAsset и commitNotes, только hideNotes и commitNotes отвечают токену вебхука ErrorPayload; webhook-агент скрывает заметку изменением hide.


Интеграция с вебхуками

Webhook-агент получает временный api_token в теле запроса (если включён Pass API key). Этот токен можно напрямую использовать с updateNotes для записи в хранилище.

Поле changes[] в ответе агента тоже поддерживает patch-операции — передайте "kind": "patch" с find и replace вместо content в объекте изменения. Изменение с "kind": "meta" и set / unset вместо content меняет ключи frontmatter по тем же правилам, что и операция meta. Подробнее о формате ответа агента — в документации вебхуков.

Cron-вебхуки запускают агентов по расписанию без внешней инфраструктуры. Например, агент может каждую ночь проверять заметки todo/**, находить просроченные задачи и обновлять их статусы через patch.


Паттерн маркера-якоря

Для дозаписи (inbox-боты, лог-записи) добавьте в заметку маркер и используйте patch для вставки контента с сохранением маркера:

Содержимое заметки:

# Inbox

$INBOX$

Вызов агента:

{
  "patch": {
    "path": "inbox.md",
    "find": "$INBOX$",
    "replace": "## 2026-05-18 10:00\nНовое сообщение\n\n$INBOX$"
  }
}

Каждый вызов добавляет новую запись перед маркером. Маркер остаётся в заметке и служит точкой вставки для следующего вызова. Паттерн работает без expectedHash — строка маркера уникальна по конструкции.


Паттерн условного вызова

Агент должен вызывать updateNotes только при наличии конкретного изменения:

получить payload вебхука
если changes[] содержит заметку с чекбоксом для переключения:
    вызвать updateNotes с patch (find=исходная строка, replace=изменённая строка)
    обработать PatchNotFound (дублирующийся текст) → вернуть ошибку в UI
    обработать HashMismatch → перечитать заметку и повторить
иначе:
    вернуть {"status": "ok"}  # нет изменений, запись не нужна

Семантика patch обеспечивает это автоматически — если строка find отсутствует, мутация возвращает ошибку, а не пишет молча. Агент, вызывающий мутацию безусловно, тоже получит корректное поведение.