Siri с Apple Intelligence на Mac с русским языком: как система принимает это решение и как её переубедить

Новая Siri с Apple Intelligence не работает на системе с русским языком. Не «работает хуже» — её просто нет в настройках. То же самое у поляков, чехов, греков, венгров, израильтян и ещё полутора десятков языков, которые Apple пока не поддерживает.
Я полез разбираться, где именно проходит эта граница, и выяснилось кое-что интересное: macOS проверяет язык системы ровно один раз — при загрузке. Дальше ей всё равно.
Загрузился с английским — Siri AI включилась. Вернул свой язык — она продолжает работать до следующей перезагрузки. Никаких патчей, никаких системных файлов: одна загрузка на английском, а дальше живи как жил.
Ниже — как я это проверял, где система прячет своё решение, и во что упирается попытка автоматизировать такой круг.
Где лежит решение
Первый вопрос: как вообще узнать, поднялась Siri AI или нет. Читаемого публичного признака нет — ни ключа в настройках, ни API. Пришлось идти в системный журнал.
Нужные события пишут три процесса: Siri, Siri AI и generativeexperiencesd. Предикат получился такой:
(process == "Siri" OR process == "Siri AI" OR process == "generativeexperiencesd")
AND (eventMessage CONTAINS[c] "Enhanced Siri"
OR eventMessage CONTAINS[c] "LanguageIsSupported"
OR eventMessage CONTAINS[c] "selectedSiriLanguageIneligible")
Сильные признаки выглядят так:
Enhanced Siri computed state: available=true
Enhanced Siri returning: unavailable
Missing Linwood desired capabilities: LanguageIsSupported
Linwood — внутреннее кодовое имя возможности. Когда язык системы не английский, в журнале ровно эта строка: не хватает LanguageIsSupported. После загрузки с английским — available=true.
И главное наблюдение: если после этого поменять язык обратно, available=true никуда не девается. Решение принято, и до следующей загрузки система к нему не возвращается.
Есть ещё слабый признак — ChatInputViewModel.isServiceAvailable changed to true. Он приходит от интерфейса и говорит только о том, что окно готово рисоваться. Я его учитываю, но как подтверждение, а не как доказательство: полагаться на него нельзя.
Как дождаться готовности
Знать предикат мало — надо понять, когда можно возвращать язык. Слишком рано вернёшь, и есть риск, что система не успела зафиксировать решение; слишком поздно — человек сидит и смотрит на английский интерфейс.
Машина состояний получилась такая:
журнал читается потоком, параллельно каждые 15 секунд дочитывается с начала загрузки — на случай, если событие пришло раньше, чем мы подписались;
события фильтруются по
bootUUID: чужая загрузка нам не интересна;состояние
readyдолжно продержаться 3 секунды — система успевает передумать, и я это видел;общий таймаут — 90 секунд, после чего один повтор с «подъёмом» Siri, чтобы она записала своё состояние;
если не вышло — язык возвращается всё равно.
Последний пункт важнее, чем кажется. В первой версии при неудачной проверке бросалось исключение, и система оставалась английской до следующей перезагрузки — то есть за неудачу платил человек. Сейчас язык возвращается в любом случае, транзакция закрывается фазой completed-without-siri, а уведомление честно говорит, что в этой загрузке не получилось.
На практике всё занимает 20–60 секунд.
Очередь: признака «встал в очередь» нет
Отдельная история для тех, у кого Apple Intelligence не включалась никогда. Apple ставит в очередь только на английской системе, и решается это тоже при загрузке. То есть первый шаг человек обязан сделать руками, а программа может только провести по шагам.
Я искал способ определить, встал человек в очередь или нет. В eligibilityd лежат внутренние коды доменов — числа без имён, завязываться на них нельзя: поменяются в следующем обновлении, и приложение начнёт врать.
Читаемое нашлось в кэшах подписочных функций:
com.apple.CloudSubscriptionFeatures.cache— записьai.enhanced-siriсcanUse = true;com.apple.CloudSubscriptionFeatures.waitlist—waitlistResults, JSON со статусомactive.
Оба — кэши, то есть могут быть пустыми в момент проверки. Поэтому первый успех запоминается своей меткой: иначе пункт «Первая настройка» в меню мигал бы туда-сюда.
Ещё одно, проверенное отдельно: очередь и скачивание моделей смену языка переживают. Сидеть в английской системе сутки не нужно — достаточно встать в очередь и вернуть свой язык.
ChatGPT, который «недоступен»
Побочный сюжет, на котором я потерял вечер. ChatGPT в Siri подключается только на английской системе: на других языках macOS показывает его недоступным и кнопку входа не даёт.
Ладно, значит подключаем во время английского окна. Но дальше начинается интересное: после возврата языка настройки снова показывают «Войти…» и «ChatGPT недоступен» — хотя он уже подключён и работает. Врёт именно интерфейс настроек.
Это пришлось выносить в документацию отдельным предупреждением, потому что первый порыв — нажать «Войти…» ещё раз, и вот тогда действительно можно сломать себе подключение.
В приложении для этого сделан отдельный режим: перезагрузка останавливается на английском, открывает настройки Siri и ждёт, пока человек войдёт и нажмёт «Готово».
Что должно произойти за одну перезагрузку
Сама последовательность:
Запомнить текущий язык и список открытых программ.
Мягко закрыть программы — не убить, а дать сохраниться.
Поставить английский.
Перезагрузить Mac.
После входа дождаться готовности Siri AI.
Вернуть язык.
Открыть программы обратно.
Между пунктами 4 и 5 процесс умирает вместе с системой. Поэтому всё состояние пишется на диск: транзакция с фазами, метка ожидания, идентификатор загрузки. После входа стартует агент, видит метку, сверяет bootUUID — и продолжает с того места, где остановились. Метка от чужой загрузки игнорируется: это защита от ситуации «перезагрузился кнопкой на корпусе посреди процесса».
Какие программы закрывать и как их вернуть
Казалось бы, мелочь. Оказалось — самая грязная часть.
Как понять, что программа «пользовательская». Через NSWorkspace.runningApplications и activationPolicy: 0 — обычная программа с окнами, 1 — фоновый агент в строке меню. Тут ждали грабли JXA, о них ниже.
Как вернуть порядок окон. Через CGWindowListCopyWindowInfo. И вот ловушка: флаг kCGWindowListOptionOnScreenOnly показывает окна только текущего рабочего стола. У меня программы с других столов считались безоконными и открывались скрытыми. Пришлось разделить: порядок окон беру с текущего стола, а сам факт наличия окон — по полному списку.
Как назвать себя в «Элементах входа». macOS подписывает фоновый элемент именем исполняемого файла. Пока агент указывал на /usr/bin/osascript, в настройках у человека висело «osascript, неизвестный разработчик» — выглядит ровно так, как выглядит зловред. Лечится тем, что агент запускает исполняемый файл внутри бандла.
Побочный эффект: агент идёт мимо LaunchServices, поэтому штатная защита от второго экземпляра не срабатывает, и в строке меню появлялось два значка. Пришлось писать свою.
Мелочь, которая съела вечер: Safari без вкладок
После перезагрузки через приложение Safari открывался пустым. При этом в меню «История» пункт «Открыть снова все окна из последнего сеанса» был живой — то есть сеанс никуда не делся, Safari просто его не поднимал.
Причина оказалась в моей же логике. Перед перезагрузкой приложение выставляло TALLogoutSavesState = false — системный флаг «не сохранять состояние при выходе». Логика была разумная: программы я открываю сам, уже после возврата языка, и не хочу, чтобы система открыла их вторым комплектом.
Но Safari восстанавливает свою сессию после перезагрузки только если система вообще собиралась что-то восстанавливать. Собственной настройки «Все окна из последнего сеанса» ему мало — она работает, когда Safari перезапускают внутри одного сеанса. Поэтому в тестах без перезагрузки вкладки возвращались, а после настоящей — нет.
Лечится одной строкой: флаг ставится по галочке пользователя, а не принудительно в false. Двойного запуска не будет — к моменту выхода все программы уже закрыты, и восстанавливать системе нечего.
Почему JXA и чем за это платишь
Всё написано на JXA — JavaScript for Automation, штатном языке автоматизации macOS с доступом к Cocoa через мост. Плюс очевиден: внутри .app лежит обычный читаемый исходник, без Xcode и без зависимостей. Для программы, которая просит доверия и не подписана, это аргумент.
Минусы я собрал все.
Числа из моста приходят строками. Это выражение всегда ложно:
alert.suppressionButton.state === 1
Слева строка. Везде нужен Number(...).
Константы-перечисления через мост не проходят вообще:
$.NSApplicationActivationPolicyRegular // undefined
Сравнивать надо с литералом 0. Именно из-за этого у меня молча пустовал список программ: условие сравнивалось с undefined и не выполнялось никогда.
Выходные параметры требуют null, обычные объектные — $(). Перепутаешь в одну сторону — JavaScriptCore падает с сегфолтом, в другую — «unrecognized selector», потому что null превращается в NSNull.
ObjC.unwrap для массива строк отдаёт объекты ObjC, нужен ObjC.deepUnwrap.
Скомпилированный аплет не рисует image у пункта меню. Совсем. Выяснил бисекцией: тот же код в обычном скрипте рисует, в собранном .app — нет. Пришлось делать свой NSView с NSImageView и NSTextField и подсовывать его в item.view.
И самое обидное. Функция IOPSGetPercentRemaining в моём собранном приложении возвращает 0xE00002C1 — kIOReturnNotPrivileged. Отказ в правах на чтение уровня заряда. В обычном скрипте и даже в отдельном тестовом аплете с теми же модулями она работает; разницы в подписи, правах и бандле я не нашёл.
Неприятен даже не сам отказ, а то, что он был молчаливым: код честно откатывался на pmset раз в двадцать секунд, и «исправление отставания процента», которое я неделей раньше с гордостью закоммитил, не работало ни дня. Обнаружилось только когда значок пропал целиком. Сейчас заряд читается цепочкой IOPSCopyPowerSourcesInfo → IOPSCopyPowerSourcesList → IOPSGetPowerSourceDescription — в приложении это работает.
Фон: с трёх секунд до двух сотых
Пока я занимался Siri, в строке меню жил индикатор заряда, написанный как пишут все: таймер раз в двадцать секунд, а в нём pmset -g batt, pmset -g, ps и top -l 2.
Замер за две минуты:
сам процесс — 0.38 с процессора, около 1.1 пробуждения в секунду;
дочерние процессы — около 3 с, из них
top -l 2даёт 0.48 с за каждый запуск.
То есть программа, которая рисует три цифры, тратила в восемь раз больше не на себя, а на то, что запускала.
Переделал на события:
Заряд и источник питания. IOKit объявляет их через notify: com.apple.system.powersources.percent и .source. Функция notify_register_file_descriptor пишет уведомления в файловый дескриптор, а его слушает NSFileHandle.waitForDataInBackgroundAndNotify. Между событиями программа не просыпается вообще.
Энергосбережение. NSProcessInfo.isLowPowerModeEnabled вместо pmset, а смена — через NSProcessInfoPowerStateDidChangeNotification. Тут грабли: это уведомление приходит из фонового потока, а JXA из фонового потока трогать нельзя. Подписка только блоком в NSOperationQueue.mainQueue.
top и ps запускаются только когда человек открыл меню. Замер идёт полторы секунды, поэтому раздел «больше всего тратят заряд» дописывается в уже открытое меню таймером в kCFRunLoopCommonModes — обычные таймеры, пока меню открыто, стоят.
Итог за те же две минуты: 0.02 секунды процессора, ноль пробуждений, ноль дочерних процессов.
Заодно: где система прячет настоящую батарейку
Раз уж индикатор всё равно рисовался, я захотел вернуть процент заряда наружу — в macOS 27 система рисует его внутри значка, и мне это не нравилось.
Штатно вынести его нельзя: в Пункте управления остался один ключ BatteryShowPercentage, который только включает число, а рисует его система сама и только внутри. Значит, рисовать самому.
Тут выяснилось, что системная батарейка — не символ из SF Symbols. Я взял battery.0percent и получил заметно более узкий и низкий значок. Настоящая фигура лежит в ресурсах Пункта управления и называется там BatteryFillShape.
В Assets.car внутри ControlCenter.app нашлось: battery-cap 2 × 12 — тот самый пупырышек справа, и две молнии — battery-bolt 11 × 14 и battery-bolt-large 13 × 16.
Две молнии — не случайность: система выбирает их по ширине корпуса. Маленькая идёт к узкому корпусу 19 × 10, большая — к обычному 23 × 12. Я сначала взял маленькую, и молния оказалась на пятую часть мельче системной; на глаз это видно, если положить значки рядом.
Ресурсы там чёрные, поэтому для цветного значка их приходится перекрашивать: нарисовать и залить цветом с NSCompositingOperationSourceAtop. А молния при зарядке выбивается из заливки через NSCompositingOperationDestinationOut.
Что получилось
Приложение называется AI Restart. Один пункт в строке меню: закрывает программы, ставит английский, перезагружает, после входа возвращает язык и открывает всё обратно. Секунд тридцать английского — и дальше всё как обычно, только Siri AI работает.
Бесплатно, открытый код под MIT, в сеть не ходит, прав администратора не просит, в фоне не работает.
Оговорки, без которых было бы нечестно:
Приложение не подписано — сертификат разработчика стоит 99 $ в год, а программа бесплатная. macOS скажет, что не может проверить разработчика. Исходник внутри бандла читаемый, сборка — одна команда без Xcode и зависимостей, так что проверить можно самому.
Проверял я только на macOS 27 на Apple Silicon. Про другие версии ничего не обещаю.
Apple может закрыть этот способ любым обновлением. Тогда Siri AI просто перестанет включаться, ничего не сломается.
Код и сборка: https://github.com/markkkkkkkkkkkkkkkkkkkkkkkkk/ai-restart
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.