From bd0f619585333c92cf85bd0e0616ddc6e9099a59 Mon Sep 17 00:00:00 2001 From: Porfiry Date: Sat, 19 Sep 2026 16:32:52 +0300 Subject: [PATCH] =?UTF-8?q?STORAGE.md:=20=D0=B3=D0=B4=D0=B5=20=D1=85=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D1=8F=D1=82=D1=81=D1=8F=20=D0=BD=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=B9=D0=BA=D0=B8,=20=D0=B3=D1=80=D1=83=D0=BF?= =?UTF-8?q?=D0=BF=D1=8B,=20=D0=BA=D1=8D=D1=88=20=D0=B8=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20=D0=B8=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=B4=D0=B8=D0=B0=D0=BB=D0=BE=D0=B3=D0=BE=D0=B2;=20?= =?UTF-8?q?=D0=B4=D1=8B=D1=80=D0=BA=D0=B8=20=D0=B2=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 +-- STORAGE.md | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+), 6 deletions(-) create mode 100644 STORAGE.md diff --git a/README.md b/README.md index 04ee4e2..d89d507 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ approved/001-sidebar-utility.html # ОСНОВА: привычный approved/004-settings-agents.html # настройки агентов + проверка связи approved/005-new-chat-picker.html # модалка «Новый диалог» approved/006-groups-manage/ # группы: 4 файла по состояниям +approved/006-groups-manage/1-list.html # Группы: изменить, удалить, добавить +approved/006-groups-manage/2-add.html # новая группа +approved/006-groups-manage/3-edit.html # изменение и удаление +approved/006-groups-manage/4-delete.html # удаление: диалоги остаются approved/README.md # что одобрено и когда ``` @@ -26,17 +30,13 @@ sketches/003-three-pane-command/index.html # три панели + состо sketches/004-settings-agents/index.html # черновик настроек агентов sketches/005-new-chat-picker/index.html # черновик модалки нового диалога ``` -``` -approved/006-groups-manage/1-list.html # Группы: изменить, удалить, добавить -approved/006-groups-manage/2-add.html # новая группа -approved/006-groups-manage/3-edit.html # изменение и удаление -approved/006-groups-manage/4-delete.html # удаление: диалоги остаются -``` `BORROW-FROM-ASSISTENT.md` — что берём из `ai/assistent` (приёмы, плагины, версии), а что не берём (архитектура, экраны, дизайн — своё). `MIC-ASR-SEARCH.md` — библиотеки для микрофона и распознавания: своё готовое (`mic-kmp`, `asr-kmp`, `vad-kmp`), вместо копирования кода из assistent. +`STORAGE.md` — где что хранится: настройки, группы, кэш истории, список +диалогов; почему файлы, а не база; что менять в API (превью и число непрочитанных). `CACHE.md` — кэш сообщений на клиенте: что для него есть в библиотеке, схема загрузки «только новое», где может порваться. `THEMES.md` — цветовые темы: цвета не пишем в коде, берём из схемы. Тем будет diff --git a/STORAGE.md b/STORAGE.md new file mode 100644 index 0000000..8a0a500 --- /dev/null +++ b/STORAGE.md @@ -0,0 +1,250 @@ +# Где что хранится на диске + +**Вопрос:** где живут настройки, кэш истории и список диалогов. + +**Ответ коротко:** всё лежит в одном каталоге клиента. Никакой базы не нужно — +история и есть поток строк, а список диалогов вообще кэшировать не надо так, +как историю. Ниже — почему, и что для этого уже есть в библиотеке. + +Всё сверено с исходниками `agentik`, не по памяти. + +--- + +## 1. Карта: что где лежит + +``` +~/.agentik/ каталог клиента (на Windows — соответствующий системный) + settings.json настройки: агенты, адреса, тема, выбранная группа + groups.json группы и в какой группе какой диалог + dialogs.json снимок списка диалогов — чтобы окно не было пустым + read.json докуда дочитан каждый диалог (для счётчиков) + history/ + .jsonl сообщения: одна строка — одно сообщение + blobs/ + картинки из сообщений, отдельными файлами +``` + +Один каталог — простое правило: **удалил каталог, клиент чистый.** Ничего не +прячется в других местах. + +### Почему файлы, а не база + +История диалога — **только дописывается**: сообщений не правят и не удаляют +(в библиотеке так и написано: «Только `insert` и чтение. Никаких обновлений»). +Для дописываемого потока база не нужна: + +- **Строка на сообщение.** Читаем конец файла — знаем последнее сообщение. + Дописываем в конец — вот и весь кэш. +- **Понятно человеку.** Открыл файл — увидел сообщения. В случае беды можно + посмотреть глазами и починить руками. +- **Нет лишней зависимости.** База — это драйвер, версии, миграции схемы. + +Плюс это прямо соответствует тому, ради чего мы вообще выбрали десктоп +эталоном: код, который **можно прочитать и понять**. + +--- + +## 2. Настройки — один файл + +Пара агентов, адреса, тема, выбранная группа. Это десяток полей — +им не нужна база и не нужен отдельный файл на каждую настройку. + +Важно: **настройки человек правит руками.** Если клиент однажды не запустится из-за +кривого адреса, должно быть можно открыть `settings.json`, увидеть и исправить. +Поэтому формат — обычный читаемый, а не сжатый. + +Отдельно: **секреты не в этом файле.** Если у агента будет пароль/ключ, +он кладётся в системное хранилище паролей, а в настройках остаётся только +ссылка на него. Иначе ключ утечёт вместе с настройками, которые человек +может кому-то переслать. + +--- + +## 3. Группы — они существуют только у нас + +**Это важное. Сервер про группы ничего не знает.** + +В библиотеке диалог описывается шестью полями: `id`, `title`, `isTemporal`, +`createdAt`, `updatedAt` и два признака про картинки. **Поля «группа» там нет.** +Значит, группы — целиком наша выдумка на стороне клиента, и жить они будут +в `groups.json`. + +**Что из этого следует, и это надо решить:** телефон и десктоп **разойдутся**. +Создал группу «Работа» на десктопе — на телефоне её нет. Отсюда два пути: + +1. **Группы только на устройстве.** Просто, но раскладка не ездит между + устройствами. Для одного человека за одним компьютером — нормально. +2. **Группы на сервере.** Тогда нужно добавить в библиотеку хранение групп — + это уже не «просто клиент», это расширение самого agentik. + +Пока в макетах нарисован путь 1. Если хотим 2 — это отдельное решение, +и его лучше принять **до** того, как начнём писать клиент. + +--- + +## 4. Кэш истории — файл на диалог + +Схема целиком описана в `CACHE.md`. Здесь — только где лежит. + +**Файл на диалог, одна строка на сообщение.** Причина простая: диалоги друг +с другом не связаны. Открыли один — читаем один файл, чужого не касаемся. +Заодно один битый файл не портит остальные. + +**Как читаем:** храним в памяти, на чём остановились (последняя прочитанная +строка). При открытии диалога читаем конец файла — это последнее сообщение, +с него и спрашиваем сервер «что новее». Весь файл при каждом открытии не +перечитываем. + +**Картинки — отдельно.** В сообщении картинка едет как массив байт. Держать +её внутри строки истории нельзя: файл распухнет, и станет нечитаемым. +Поэтому картинка кладётся в `blobs/`, а в истории остаётся +ссылка на неё. + +### Три риска, честно + +1. **Обрыв записи.** Клиент упал посреди записи строки — в конце файла + остался обрубок. Тогда: обрубок отбрасываем, спрашиваем сервер заново + по последней целой строке. Так как сервер — источник правды, потеря + не страшна. +2. **Очень длинный диалог.** Файл растёт. Пока спасает то, что читаем конец, + а не весь файл. Если однажды станет тяжело — режем историю на части + по месяцам, но это потом, не сейчас. +3. **Файлов много.** Да, на каждый диалог свой файл. Но файлы мелкие, + а система умеет держать миллионы файлов. Проблемой это станет на порядки + позже, чем что-то другое. + +--- + +## 5. Список диалогов — кэшировать не надо (в главном) + +Вот тут отличие от истории, и оно важное. + +| | История диалога | Список диалогов | +|---|---|---| +| Сколько данных | много сообщений, каждое с текстом | сто строк, в каждой имя и дата | +| Размер одной порции | сотни килобайт | несколько килобайт | +| Растёт | на каждом ответе агента | медленно | + +**Вывод: список диалогов надо честно спрашивать у сервера.** Причины: + +- **Он лёгкий.** Сто диалогов — это несколько килобайт. Одна быстрая просьба, + а не выкачивание истории. +- **Он всегда свежий.** Список меняется от чужой работы агента (он может вести + другой диалог, пока мы смотрим этот). Кэшировать его и синхронизировать + приростом — это сложная механика ради нескольких килобайт. +- **Много диалогов — уже решено.** В библиотеке есть постраничная выдача: + берёт по 100 штук и подгружает следующие страницы по мере надобности. + Отдельно ничего придумывать не надо. + +### Но пустое окно при запуске — реальная беда + +Проблема не в размере списка, а в том, что **пока он едет, окно пустое.** +Поэтому в карте выше и лежит `dialogs.json` — **снимок** списка с прошлого раза. + +Схема такая: + +1. Запустились — рисуем список из снимка. Мгновенно, окно не пустое. +2. Одновременно спрашиваем сервер свежий список. +3. Пришёл — заменяем нарисованное целиком. + +Это **не кэш**, а занавеска, чтобы не смотреть в пустоту. Никакой сверки +прироста, никаких сложных правил: пришёл свежий список — взяли его целиком. +Снимок всегда считается устаревшим. + +Если снимка нет (первый запуск) — показываем «загружаю». + +--- + +## 6. Что в API не хватает + +Проверено по коду библиотеки. Вот что есть и чего нет для того, что +нарисовано в макетах. + +| Что нужно в списке | Есть в библиотеке? | +|---|---| +| Название диалога | **Есть** — `title` | +| Время последнего изменения | **Есть** — `updatedAt`, список уже отсортирован по свежести | +| Понять, что диалог удалён или переименован | **Есть** — события `Deleted`, `Renamed` | +| Постраничная выдача | **Есть** — по 100 штук | +| **Последнее сообщение строкой** | **Нет!** | +| **Сколько непрочитанных** | **Нет!** | +| **Событие «в диалоге что-то произошло»** | **Нет!** | +| **Группа диалога** | **Нет!** | + +Три дырки разберём отдельно — они разные по последствиям. + +### 6.1. Превью последнего сообщения — нет + +В макете в каждой строке под именем стоит текст: «Собрал отчёт, жду правок». +**Взять его неоткуда.** В списке диалогов только имя и дата. + +Варианты: + +- **Добавить в библиотеку.** В том же хранилище лежат сообщения — превью + берётся одним запросом рядом со списком. Это правильный путь: один запрос + за списком, как и было. +- **Спрашивать по диалогу.** Сто диалогов — сто запросов. Не годится. +- **Убрать из макета.** Строки станут суше, зато ничего не меняем. + +### 6.2. Счётчик непрочитанных — нет + +В макете справа в строке стоит «2». **Числа взять неоткуда.** + +Тут два уровня, и они разной цены: + +- **Точка «есть новое»** — сервер менять **не нужно**. У нас уже есть + `updatedAt` (когда диалог последний раз менялся) и свой `read.json` + (докуда дочитали). Есть новое = `updatedAt` новее нашей отметки. +- **Число непрочитанных** — **нужен сервер.** Чтобы получить «2», надо знать, + сколько сообщений в диалоге всего: тогда число = всего минус прочитанное. + Одно поле в ответе списка, считается по той же базе. + +### 6.3. «В диалоге что-то произошло» — события нет + +В потоке агента есть три события: **создан, удалён, переименован.** +События «пришло новое сообщение» **нет** — хотя сервер в этот момент как раз +обновляет `updatedAt` диалога (это видно по коду: `touch(id, now)`). + +**Что это значит на практике:** агент работает в другом диалоге, а мы в этот +момент смотрим список — **список сам не обновится.** Ни порядок не поедет, +ни точка «есть новое» не загорится, пока мы не спросим сервер заново. + +Пути: + +- **Обновлять список самому, раз в несколько секунд.** Без изменения API. + Список лёгкий (см. §5) — это честно и дёшево. Плюс обновлять при + возвращении окна в фокус. +- **Добавить событие.** Тогда список живёт сам, без опроса. Но опрос всё + равно остаётся как страховка от обрыва связи. + +Первый путь достаточен. Второй — приятная добавка на потом. + +### 6.4. Итог: что менять в API + +**Минимум, который закрывает макеты:** + +1. **Превью последнего сообщения** в ответе списка диалогов. +2. **Число непрочитанных** в ответе списка диалогов. + +Обе правки — в том же хранилище, рядом с тем, что уже читается для списка. +Ни новых таблиц, ни новых запросов со стороны клиента: как был один запрос +за списком, так и остался. + +**Не обязательно:** событие «в диалоге что-то произошло» — вместо него опрос +раз в несколько секунд. Можно отложить. + +**Нужно решить отдельно:** группы (§3). Это не правка ответа, это новое +понятие в библиотеке. + +--- + +## 7. Открытые вопросы + +1. **Группы — на устройстве или на сервере?** От этого зависит, поедут ли они + между десктопом и телефоном (§3). +2. **Меняем API: превью и число непрочитанных?** Или убираем их из макета + и живём на том, что есть (§6.4). +3. **Что засчитывать сообщением** при подсчёте непрочитанных: только ответы + агента или ещё вызовы инструментов, которые в истории тоже лежат + отдельными записями. +4. **Бейдж — число или точка?** Точка не требует менять сервер вообще (§6.2).