Plugins
Ядро ходу каналу
Ядро ходу каналу — це спільна вхідна машина станів, яка перетворює нормалізовану подію платформи на хід агента. Plugin-и каналів надають факти платформи та зворотний виклик доставки. Ядро відповідає за оркестрацію: приймання, класифікацію, попередню перевірку, розв’язання, авторизацію, складання, запис, диспетчеризацію та завершення.
Використовуйте це, коли ваш Plugin перебуває на гарячому шляху вхідних повідомлень. Для подій, що не є повідомленнями (slash-команди, модальні вікна, взаємодії з кнопками, події життєвого циклу, реакції, голосовий стан), залишайте їх локальними для Plugin-а. Ядро відповідає лише за події, які можуть стати текстовим ходом агента.
Навіщо потрібне спільне ядро
Plugin-и каналів повторюють той самий вхідний потік: нормалізувати, маршрутизувати, обмежити, побудувати контекст, записати метадані сесії, диспетчеризувати хід агента, завершити стан доставки. Без спільного ядра зміну до обмеження за згадкою, видимих відповідей лише для інструментів, метаданих сесії, відкладеної історії або завершення диспетчеризації потрібно застосовувати окремо для кожного каналу.
Ядро навмисно тримає чотири поняття окремими:
ConversationFacts: звідки надійшло повідомленняRouteFacts: який агент і яка сесія мають його обробитиReplyPlanFacts: куди мають надходити видимі відповідіMessageFacts: яке тіло та додатковий контекст має бачити агент
Slack DM, теми Telegram, потоки Matrix і тематичні сесії Feishu на практиці розрізняють усе це. Якщо трактувати їх як один ідентифікатор, з часом виникає розходження.
Життєвий цикл етапів
Ядро виконує той самий фіксований конвеєр незалежно від каналу:
ingest-- адаптер перетворює необроблену подію платформи наNormalizedTurnInputclassify-- адаптер оголошує, чи може ця подія почати хід агентаpreflight-- адаптер виконує дедуплікацію, відкидання власного відлуння, гідратацію, debounce, дешифрування, часткове попереднє заповнення фактівresolve-- адаптер повертає повністю складений хід (маршрут, план відповіді, повідомлення, доставка)authorize-- до складених фактів застосовується політика DM, груп, згадок і командassemble--FinalizedMsgContextбудується з фактів черезbuildContextrecord-- зберігаються вхідні метадані сесії та останній маршрутdispatch-- хід агента виконується через буферизований диспетчер блоківfinalize--onFinalizeадаптера виконується навіть у разі помилки диспетчеризації
Кожен етап створює структуровану подію журналу, коли надано зворотний виклик log. Див. Спостережуваність.
Види допуску
Ядро не викидає помилку, коли хід заблоковано. Воно повертає ChannelTurnAdmission:
| Вид | Коли |
|---|---|
dispatch |
Хід допущено. Хід агента виконується, і шлях видимої відповіді задіюється. |
observeOnly |
Хід виконується наскрізно, але адаптер доставки не надсилає нічого видимого. Використовується для агентів-спостерігачів трансляцій та інших пасивних багатоагентних потоків. |
handled |
Подію платформи спожито локально (життєвий цикл, реакція, кнопка, модальне вікно). Ядро пропускає диспетчеризацію. |
drop |
Шлях пропуску. Необов’язковий recordHistory: true зберігає повідомлення у відкладеній історії групи, щоб майбутня згадка мала контекст. |
Допуск може надійти з classify (клас події вказав, що вона не може почати хід), з preflight (дедуплікація, власне відлуння, відсутня згадка із записом історії) або з самого resolveTurn.
Точки входу
Runtime надає три бажані точки входу, щоб адаптери могли підключатися на рівні, який відповідає каналу.
runtime.channel.turn.run(...) // повний конвеєр, керований адаптером
runtime.channel.turn.runPrepared(...) // канал володіє диспетчеризацією; ядро виконує запис + завершення
runtime.channel.turn.buildContext(...) // чисте відображення фактів у FinalizedMsgContext
Два старіші помічники runtime залишаються доступними для сумісності з Plugin SDK:
runtime.channel.turn.runResolved(...) // застарілий псевдонім сумісності; віддавайте перевагу run
runtime.channel.turn.dispatchAssembled(...) // застарілий псевдонім сумісності; віддавайте перевагу run або runPrepared
run
Використовуйте, коли ваш канал може виразити свій вхідний потік як ChannelTurnAdapter<TRaw>. Адаптер має зворотні виклики для ingest, необов’язкового classify, необов’язкового preflight, обов’язкового resolveTurn і необов’язкового onFinalize.
await runtime.channel.turn.run({
channel: "tlon",
accountId,
raw: platformEvent,
adapter: {
ingest(raw) {
return {
id: raw.messageId,
timestamp: raw.timestamp,
rawText: raw.body,
textForAgent: raw.body,
};
},
classify(input) {
return { kind: "message", canStartAgentTurn: input.rawText.length > 0 };
},
async preflight(input, eventClass) {
if (await isDuplicate(input.id)) {
return { admission: { kind: "drop", reason: "dedupe" } };
}
return {};
},
resolveTurn(input) {
return buildAssembledTurn(input);
},
onFinalize(result) {
clearPendingGroupHistory(result);
},
},
});
run має правильну форму, коли канал має невелику логіку адаптера й отримує користь від володіння життєвим циклом через хуки.
runPrepared
Використовуйте, коли канал має складний локальний диспетчер із попередніми переглядами, повторами, редагуваннями або bootstrap-ом потоку, який має залишатися у власності каналу. Ядро все одно записує вхідну сесію перед диспетчеризацією та надає уніфікований DispatchedChannelTurnResult.
const { dispatchResult } = await runtime.channel.turn.runPrepared({
channel: "matrix",
accountId,
routeSessionKey,
storePath,
ctxPayload,
recordInboundSession,
record: {
onRecordError,
updateLastRoute,
},
onPreDispatchFailure: async (err) => {
await stopStatusReactions();
},
runDispatch: async () => {
return await runMatrixOwnedDispatcher();
},
});
Насичені канали (Matrix, Mattermost, Microsoft Teams, Feishu, QQ Bot) використовують runPrepared, бо їхній диспетчер оркеструє специфічну для платформи поведінку, про яку ядро не має знати.
buildContext
Чиста функція, яка відображає пакети фактів у FinalizedMsgContext. Використовуйте її, коли ваш канал вручну реалізує частину конвеєра, але хоче узгоджену форму контексту.
const ctxPayload = runtime.channel.turn.buildContext({
channel: "googlechat",
accountId,
messageId,
timestamp,
from,
sender,
conversation,
route,
reply,
message,
access,
media,
supplemental,
});
buildContext також корисна всередині зворотних викликів resolveTurn, коли складається хід для run.
Типи фактів
Факти, які ядро споживає з вашого адаптера, не залежать від платформи. Перетворіть об’єкти платформи на ці форми, перш ніж передавати їх ядру.
NormalizedTurnInput
| Поле | Призначення |
|---|---|
id |
Стабільний id повідомлення, що використовується для дедуплікації та журналів |
timestamp |
Необов’язковий epoch у мс |
rawText |
Тіло в тому вигляді, у якому його отримано з платформи |
textForAgent |
Необов’язкове очищене тіло для агента (видалення згадки, обрізання введення) |
textForCommands |
Необов’язкове тіло, що використовується для розбору /command |
raw |
Необов’язкове наскрізне посилання для зворотних викликів адаптера, яким потрібен оригінал |
ChannelEventClass
| Поле | Призначення |
|---|---|
kind |
message, command, interaction, reaction, lifecycle, unknown |
canStartAgentTurn |
Якщо false, ядро повертає { kind: "handled" } |
requiresImmediateAck |
Підказка для адаптерів, яким потрібно ACK до диспетчеризації |
SenderFacts
| Поле | Призначення |
|---|---|
id |
Стабільний id відправника на платформі |
name |
Відображуване ім’я |
username |
Handle, якщо відрізняється від name |
tag |
Дискримінатор у стилі Discord або тег платформи |
roles |
id ролей, що використовуються для зіставлення allowlist ролей учасників |
isBot |
True, коли відправник є відомим ботом (ядро використовує для відкидання) |
isSelf |
True, коли відправник є самим налаштованим агентом |
displayLabel |
Попередньо відрендерена мітка для тексту конверта |
ConversationFacts
| Поле | Призначення |
|---|---|
kind |
direct, group або channel |
id |
id розмови, що використовується для маршрутизації |
label |
Людська мітка для конверта |
spaceId |
Необов’язковий ідентифікатор зовнішнього простору (workspace Slack, homeserver Matrix) |
parentId |
id зовнішньої розмови, коли це потік |
threadId |
id потоку, коли це повідомлення всередині потоку |
nativeChannelId |
Нативний для платформи id каналу, коли він відрізняється від id маршрутизації |
routePeer |
Peer, що використовується для пошуку resolveAgentRoute |
RouteFacts
| Поле | Призначення |
|---|---|
agentId |
Агент, який має обробити цей хід |
accountId |
Необов’язкове перевизначення (багатоакаунтні канали) |
routeSessionKey |
Ключ сесії, що використовується для маршрутизації |
dispatchSessionKey |
Ключ сесії, що використовується під час диспетчеризації, коли відрізняється від ключа маршруту |
persistedSessionKey |
Ключ сесії, записаний до збережених метаданих сесії |
parentSessionKey |
Батьківський ключ для розгалужених/потокових сесій |
modelParentSessionKey |
Батьківський ключ на боці моделі для розгалужених сесій |
mainSessionKey |
Закріплення власника основного DM для прямих розмов |
createIfMissing |
Дозволити кроку запису створити відсутній рядок сесії |
ReplyPlanFacts
| Поле | Призначення |
|---|---|
to |
Логічна ціль відповіді, записана в контекст To |
originatingTo |
Початкова ціль контексту (OriginatingTo) |
nativeChannelId |
Нативний для платформи ідентифікатор каналу для доставки |
replyTarget |
Кінцева видима ціль відповіді, якщо вона відрізняється від to |
deliveryTarget |
Низькорівневе перевизначення доставки |
replyToId |
Ідентифікатор процитованого/прив’язаного повідомлення |
replyToIdFull |
Повна форма процитованого ідентифікатора, коли платформа має обидві |
messageThreadId |
Ідентифікатор треду на момент доставки |
threadParentId |
Ідентифікатор батьківського повідомлення треду |
sourceReplyDeliveryMode |
thread, reply, channel, direct або none |
AccessFacts
AccessFacts містить булеві значення, потрібні етапу авторизації. Зіставлення ідентичності залишається в каналі: ядро лише споживає результат.
| Поле | Призначення |
|---|---|
dm |
Рішення дозволу/спарювання/заборони DM та список allowFrom |
group |
Політика групи, дозвіл маршруту, дозвіл відправника, allowlist, вимога згадки |
commands |
Авторизація команд у налаштованих авторизаторах |
mentions |
Чи можливе виявлення згадки та чи було згадано агента |
MessageFacts
| Поле | Призначення |
|---|---|
body |
Кінцеве тіло конверта (відформатоване) |
rawBody |
Сире вхідне тіло |
bodyForAgent |
Тіло, яке бачить агент |
commandBody |
Тіло, що використовується для розбору команд |
envelopeFrom |
Попередньо відрендерена мітка відправника для конверта |
senderLabel |
Необов’язкове перевизначення для відрендереного відправника |
preview |
Короткий редагований попередній перегляд для журналів |
inboundHistory |
Останні записи вхідної історії, коли канал тримає буфер |
SupplementalContextFacts
Додатковий контекст охоплює контекст цитати, пересланого повідомлення та початкового завантаження треду. Ядро застосовує налаштовану політику contextVisibility. Адаптер каналу лише надає факти та прапорці senderAllowed, щоб міжканальна політика залишалася узгодженою.
InboundMediaFacts
Медіа має форму фактів. Завантаження платформи, автентифікація, політика SSRF, правила CDN і дешифрування залишаються локальними для каналу. Ядро відображає факти в MediaPath, MediaUrl, MediaType, MediaPaths, MediaUrls, MediaTypes і MediaTranscribedIndexes.
Контракт адаптера
Для повного run форма адаптера така:
type ChannelTurnAdapter<TRaw> = {
ingest(raw: TRaw): Promise<NormalizedTurnInput | null> | NormalizedTurnInput | null;
classify?(input: NormalizedTurnInput): Promise<ChannelEventClass> | ChannelEventClass;
preflight?(
input: NormalizedTurnInput,
eventClass: ChannelEventClass,
): Promise<PreflightFacts | ChannelTurnAdmission | null | undefined>;
resolveTurn(
input: NormalizedTurnInput,
eventClass: ChannelEventClass,
preflight: PreflightFacts,
): Promise<ChannelTurnResolved> | ChannelTurnResolved;
onFinalize?(result: ChannelTurnResult): Promise<void> | void;
};
resolveTurn повертає ChannelTurnResolved, тобто AssembledChannelTurn з необов’язковим видом допуску. Повернення { admission: { kind: "observeOnly" } } запускає хід без створення видимого виводу. Адаптер усе ще володіє callback доставки; він просто стає no-op для цього ходу.
onFinalize виконується для кожного результату, включно з помилками диспетчеризації. Використовуйте його, щоб очистити очікувану групову історію, прибрати ack-реакції, зупинити індикатори стану та скинути локальний стан.
Адаптер доставки
Ядро не викликає платформу напряму. Канал передає ядру ChannelTurnDeliveryAdapter:
type ChannelTurnDeliveryAdapter = {
deliver(payload: ReplyPayload, info: ChannelDeliveryInfo): Promise<ChannelDeliveryResult | void>;
onError?(err: unknown, info: { kind: string }): void;
durable?: false | DurableInboundReplyDeliveryOptions;
};
type ChannelDeliveryResult = {
messageIds?: string[];
receipt?: MessageReceipt;
threadId?: string;
replyToId?: string;
visibleReplySent?: boolean;
};
deliver викликається один раз для кожного буферизованого фрагмента відповіді. Під час міграції життєвого циклу повідомлень доставка зібраного ходу каналу за замовчуванням належить каналу: відсутнє поле durable означає, що ядро має викликати deliver напряму й не має маршрутизувати через загальну вихідну доставку. Встановлюйте durable лише після аудиту каналу, який доведе, що загальний шлях надсилання зберігає стару поведінку доставки, включно з цілями відповіді/треду, обробкою медіа, кешами надісланих повідомлень/самоехо, очищенням стану та поверненими ідентифікаторами повідомлень. durable: false залишається сумісним написанням для "використовувати callback, що належить каналу", але немігрованим каналам не потрібно його додавати. Повертайте ідентифікатори повідомлень платформи, коли канал їх має, щоб диспетчер міг зберігати прив’язки тредів і редагувати пізніші фрагменти; новіші шляхи доставки також мають повертати receipt, щоб відновлення, фіналізація попереднього перегляду та придушення дублікатів могли відійти від messageIds. Для ходів лише зі спостереженням повертайте { visibleReplySent: false } або використовуйте createNoopChannelTurnDeliveryAdapter().
Канали, що використовують runPrepared із диспетчером, який повністю належить каналу, не мають ChannelTurnDeliveryAdapter. Такі диспетчери за замовчуванням не є durable. Вони мають зберігати свій прямий шлях доставки, доки явно не приєднаються до нового контексту надсилання з повною ціллю, безпечним для повторного відтворення адаптером, контрактом receipt і хуками побічних ефектів каналу.
Публічні помічники сумісності, як-от recordInboundSessionAndDispatchReply, dispatchInboundReplyWithBase і помічники direct-DM, мають зберігати поведінку під час міграції. Вони не повинні викликати загальну durable-доставку перед callback deliver або reply, що належать викликачу.
Параметри запису
Етап запису обгортає recordInboundSession. Більшість каналів можуть використовувати значення за замовчуванням. Перевизначайте через record:
record: {
groupResolution,
createIfMissing: true,
updateLastRoute,
onRecordError: (err) => log.warn("record failed", err),
trackSessionMetaTask: (task) => pendingTasks.push(task),
}
Диспетчер очікує завершення етапу запису. Якщо запис кидає помилку, ядро запускає onPreDispatchFailure (коли його передано в runPrepared) і повторно кидає помилку.
Спостережуваність
Кожен етап випромінює структуровану подію, коли надано callback log:
await runtime.channel.turn.run({
channel: "twitch",
accountId,
raw,
adapter,
log: (event) => {
runtime.log?.debug?.(`turn.${event.stage}:${event.event}`, {
channel: event.channel,
accountId: event.accountId,
messageId: event.messageId,
sessionKey: event.sessionKey,
admission: event.admission,
reason: event.reason,
});
},
});
Етапи, що журналюються: ingest, classify, preflight, resolve, authorize, assemble, record, dispatch, finalize. Уникайте журналювання сирих тіл; використовуйте MessageFacts.preview для коротких редагованих попередніх переглядів.
Що залишається локальним для каналу
Ядро володіє оркестрацією. Канал усе ще володіє:
- Транспорти платформи (Gateway, REST, websocket, polling, webhooks)
- Розв’язання ідентичності та зіставлення відображуваних імен
- Нативні команди, slash-команди, автодоповнення, модальні вікна, кнопки, стан голосу
- Рендеринг карток, модальних вікон і adaptive-card
- Автентифікація медіа, правила CDN, зашифровані медіа, транскрипція
- API редагування, реакцій, редагування вмісту та присутності
- Backfill і отримання історії на боці платформи
- Потоки спарювання, що потребують перевірки, специфічної для платформи
Якщо двом каналам почне бути потрібен той самий помічник для одного з цих пунктів, винесіть спільний помічник SDK замість того, щоб проштовхувати його в ядро.
Стабільність
runtime.channel.turn.* є частиною публічної поверхні runtime Plugin. Типи фактів (SenderFacts, ConversationFacts, RouteFacts, ReplyPlanFacts, AccessFacts, MessageFacts, SupplementalContextFacts, InboundMediaFacts) і форми допуску (ChannelTurnAdmission, ChannelEventClass) доступні через PluginRuntime з openclaw/plugin-sdk/core.
Застосовуються правила зворотної сумісності: нові поля фактів є додатковими, види допуску не перейменовуються, а назви точок входу залишаються стабільними. Нові потреби каналу, що вимагають неадитивної зміни, мають проходити через процес міграції plugin SDK.
Пов’язане
- Рефакторинг життєвого циклу повідомлень для запланованого життєвого циклу надсилання/отримання/live, який обгорне це ядро
- Створення channel plugins для ширшого контракту channel plugin
- Помічники runtime Plugin для інших поверхонь
runtime.* - Внутрішня архітектура Plugin для механіки конвеєра завантаження та реєстру