Skip to main content
Эта страница изначально представляла собой перспективное проектное предложение. Основная часть этой архитектуры впоследствии была реализована в src/channels/message/* и общедоступных подпутях openclaw/plugin-sdk/channel-outbound / channel-inbound. Для работы с текущим API используйте API исходящих сообщений канала и API входящих сообщений канала. На этой странице описано, что было реализовано, где реализация разошлась с первоначальным эскизом и какие вопросы остаются открытыми.

Почему был выполнен этот рефакторинг

Стек каналов вырос из нескольких локальных исправлений: отдельных вспомогательных средств для входящих сообщений каждого уровня зрелости (runtime.channel.inbound.run для простых адаптеров, runtime.channel.inbound.runPreparedReply для расширенных), устаревших вспомогательных средств диспетчеризации ответов (dispatchInboundReplyWithBase, recordInboundSessionAndDispatchReply), потоковой передачи предварительного просмотра для отдельных каналов и обеспечения надёжности окончательной доставки, добавленного поверх существующих путей полезной нагрузки ответов. Такая структура создала слишком много общедоступных концепций и слишком много мест, где семантика доставки могла расходиться. Пробел в надёжности, из-за которого потребовалось перепроектирование:
Целевой инвариант: как только ядро решает, что видимое исходящее сообщение должно существовать, намерение отправки должно быть сохранено до попытки вызова платформы, а квитанция платформы должна быть зафиксирована после успешного выполнения. По умолчанию это обеспечивает восстановление с семантикой «не менее одного раза». Семантика «ровно один раз» существует только там, где адаптер подтверждает собственную идемпотентность или сопоставляет попытку с неизвестным результатом после отправки с состоянием платформы перед повторным воспроизведением.

Что было реализовано

Внутренняя предметная область находится в src/channels/message/*: Общедоступный интерфейс: openclaw/plugin-sdk/channel-outbound (вспомогательные средства отправки/квитанций/надёжности/предварительного просмотра/конвейера ответов) и openclaw/plugin-sdk/channel-inbound (контекст входящих сообщений, runChannelInboundEvent, dispatchChannelInboundReply). Примеры адаптеров, текущие имена типов и примечания по миграции приведены на этих страницах — именно они являются достоверным источником сведений о структуре API, а не приведённые ниже эскизы.

Контекст отправки

withDurableMessageSendContext предоставляет коду канала шаги render, previewUpdate, send, edit, delete, commit и fail для одного исходящего сообщения. sendDurableMessageBatch — обёртка для типового случая: отобразить, отправить, затем зафиксировать при sent/suppressed или завершить с ошибкой. sendDurableMessageBatch возвращает один дискриминируемый результат: Режим надёжности — один из required, best_effort или disabled (MessageDurabilityPolicy в src/channels/message/types.ts). required безопасно завершает работу с ошибкой, если сохраняемое намерение невозможно записать; best_effort переходит к непосредственной отправке, если сохранение недоступно; disabled сохраняет поведение непосредственной отправки до рефакторинга. Вспомогательные средства обратной совместимости по умолчанию используют disabled и не выводят required только из наличия у канала универсального адаптера исходящих сообщений. Опасная граница сохраняется после успешного вызова платформы и до фиксации квитанции. Если процесс завершится в этот момент, ядро не сможет определить, существует ли сообщение платформы, если адаптер не объявляет reconcileUnknownSend. Этот хук классифицирует прерванную отправку как sent, not_sent или unresolved; только not_sent разрешает повторное воспроизведение. Каналы без механизма сопоставления переходят к состоянию unknown_after_send (src/channels/message/state.ts, src/infra/outbound/delivery-queue-recovery.ts) и могут выбрать повторное воспроизведение с семантикой «не менее одного раза», только если дублирование видимых сообщений является приемлемым и документированным компромиссом для этого канала.

Контекст получения

createMessageReceiveContext отслеживает состояние подтверждения/отклонения для каждого входящего события с идемпотентным ack() и явным nack(error). Политика подтверждения (ChannelMessageReceiveAckPolicy) принимает одно из следующих значений: Опрос Telegram использует это для сохранения безопасной отметки завершённых обновлений (safeCompletedUpdateId в extensions/telegram/src/bot-update-tracker.ts): grammY по-прежнему наблюдает каждое обновление при его поступлении в цепочку промежуточного ПО, но OpenClaw продвигает сохраняемую отметку перезапуска только за обновления, которые завершили диспетчеризацию, поэтому неудачные или всё ещё ожидающие обновления воспроизводятся после перезапуска. Смещение вышестоящего getUpdates Telegram по-прежнему контролирует grammY; полностью надёжный источник опроса, управляющий повторной доставкой на уровне платформы за пределами этой отметки, не реализован (см. «Открытые вопросы»).

Предварительный просмотр в реальном времени

src/channels/message/live.ts моделирует предварительный просмотр, редактирование и завершение как единый жизненный цикл: createLiveMessageState, markLiveMessagePreviewUpdated, markLiveMessageFinalized, markLiveMessageCancelled и deliverFinalizableLivePreviewAdapter (сформировать окончательную правку из черновика, применить её и перейти к обычной отправке, если редактирование невозможно или завершается с ошибкой). LiveMessageState.phase — это idle | previewing | finalizing | finalized | cancelled; canFinalizeInPlace определяет, может ли предварительный просмотр стать окончательным сообщением посредством редактирования вместо новой отправки.

Сохраняемые квитанции

MessageReceipt (src/channels/message/types.ts) нормализует один или несколько идентификаторов сообщений платформы из одной логической отправки в platformMessageIds, а также parts для каждой части (тип, индекс, идентификатор ветки, идентификатор сообщения, на которое дан ответ). Основной идентификатор сохраняется для ветвления и последующих изменений. Благодаря этому составные доставки (текст с медиафайлами, текст, разбитый на части, резервное отображение карточки) можно повторно воспроизводить и дедуплицировать после перезапуска.

Сокращение общедоступного SDK

Рефакторинг поглотил или объявил устаревшими: reply-runtime, reply-dispatch-runtime, reply-reference, reply-chunking, вспомогательные средства reply-payload, представленные как общедоступный API, inbound-reply-dispatch, channel-reply-pipeline и большинство общедоступных применений outbound-runtime. Теперь src/plugin-sdk/channel-message.ts — это модуль повторного экспорта @deprecated, указывающий на channel-outbound / channel-inbound; псевдонимы среды выполнения channel.turn были удалены, а старая страница документации /plugins/sdk-channel-turn перенаправляет на API входящих сообщений канала. Код новых плагинов должен напрямую использовать channel-outbound и channel-inbound.

Где реализация разошлась с первоначальным проектом

Приведённый ниже проектный эскиз никогда не был реализован буквально в описанном виде. Он сохранён для исторической точности; не считайте эти имена типов текущим API.
  • Нет MessageOrigin / shouldDropOpenClawEcho. Первоначальный план предусматривал тег происхождения source: "openclaw" для сообщений об ошибках Gateway, а также общий предикат, удаляющий помеченные повторные сообщения от бота в общих комнатах до авторизации allowBots. Этот тип и предикат отсутствуют в кодовой базе. Сам allowBots является реальным ключом конфигурации для каждого канала (Slack, Discord, Google Chat и других), но механизм тегов происхождения, который должен был его защищать, так и не был реализован. Подавление повторных сообщений об ошибках Gateway в комнатах с включёнными ботами остаётся открытым пробелом, а не реализованной гарантией.
  • Нет единого пространства имён core.messages.receive/send/live/state. Реализованные функции находятся непосредственно в src/channels/message/* (withDurableMessageSendContext, createMessageReceiveContext, createLiveMessageState, classifyDurableSendRecoveryState), а не за фасадом core.messages.*.
  • Нет универсального нормализованного типа сообщений ChannelMessage / MessageTarget / MessageRelation. Ядро по-прежнему передаёт конкретные полезные нагрузки ответов (ReplyPayload) и контексты отдельных каналов через адаптеры отправки, а не единую платформонезависимую структуру сообщения с отношением kind: "reply" | "followup" | "broadcast" | "system".
  • Имена политик подтверждения отличаются от эскиза. Реализовано: after_receive_record | after_agent_dispatch | after_durable_send | manual. В первоначальном эскизе использовался immediate | after-record | after-durable-send | manual с полем причины тайм-аута Webhook; такая структура не была реализована.
  • Ключи возможностей DurableFinalDeliveryRequirementMap заменили предложенный в эскизе объект MessageCapabilities. Возможности представлены плоскими логическими флагами (text, media, poll, payload, silent, replyTo, thread, nativeQuote, messageSendingHooks, batch, reconcileUnknownSend, afterSendSuccess, afterCommit), проверяемыми через verifyDurableFinalCapabilityProofs, а не вложенной структурой в стиле text.chunking / attachments.voice.

Конкретные риски миграции (по-прежнему актуальны)

Эти побочные эффекты, относящиеся к отдельным каналам, появились до рефакторинга и должны продолжать работать через новые пути отправки. Они не гипотетические: каждый из них реализован и критически важен сегодня.
  • iMessage (extensions/imessage/src/monitor/echo-cache.ts, persisted-echo-cache.ts): после успешной отправки средство мониторинга записывает отправленные сообщения в кеш эха. При завершающих отправках с гарантированной доставкой этот кеш по-прежнему должен заполняться, иначе OpenClaw может повторно принять собственные ответы как входящие сообщения пользователя.
  • Tlon (extensions/tlon/src/monitor/index.ts): добавляет необязательную подпись модели и после ответов в группах регистрирует ветки, в которых было участие. Доставка с гарантированной сохранностью не должна обходить эти действия.
  • Discord и другие подготовленные диспетчеры уже самостоятельно обеспечивают прямую доставку и предварительный просмотр. Канал не обеспечивает сквозную гарантированную доставку, пока его подготовленный диспетчер явно не направляет завершающие сообщения через контекст отправки; не следует считать, что одного универсального адаптера для этого достаточно.
  • Резервная доставка без уведомления в Telegram после разбиения на части или резервного проецирования должна доставлять весь массив спроецированной полезной нагрузки, а не только первый элемент.
  • LINE, Zalo, Nostr и аналогичные вспомогательные пути могут поддерживать обработку токенов ответа, проксирование медиафайлов, кеши отправленных сообщений или цели, доступные только через обратные вызовы. Доставка для них остаётся в ведении канала, пока эта семантика не будет представлена адаптером отправки и покрыта тестами.
  • Вспомогательные средства для прямых личных сообщений могут иметь обратный вызов ответа, который является единственной корректной транспортной целью. Универсальный механизм исходящей отправки не должен определять цель по необработанным полям платформы и пропускать этот обратный вызов.

Классификация сбоев

Адаптеры классифицируют транспортные сбои по закрытым категориям в стиле DeliveryFailureKind (временный сбой, ограничение частоты запросов, аутентификация, разрешение, объект не найден, недопустимая полезная нагрузка, конфликт, отмена, неизвестная ошибка). Политика ядра:
  • Повторять попытки при временных сбоях и ограничении частоты запросов.
  • Не повторять попытки при недопустимой полезной нагрузке, если не предусмотрен резервный способ её формирования.
  • Не повторять попытки при сбоях аутентификации или разрешений до изменения конфигурации.
  • Если объект не найден, разрешить механизму завершения активной отправки перейти от редактирования к новой отправке, когда канал объявляет такой переход безопасным.
  • При конфликте использовать состояние квитанции и идемпотентности, чтобы определить, существует ли сообщение.
  • Любая ошибка после вызова платформы, который мог завершиться успешно, но до фиксации квитанции становится unknown_after_send, если адаптер не докажет, что операция на платформе не была выполнена.

Открытые вопросы

  • Следует ли Telegram со временем заменить средство запуска опроса grammY (1.43.0) на полностью устойчивый источник опроса, который управляет повторной доставкой на уровне платформы, а не только сохраняемой OpenClaw отметкой перезапуска (safeCompletedUpdateId).
  • Должно ли состояние предварительного просмотра в реальном времени храниться в одной записи с намерением завершающей отправки или в отдельном связанном хранилище текущего состояния.
  • Требуется ли для подавления эха при сбое Gateway в общих комнатах с ботами изначально запланированный механизм тегирования источника, более простой контракт для каждого канала или эта задача выходит за рамки текущей области.
  • Какие каналы изначально поддерживают источник и метаданные для подавления межботового эха, а каким требуется сохраняемый реестр исходящих сообщений.

Связанные материалы