Pi-bridge: как связать телефон с локальным ИИ-агентом и научить его присылать файлы
У меня есть локальный ИИ-агент - pi coding agent, который живёт на ноутбуке и работает секретарем, дизайнером, писателем, исследователем, программистом и тестировщиком (похоже на dots от OpenAI только без наворотов). Работает с кодом, файлами всех проектов, ведет учет всех задач и проблем по всем проектам и имеет полный доступ к bash в системе. Это дает возможность выполнять задачи по разным проектам, в том числе связанным между собой, с телефона голосом через чат.
Какое-то время было одно неудобство: пользоваться pi можно было только за ноутбуком. pi-web и пару других альтернатив ставить я сразу расхотел, когда посмотрел на размер их кода - из-за их монстроузности и простоты самой задачи моста. Это как если бы была нужна одна спичка, а тебе предлагают вагон с фейерверками. Поэтому сделали маленький мост pi-bridge, чтобы открывать его с телефона через браузер - и в итоге дошёл до того, что агент теперь сам присылает мне результаты не только текстом, но также аудио, видео и картинки прямо в чат.
Что это такое
pi-bridge - это лёгкий Node.js-мост между браузером на телефоне и pi-агентом на ноутбуке. Схема простая:
телефон → nginx на домашнем сервере с доменом и внешним IP (TLS + basic auth) → pi-bridge на ноутбуке → pi в режиме RPC
Мост поднимает страницу с живым логом сессии, стримингом текста, вызовами инструментов, мульти-сессиями, выбором модели. Всё на одном файле server.js (HTTP + WebSocket) плюс index.html и безопасный markdown-рендерер без внешних CDN. Авторизация настраивается на nginx одной строкой.
Задача: показать файлы с хоста
Хочется спросить агента: напиши мне стихи про осень через чат arena, потом забрось их в suno, сгенерируй песни по стихам, скачай полученные треки и пришли их мне - и он присылает готовые аудио прямо в чат списком воспроизводимого аудио. То же работает для картинок и видео.
Оказалось, что в pi нет нативного канала «отдай файл клиенту». Поэтому выбрали такую схему:
байты файлов отдаёт HTTP-эндпоинт /media самого моста;
агент объявляет медиа через инструмент send_media, который возвращает ссылки на эти файлы;
страница рисует карточки с плеерами и миниатюрами.Как устроен send_media
Это наше расширение pi (extension/send-media.js). Регистрируется через официальный API pi - pi.registerTool(): имя, описание, JSON-схема параметров и функция execute. Загружается так: мост при спавне pi добавляет аргумент --extension:
pi --mode rpc --extension .../extension/send-media.js
Важно: расширение видно только в сессиях моста - в обычных сессиях pi на ноутбуке этого инструмента нет.
Когда модель вызывает send_media с путями, execute проверяет:
путь существует и лежит внутри разрешённых корней (MEDIA_DIRS) - realpath + containment, без симлинков наружу и ../;
расширение в белом списке (m4a, mp3, wav, ogg, flac; jpg, png, webp, gif, bmp, avif; mp4, webm, mov);
это обычный файл, не каталог.
Если путь плохой - инструмент возвращает понятную ошибку, и модель сама её исправляет.
Эндпоинт /media
GET /media?path=... отдаёт файл с соблюдением всех тонкостей:
поддержка Range-запросов: 206, Content-Range, стрим через fs.createReadStream(start, end) - без этого перемотка аудио на телефоне не работала бы;
ETag и 304, чтобы лишний раз не качать;
?dl=1 - скачивание как вложение;
заголовки nosniff и CSP default-src 'none', HTML/SVG/JS никогда не отдаются inline.
Как это выглядит у пользователя
В чате появляется сворачиваемая карточка «5 медиа: 5 аудио» с плеерами, названиями и комментариями. Картинки - грид миниатюр. Если файл пропал - вместо плеера подпись «файл недоступен».
Карточка живёт в истории: даже после переподключения телефона она восстанавливается из файла сессии.Что было сложного
Сначала не заработало с телефона: URL были вида /media?path=..., а страница открыта по адресу /piweb/, и браузер резолвил их от корня домена. Лечится переходом на относительные URL - media?path=...
Пути к файлам не стоит светить абсолютными: nginx на VPS пишет их в логи. Поэтому в URL всегда относительный путь от корня.
Параметр MEDIA_DIRS
По умолчанию корень - ~/myprjs/Ideal. У меня мост запущен с MEDIA_DIRS=~/myprjs, чтобы агент мог показывать файлы из любых проектов, а не только из одного.
Тесты
npm test - 36 автоматических проверок: эндпоинт /media (200, 206, 416, 403, 404, 304), сквозной вызов send_media, восстановление карточки из истории.
Пара команд
Проект - чистый Node.js, одна зависимость (ws). Развернуть и запустить:
git clone https://github.com/NikasAl/pi-bridge
cd pi-bridge && npm install
MEDIA_DIRS=~/myprjs PI_BRIDGE_CWD=/путь/к/проекту node server.js
Проверка, что мост жив, и тесты:
curl -s localhost:8765/healthz
npm test
Итог
Теперь с телефона можно просить агента прислать треки, картинки, обложки - и они появляются в чате, их можно слушать и смотреть. Всё это на чистом Node.js без тяжёлых зависимостей, с безопасной раздачей файлов через nginx.
Ссылка на репозиторий: https://github.com/NikasAl/pi-bridge