Plank help · updated 2026-09-17
Запуск команд на машине
Что реально установлено на машине рабочего пространства, а чего нет: имя интерпретатора, где лежат пакеты Python, каких утилит не хватает и куда можно писать временные файлы.
Agents: fetch the raw markdown of this page at /ru/help/running-commands.md
Запуск команд на машине
Здесь собраны факты о машине рабочего пространства, из-за которых чаще всего теряется ход. Каждый наблюдался как реальная неудачная первая попытка. Прочитайте перед первым вызовом bash в рабочем пространстве и считайте это важнее любых предположений, перенесённых из другого окружения.
О том, что машина собой представляет — где работает, что сохраняется, как устроен вход в сервисы, — см. Среда вашего ассистента.
Что есть, а чего нет
| Предположение | Как на самом деле |
|---|---|
python | Не существует. Есть только python3. Голый python падает с command not found. |
| Пакеты Python стоят глобально | Дополнительные пакеты ставятся в пользовательский site-packages (~/.local/lib/python3.X/site-packages) командой pip install --user — это домашняя папка, которая у общего командного чата не та же, что у личного. |
PYTHONNOUSERSITE=1 безобиден | Он ломает все доустановленные пакеты, включая все библиотеки Google. Никогда его не задавайте. |
file, rg, java, xz доступны | Ничего из этого не установлено. Для разбора файлов используйте python3, вместо rg — grep (или инструмент grep). |
/tmp/opencode доступен на запись | Он принадлежит root, а вы работаете под обычным пользователем. Пишите во временные файлы вида /tmp/<имя> или, лучше, прямо в рабочее пространство. |
Предустановлено и на это можно опираться: python3 (с openpyxl и requests в системном пути), node, curl, grep, sed, awk, tar, gzip, а также набор инструментов для документов, описанный ниже.
Пакет, поставленный через pip install --user, ведёт себя не так, как предустановленный. openpyxl и requests лежат в системном пути и переживают всё. Всё, что вы доустановили сами, включая клиентские библиотеки Google, лежит в пользовательском site-packages и исчезает в тот момент, когда задан PYTHONNOUSERSITE. Эта разница незаметна ровно до того момента, когда становится заметной.
Проверяйте свои документы, посмотрев на них
Файл .pptx, .docx или .xlsx можно превратить в PDF, а затем в картинку — поэтому смотрите на собранный документ, а не предполагайте, что он получился правильно. Текст, вылезший за границы блока, заголовок, перенесённый на три строки, диаграмма, закрывшая подпись, — ничего из этого не видно в файле, который вы записали, и всё это видно на картинке.
# 1. конвертация (обе переменные окружения обязательны, см. ниже)
SAL_USE_VCLPLUGIN=svp soffice -env:UserInstallation=file:///tmp/lo \
--headless --convert-to pdf deck.pptx --outdir /tmp
# 2. растрируем страницу, которую можно открыть и рассмотреть
pdftoppm -png -r 110 /tmp/deck.pdf /tmp/page
- Обе переменные обязательны. Просто
sofficeпадает с «User installation could not be completed»;-env:UserInstallationдаёт ему профиль, доступный на запись, аSAL_USE_VCLPLUGIN=svpвыбирает headless-бэкенд. Дисплея к машине не подключено. - Запускайте по одному файлу за раз. Два процесса
sofficeс общим каталогом профиля будут мешать друг другу; если запуски всё же пересекаются, задайте каждому свой путь-env:UserInstallation. - Также доступны:
pdftotext(проверить, что текст действительно попал в файл),markitdown(выгрузить собранный документ обратно в markdown — удобно, чтобы поймать забытый текст-заглушку),pandoc,wkhtmltopdf.
Для HTML используйте Chromium, а не wkhtmltopdf. wkhtmltopdf — это форк WebKit 2012 года: flexbox и grid поддержаны частично, веб-шрифты работают ненадёжно, поэтому страница, которая в браузере выглядит правильно, может отрендериться сломанной. chrome-headless-shell установлен и печатает в том размере страницы, который задан правилом @page самого документа:
chrome-headless-shell --no-sandbox --disable-gpu --no-pdf-header-footer \
--user-data-dir=/tmp/chrome-$$ --print-to-pdf=/tmp/out.pdf /abs/path/page.html
--no-sandboxобязателен — контейнер не даёт user namespaces, которые нужны собственной песочнице Chromium, и без этого флага браузер завершится, не загрузив страницу.- Каждому запуску — свой
--user-data-dir. Два процесса Chromium с общим профилем блокируют его, и второй ничего не запишет. - Размер страницы задаёт документ, а не команда. Флага для этого нет: пропишите
@page { size: 1280px 720px; margin: 0 }в CSS, иначе получите US Letter. - Никогда не используйте
repeating-linear-gradient,repeating-radial-gradientиconic-gradientна странице, которую собираетесь печатать. Градиент в PDF — это переход цвета вдоль прямой или окружности, поэтомуlinear-gradientиradial-gradientпереводятся напрямую и ничего не стоят. Эти три не переводятся, и Chromium заменяет их шейдингом, цвет которого вычисляет PostScript-программа, — её читалка выполняет для каждого пикселя. Preview на macOS и iOS растрирует всю ячейку паттерна до отсечения, поэтому цена определяется размером блока, на котором висит градиент, а не тем, сколько его видно. Замеренная колода из двенадцати слайдов открывалась 43 секунды, из них 26 — титульный слайд, и 27 секунд из 43 ушли на градиент, который был полностью отсечён и не нарисовал ни одного пикселя. До конца отрисовки страница остаётся пустой, поэтому это выглядит как битый файл, а не как медленный. Для сетки в волосяную линию или для полос используйте повторяющийся SVG вbackground-image(data:-URI с одной плиткой) — он остаётся векторным и рисуется мгновенно; для развёртки используйтеlinear-gradientпод углом. - Проверьте напечатанный PDF — в исходнике этого не видно:
python3 -c "import re,sys;print(len(re.findall(rb'/ShadingType\s+1\b',open(sys.argv[1],'rb').read())))" out.pdf. Любое значение больше0означает, что страница будет открываться медленно.plank_deck_qa.pyделает эту проверку сам и заваливает колоду по ней.
Шрифты, если в документе есть русский или казахский текст. Используйте Arial, Calibri, Times New Roman, Courier New или Inter — для каждого установлена замена с полным покрытием кириллицы, включая Әә Ғғ Ққ Ңң Өө Ұұ Үү Һһ Іі. Избегайте Cambria: замены для неё здесь нет, поэтому она незаметно подменяется посторонним начертанием, и вёрстка, которую видите вы, — не та, что получит пользователь. Если на растрированной странице текст выглядит как квадратики или интервалы явно неправильные, меняйте шрифт, а не отправляйте файл как есть.
Node-пакеты для сборки документов уже стоят глобально. pptxgenjs, sharp, react, react-dom и react-icons установлены на уровне образа, а NODE_PATH задан, так что require('pptxgenjs') работает откуда угодно. Не ставьте их через npm install в рабочее пространство: это медленно, создаёт тысячи файлов на сетевом хранилище и не нужно.
Две привычки, которые экономят больше всего ходов
- Гасите лишний вывод максимально узко. Библиотеки Google на каждом запуске печатают
FutureWarningпро версию Python. Заглушайте его черезPYTHONWARNINGS=ignore— и никогда черезPYTHONNOUSERSITE, который «убирает предупреждение» вместе с самой библиотекой. - Пишите файл скрипта вместо длинного однострочника
python3 -c. Однострочники с вложенными кавычками — вторая по частоте причина падений здесь: оболочка превращает их вSyntaxErrorили разбивает на посторонние команды. Файл к тому же можно запустить повторно, а с заголовком@plank-integrationон появится в боковой панели пользователя. См. Скрипты рабочего пространства и боковая панель.
Оболочка неинтерактивна — команда, ждущая ввода, вешает ход
К машине не подключён терминал, и ответить на запрос некому. Команда, которая останавливается и что-то спрашивает — код входа, учётные данные, подтверждение [y/N], пейджер в ожидании нажатия клавиши, — не получает ответа и висит, пока ход не завершат принудительно (примерно через 30 минут), а пользователь видит это как проваленный ход. Считайте любую интерактивную команду убийцей хода.
- Никогда не запускайте интерактивный вход —
gcloud auth login,az login, клиент базы данных, спрашивающий пароль. Для OAuth используйте релей входа: сгенерируйте URL, дайте пользователю подтвердить его в своём браузере и завершите в следующем ходе, когда он вставит обратно URL перенаправления. Не держите процесс живым в ожидании. - Обычные виновники заранее настроены падать быстро, а не висеть. Образ задаёт
CLOUDSDK_CORE_DISABLE_PROMPTS=1(gcloud не спрашивает),GIT_TERMINAL_PROMPT=0(git не запрашивает учётные данные) иPAGER=cat(без пейджера). Когда что-то из этого падает с ошибкой вместо зависания — прочитайте ошибку и не запускайте ту же интерактивную команду снова. Но не полагайтесь на то, что это перехватит любой инструмент: для всего остального всё равно берите явный неинтерактивный флаг ниже. - Для всего остального включайте неинтерактивный режим сами: передайте флаг инструмента (
-y,--yes,--no-input,--non-interactive) или перенаправьте ввод из пустоты через< /dev/null. Если задачу действительно нельзя выполнить без интерактивного ввода, честно скажите об этом, а не вешайте ход.
Как запускать долгую задачу в фоне
Задача на несколько минут — синхронизация, массовый импорт, долгая отрисовка — должна уходить в фон, чтобы ход оставался свободным и вы могли отвечать пользователю, пока она идёт. Чтобы это действительно случилось, нужны две вещи, и каждая из них уже ломалась по отдельности — с замерами.
1. Запуск — это отдельный вызов bash. Перед ним в строке ничего нет: ни cd … &&, ни … ;, ни другой команды. A && B & не отправляет в фон B — он отправляет туда весь список, в подоболочке, которая наследует канал вывода инструмента, и вызов не вернётся, пока этот канал не закроется. Замер на шестисекундной задаче: сама оболочка завершается через 0.012 с, а канал закрывается через 6.02 с — то есть вызов занимает все шесть секунд. В проде фоновая синхронизация, запущенная составным выражением, удерживала один вызов bash 616.327 с; тот же запуск отдельным вызовом возвращается за 0.284 с. Пишите абсолютные пути или передавайте параметр workdir инструменту оболочки — но не cd. Инструмент определяет рабочий каталог на каждый вызов, и его собственная инструкция велит не писать cd <каталог> && <команда>, а передавать workdir: cd из прошлого вызова в этот не переносится. (; перед запуском такую подоболочку не порождает и возвращается сразу — правило запрещает его всё равно: иначе приходится каждый раз выводить приоритет & и && в момент написания запуска, а одна всегда верная форма не стоит ничего. Запрет && и ; — про запуск: обычные команды, которые ничего не отправляют в фон, можно соединять как угодно. Правило про cd — нет: оно действует для любого вызова, потому что ни один вызов не наследует каталог предыдущего.)
2. Оба потока вывода идут в файл журнала. > job.log 2>&1. Задача, у которой stdout остался каналом инструмента, держит вызов открытым ровно так же — с & или без: замер показывает, что nohup sleep 6 & без перенаправления возвращается через 6.0 с.
mkdir -p /workspace/logs # отдельным вызовом: перед запуском ничего быть не должно
nohup python3 -u /workspace/scripts/sync.py > /workspace/logs/sync.log 2>&1 &
echo "started pid $!"
Каталога /workspace/logs/ в новом рабочем пространстве нет — создайте его отдельным вызовом до запуска. Если каталога для перенаправления не существует, запуск не запишет вообще ничего, и вы будете опрашивать журнал, который никогда не появится.
-u здесь несущий, а не украшение. Когда stdout Python указывает на файл, он буферизуется блоками, поэтому задача, печатающая строку прогресса каждые 15 секунд, не пишет в журнал вообще ничего, пока не завершится или не наберёт ~8 КБ. Замер: через три секунды после старта задачи, печатающей раз в секунду, журнал пуст без -u и содержит три строки с ним. Пустой журнал неотличим от задачи, которая не запустилась, — именно его нашли пять из шести чтений журнала в том инциденте. Для не-Python используйте stdbuf -o0 -e0 <команда> — это снимает буферизацию с обоих потоков. Не -oL и не -eL: построчная буферизация сбрасывает по переводу строки, а команда, печатающая прогресс как \rprogress 42, его не пишет — с любым из этих флагов вывод остаётся в буфере ровно так же, как если бы stdbuf не было вовсе: замер даёт 0 байт за 3 секунды в обоих случаях против 44 с -o0. -o0 не дороже -oL: оба укладываются примерно в 285 мс на 200 000 строк, а разброс между прогонами шире разницы между ними.
Фоновая задача сообщает о прогрессе не реже чем раз в 15 секунд
Пользователь спрашивает «работает?» задолго до конца задачи, и отвечаете вы по журналу. Значит, журнал должен что-то сказать в течение 15 секунд после запуска и дальше не реже чем раз в 15 секунд. Задача, которая печатает только в конце, невидима: в том инциденте это дало 319 секунд слепого опроса, пять пустых чтений журнала из шести и три сообщения пользователя «оно ещё идёт?», на которые нечем было ответить.
- Скрипт, который вы написали сами: печатайте строку с отметкой времени на каждой границе шага и не реже раза в 15 секунд — и не забывайте
-u, иначе она не выйдет из буфера. Это тот случай, которым вы управляете, и именно он важен: сделайте так, чтобы задача говорила сама. - Команда, которую вы изменить не можете: заставить её говорить нельзя — и не делайте вид, что можно. Без всякой обёртки вокруг неё честно наблюдаются две вещи:
- размер журнала и время его изменения. Рост между двумя опросами — это движение; отсутствие роста не доказывает, что задача зависла, но именно об этом вы и сообщаете.
- жив ли запущенный вами процесс. Запуск напечатал его pid, так что
kill -0 <pid>отвечает на это одной командой.
kill -0 12345 2>/dev/null && echo "ещё работает" || echo "запущенный мной процесс завершился"
ls -l --time-style=+%H:%M:%S /workspace/logs/sync.log
- Сообщайте о том, что наблюдали, а не об итоге, которого не видели. «Процесс работает, журнал вырос на 4 КБ за последнюю минуту» — честно. «Синхронизация завершилась» — утверждение, которое можно делать, только если об этом сказала сама задача в журнале. Команда, уходящая в собственную сессию, с точки зрения вашей оболочки завершается сразу, а работа продолжается, — поэтому исчезнувший pid означает запущенный мной процесс завершился, а не задача готова.
- Дальше опрашивайте, а не гадайте.
plank_waitна 15-30 секунд, затемtail -n 20 /workspace/logs/sync.log— и расскажите пользователю, что там на самом деле написано.plank_waitзаканчивается в тот момент, когда пользователь пишет в чат, поэтому его сообщение доходит до вас за секунды. - Если журнал не появился вовсе, запуск не состоялся: перечитайте вывод самой команды запуска — оболочка сообщает о неудачном перенаправлении (например, об отсутствии
/workspace/logs) прямо там. - Чего это не делает: задача от этого не заканчивается быстрее. Собственная фоновая синхронизация коннектора была замерена в 463-958 секунд и занимает ровно столько же. Меняется другое: вы можете отвечать, пока она идёт, а пользователь видит движение.
Учётные данные не попадают в вывод команды
Стенограмма постоянна. Ключ, напечатанный в неё, обратно уже не забрать: удаление файла, откуда он взялся, ничего не меняет, а некоторые ключи не умеют перевыпускать сами себя. Поэтому правило не «обращайтесь с секретами аккуратно», а никогда не давайте секрету стать выводом.
- Никогда не трассируйте команду, читающую файл с учётными данными.
bash -x,set -xиsh -xпечатают каждое присваивание, которое выполняют, — так что трассировка скрипта сsource .envнапечатает весь файл. Оболочка вас не остановит: машина намеренно не подменяетsource, потому что это меняет поведение всех остальных подключаемых файлов. Здесь всё зависит от вас. - Читайте одно значение, а не файл.
grep -m1 '^POSTHOG_PERSONAL_API_KEY=' .env | cut -d= -f2-в переменную — и дальше работайте с переменной. Никогда не делайтеcat,echo,headилиprintfпо файлу с учётными данными и не выводите переменную с ключом, чтобы проверить, задана ли она: проверяйте через[ -n "$VAR" ]. - В командной строке ключам тоже не место.
curl -H "Authorization: Bearer $TOKEN"— нормально; вставленный туда сам токен — нет, потому что команда показывается рядом со своим выводом. - Если что-то всё же просочилось, скажите об этом в том же ходе. Plank затирает значения, которые опознаёт как учётные данные, до того как они сохранятся, — на месте значения вы увидите
[plank:secret-redacted]. Это подстраховка, а не разрешение: назовите пользователю, какой ключ засветился, чтобы он его перевыпустил, — действующий ключ остаётся утечкой.
Если чего-то действительно не хватает
Ставьте через pip install --user <пакет> для Python или npm install --prefix scripts/<provider> <пакет> для Node, чтобы пакет лежал рядом со скриптом, которому он нужен. Никогда не ставьте пакеты в .opencode/node_modules — это дерево плагинов образа, и оно сбрасывается из образа. Пакет из pip install --user лежит в домашней папке, которая у общего командного чата не та же, что у личного, поэтому для скрипта, который запускают чаты обоих видов, его, возможно, придётся поставить заново. Не тянитесь к apt-get: вы не root, да и системный пакет всё равно не переживёт пересоздание машины. Если задача действительно требует отсутствующей программы, честно скажите об этом, а не жгите ходы на обходные пути.