Что произошло
На Windows convert.js не конвертирует файл, в имени которого есть пробел — в том числе пример из справки самого скрипта (node convert.js a.pdf b.xlsx "отчёт за март.pptx").
Причина: spawnSync('npx', ['-y', '@firecrawl/anydoc', f, '-o', out], { shell: isWin }). Под shell: true аргументы уходят в cmd.exe одной строкой, cmd режет её по пробелам, и anydoc получает второй «вход».
Тот же корень даёт второй случай: имя с %VAR% (например отчёт %TEMP% март.docx) — cmd раскрывает переменную, и в anydoc уходит уже другой путь (io error: The filename, directory name, or volume label syntax is incorrect. (os error 123)).
Отдельно, и по-моему важнее самого бага: исход категоризируется неверно. Оба случая печатаются как ПРОПУСК с советом «Пропущенные, скорее всего, сканы или PDF-картинки без текстового слоя — прогони их через OCR», хотя файл идеально читаемый. А process.exit(fail > 0 ? 1 : 0) даёт exit 0, потому что это skip, а не fail. В пакетном режиме по папке такой файл молча теряется при статусе «успех», а пользователя отправляют на бессмысленный OCR.
Как воспроизвести
Команда:
node skills/doc2md/scripts/convert.js "a b.docx"
Ожидал: OK
Получил:
ПРОПУСК C:\...\a b.docx — anydoc: one document per invocation: unexpected second input 'b.docx'
Готово: 0 конвертировано, 1 пропущено (не читается/зашифровано/скан), 0 ошибок вызова
Пропущенные, скорее всего, сканы или PDF-картинки без текстового слоя — прогони их через OCR (скилл pdf, шаг «сделать PDF searchable») и повтори.
Кириллица тут не при чём — проверял раздельно, данные.csv и отчёт.docx без пробела конвертируются нормально. Ломает именно пробел.
Окружение
- ОС и версия: Windows 11 Enterprise 26200, проверял и из
cmd.exe, и из PowerShell
- Node.js (
node -v): v26.2.0, npm 11.13.0
- Версия обёртки:
skills/doc2md/scripts/convert.js (кросс-платформенная Node-версия), клон main от 06.08.2026
- anydoc: через
npx -y @firecrawl/anydoc, версия 0.1.6 (глобально не ставил)
Секция «Протестировано» в SKILL.md просит перепроверить и дополнить её при первом боевом запуске на Windows — этим и занимаюсь. Вывод «convert.js написан без shell-специфичного синтаксиса … но это рассуждение о корректности, а не проверка» оказался ровно в точку.
Что помогло у меня — не настаиваю на способе
-
Квотировать аргументы под shell: true — лечит пробелы, но не лечит %VAR%: раскрытие переменных на командной строке cmd не экранируется.
-
Корневой фикс — убрать cmd.exe из цепочки. shell: true нужен только чтобы нашёлся npx.cmd; вместо этого можно позвать npx-cli.js напрямую тем же node, без shell:
const npxCli = path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npx-cli.js');
spawnSync(process.execPath, [npxCli, '-y', '@firecrawl/anydoc', f, '-o', out]);
После этого у меня прошли все имена: a b.docx, отчёт за март.docx, a&b^c%TEMP% d.docx — результат байт-в-байт совпал с эталоном. Оговорка: путь к npx-cli.js — деталь раскладки npm-бандла, а не публичный контракт, и на nvm-windows/Volta он может лежать не рядом с node.exe.
-
Глубже, и это, кажется, интереснее всего для самого скилла: у @firecrawl/anydoc есть официальный Node API. В установленном 0.1.6 есть toMarkdown(path), toMarkdownBytes, toDocument, formatFromPath и типизированный ConvertErrorCode (unsupported | malformed | encrypted | resourceLimit | missingPart | io), таргет x86_64-pc-windows-msvc. Если звать его вместо CLI:
- исчезает
cmd.exe и вместе с ним весь класс проблем с именами — чинить нечего, потому что ломаться больше нечему;
- исчезают накладные
npx на каждый файл. Замерил у себя: через npx ~1.02 с на файл (5 тривиальных CSV = 5.11 с) при собственной медиане anydoc ~4.4 мс; после перехода на API 20 файлов конвертируются за 111 мс. На пачке это порядка 100×, а пакетная обработка — ровно то, ради чего скилл и нужен;
- вместо угадывания по exit-коду приходит точная причина, и
malformed document больше не советует OCR. Сейчас, например, лок-файл Word ~$договор.docx попадает в обработку и рапортуется как «скорее всего скан».
Я в итоге так и сделал у себя и был неправ, когда сначала полез чинить обёртку вокруг CLI: правильный ход оказался не «квотировать лучше», а не звать cmd вообще.
Две мелочи, если пригодятся
-o последним аргументом молча игнорируется: outDir = argv[++i] даёт undefined, дальше if (outDir) ложно → всё пишется рядом с исходниками, без предупреждения, exit 0. Человек думает, что собрал результат в отдельную папку.
- При
-o и рекурсивном обходе имя выхода строится из path.basename(f), поэтому 2025\акт.pdf и 2026\акт.pdf дают один акт.pdf.md — второй молча перезаписывает первый, оба со статусом OK. Это тот же класс, что уже поправлен для отчёт.csv / отчёт.docx (сохранением расширения), только по другой оси — по подпапкам.
Спасибо за скилл: идея «сконвертировать пачку до того, как её начнёт читать агент» оказалась ровно тем, что было нужно, и basename + .md против коллизий, и отсечение циклов по симлинкам — приятно аккуратные детали.
Что произошло
На Windows
convert.jsне конвертирует файл, в имени которого есть пробел — в том числе пример из справки самого скрипта (node convert.js a.pdf b.xlsx "отчёт за март.pptx").Причина:
spawnSync('npx', ['-y', '@firecrawl/anydoc', f, '-o', out], { shell: isWin }). Подshell: trueаргументы уходят вcmd.exeодной строкой, cmd режет её по пробелам, и anydoc получает второй «вход».Тот же корень даёт второй случай: имя с
%VAR%(напримеротчёт %TEMP% март.docx) — cmd раскрывает переменную, и в anydoc уходит уже другой путь (io error: The filename, directory name, or volume label syntax is incorrect. (os error 123)).Отдельно, и по-моему важнее самого бага: исход категоризируется неверно. Оба случая печатаются как
ПРОПУСКс советом «Пропущенные, скорее всего, сканы или PDF-картинки без текстового слоя — прогони их через OCR», хотя файл идеально читаемый. Аprocess.exit(fail > 0 ? 1 : 0)даёт exit 0, потому что этоskip, а неfail. В пакетном режиме по папке такой файл молча теряется при статусе «успех», а пользователя отправляют на бессмысленный OCR.Как воспроизвести
Команда:
node skills/doc2md/scripts/convert.js "a b.docx"Ожидал:
OKПолучил:
Кириллица тут не при чём — проверял раздельно,
данные.csvиотчёт.docxбез пробела конвертируются нормально. Ломает именно пробел.Окружение
cmd.exe, и из PowerShellnode -v): v26.2.0, npm 11.13.0skills/doc2md/scripts/convert.js(кросс-платформенная Node-версия), клон main от 06.08.2026npx -y @firecrawl/anydoc, версия 0.1.6 (глобально не ставил)Секция «Протестировано» в SKILL.md просит перепроверить и дополнить её при первом боевом запуске на Windows — этим и занимаюсь. Вывод «
convert.jsнаписан без shell-специфичного синтаксиса … но это рассуждение о корректности, а не проверка» оказался ровно в точку.Что помогло у меня — не настаиваю на способе
Квотировать аргументы под
shell: true— лечит пробелы, но не лечит%VAR%: раскрытие переменных на командной строке cmd не экранируется.Корневой фикс — убрать
cmd.exeиз цепочки.shell: trueнужен только чтобы нашёлсяnpx.cmd; вместо этого можно позватьnpx-cli.jsнапрямую тем же node, без shell:После этого у меня прошли все имена:
a b.docx,отчёт за март.docx,a&b^c%TEMP% d.docx— результат байт-в-байт совпал с эталоном. Оговорка: путь кnpx-cli.js— деталь раскладки npm-бандла, а не публичный контракт, и на nvm-windows/Volta он может лежать не рядом сnode.exe.Глубже, и это, кажется, интереснее всего для самого скилла: у
@firecrawl/anydocесть официальный Node API. В установленном 0.1.6 естьtoMarkdown(path),toMarkdownBytes,toDocument,formatFromPathи типизированныйConvertErrorCode(unsupported | malformed | encrypted | resourceLimit | missingPart | io), таргетx86_64-pc-windows-msvc. Если звать его вместо CLI:cmd.exeи вместе с ним весь класс проблем с именами — чинить нечего, потому что ломаться больше нечему;npxна каждый файл. Замерил у себя: через npx ~1.02 с на файл (5 тривиальных CSV = 5.11 с) при собственной медиане anydoc ~4.4 мс; после перехода на API 20 файлов конвертируются за 111 мс. На пачке это порядка 100×, а пакетная обработка — ровно то, ради чего скилл и нужен;malformed documentбольше не советует OCR. Сейчас, например, лок-файл Word~$договор.docxпопадает в обработку и рапортуется как «скорее всего скан».Я в итоге так и сделал у себя и был неправ, когда сначала полез чинить обёртку вокруг CLI: правильный ход оказался не «квотировать лучше», а не звать cmd вообще.
Две мелочи, если пригодятся
-oпоследним аргументом молча игнорируется:outDir = argv[++i]даётundefined, дальшеif (outDir)ложно → всё пишется рядом с исходниками, без предупреждения, exit 0. Человек думает, что собрал результат в отдельную папку.-oи рекурсивном обходе имя выхода строится изpath.basename(f), поэтому2025\акт.pdfи2026\акт.pdfдают одинакт.pdf.md— второй молча перезаписывает первый, оба со статусомOK. Это тот же класс, что уже поправлен дляотчёт.csv/отчёт.docx(сохранением расширения), только по другой оси — по подпапкам.Спасибо за скилл: идея «сконвертировать пачку до того, как её начнёт читать агент» оказалась ровно тем, что было нужно, и
basename + .mdпротив коллизий, и отсечение циклов по симлинкам — приятно аккуратные детали.