mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:50:43 +00:00
refactor(channels): route inbound turns through kernel
This commit is contained in:
@@ -111,6 +111,39 @@ describe("broadcast dispatch", () => {
|
||||
saveMediaBuffer: mockSaveMediaBuffer,
|
||||
},
|
||||
turn: {
|
||||
run: vi.fn(async (params: Parameters<PluginRuntime["channel"]["turn"]["run"]>[0]) => {
|
||||
const input = await params.adapter.ingest(params.raw);
|
||||
if (!input) {
|
||||
return {
|
||||
admission: { kind: "drop" as const, reason: "ingest-null" },
|
||||
dispatched: false,
|
||||
};
|
||||
}
|
||||
const eventClass = {
|
||||
kind: "message" as const,
|
||||
canStartAgentTurn: true,
|
||||
};
|
||||
const turn = await params.adapter.resolveTurn(input, eventClass, {});
|
||||
if (!("runDispatch" in turn)) {
|
||||
throw new Error("feishu broadcast test runtime only supports prepared turns");
|
||||
}
|
||||
await turn.recordInboundSession({
|
||||
storePath: turn.storePath,
|
||||
sessionKey: turn.ctxPayload.SessionKey ?? turn.routeSessionKey,
|
||||
ctx: turn.ctxPayload,
|
||||
groupResolution: turn.record?.groupResolution,
|
||||
createIfMissing: turn.record?.createIfMissing,
|
||||
updateLastRoute: turn.record?.updateLastRoute,
|
||||
onRecordError: turn.record?.onRecordError ?? (() => undefined),
|
||||
});
|
||||
return {
|
||||
admission: { kind: "dispatch" as const },
|
||||
dispatched: true,
|
||||
ctxPayload: turn.ctxPayload,
|
||||
routeSessionKey: turn.routeSessionKey,
|
||||
dispatchResult: await turn.runDispatch(),
|
||||
};
|
||||
}),
|
||||
runPrepared: vi.fn(
|
||||
async (turn: Parameters<PluginRuntime["channel"]["turn"]["runPrepared"]>[0]) => {
|
||||
await turn.recordInboundSession({
|
||||
|
||||
@@ -198,6 +198,16 @@ function createFeishuBotRuntime(overrides: DeepPartial<PluginRuntime> = {}): Plu
|
||||
buildPairingReply: vi.fn(),
|
||||
},
|
||||
turn: {
|
||||
run: vi.fn(async (params) => {
|
||||
const input = await params.adapter.ingest(params.raw);
|
||||
const turn = await params.adapter.resolveTurn(input, {
|
||||
kind: "message",
|
||||
canStartAgentTurn: true,
|
||||
});
|
||||
return {
|
||||
dispatchResult: await turn.runDispatch(),
|
||||
};
|
||||
}),
|
||||
runPrepared: vi.fn(async (params) => ({
|
||||
dispatchResult: await params.runDispatch(),
|
||||
})),
|
||||
|
||||
@@ -1312,31 +1312,46 @@ export async function handleFeishuMessage(params: {
|
||||
log(
|
||||
`feishu[${account.accountId}]: broadcast active dispatch agent=${agentId} (session=${agentSessionKey})`,
|
||||
);
|
||||
await core.channel.turn.runPrepared({
|
||||
await core.channel.turn.run({
|
||||
channel: "feishu",
|
||||
accountId: route.accountId,
|
||||
routeSessionKey: agentSessionKey,
|
||||
storePath: agentStorePath,
|
||||
ctxPayload: agentCtx,
|
||||
recordInboundSession: core.channel.session.recordInboundSession,
|
||||
record: agentRecord,
|
||||
onPreDispatchFailure: () =>
|
||||
core.channel.reply.settleReplyDispatcher({
|
||||
dispatcher,
|
||||
onSettled: () => markDispatchIdle(),
|
||||
raw: ctx,
|
||||
adapter: {
|
||||
ingest: () => ({
|
||||
id: ctx.messageId,
|
||||
timestamp: messageCreateTimeMs,
|
||||
rawText: ctx.content,
|
||||
textForAgent: agentCtx.BodyForAgent,
|
||||
textForCommands: agentCtx.CommandBody,
|
||||
raw: ctx,
|
||||
}),
|
||||
runDispatch: () =>
|
||||
core.channel.reply.withReplyDispatcher({
|
||||
dispatcher,
|
||||
onSettled: () => markDispatchIdle(),
|
||||
run: () =>
|
||||
core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: agentCtx,
|
||||
cfg,
|
||||
resolveTurn: () => ({
|
||||
channel: "feishu",
|
||||
accountId: route.accountId,
|
||||
routeSessionKey: agentSessionKey,
|
||||
storePath: agentStorePath,
|
||||
ctxPayload: agentCtx,
|
||||
recordInboundSession: core.channel.session.recordInboundSession,
|
||||
record: agentRecord,
|
||||
onPreDispatchFailure: () =>
|
||||
core.channel.reply.settleReplyDispatcher({
|
||||
dispatcher,
|
||||
replyOptions,
|
||||
onSettled: () => markDispatchIdle(),
|
||||
}),
|
||||
runDispatch: () =>
|
||||
core.channel.reply.withReplyDispatcher({
|
||||
dispatcher,
|
||||
onSettled: () => markDispatchIdle(),
|
||||
run: () =>
|
||||
core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: agentCtx,
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyOptions,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Observer agent: no-op dispatcher (session entry + inference, no Feishu reply).
|
||||
@@ -1356,24 +1371,39 @@ export async function handleFeishuMessage(params: {
|
||||
log(
|
||||
`feishu[${account.accountId}]: broadcast observer dispatch agent=${agentId} (session=${agentSessionKey})`,
|
||||
);
|
||||
await core.channel.turn.runPrepared({
|
||||
await core.channel.turn.run({
|
||||
channel: "feishu",
|
||||
accountId: route.accountId,
|
||||
routeSessionKey: agentSessionKey,
|
||||
storePath: agentStorePath,
|
||||
ctxPayload: agentCtx,
|
||||
recordInboundSession: core.channel.session.recordInboundSession,
|
||||
record: agentRecord,
|
||||
runDispatch: () =>
|
||||
core.channel.reply.withReplyDispatcher({
|
||||
dispatcher: noopDispatcher,
|
||||
run: () =>
|
||||
core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: agentCtx,
|
||||
cfg,
|
||||
raw: ctx,
|
||||
adapter: {
|
||||
ingest: () => ({
|
||||
id: ctx.messageId,
|
||||
timestamp: messageCreateTimeMs,
|
||||
rawText: ctx.content,
|
||||
textForAgent: agentCtx.BodyForAgent,
|
||||
textForCommands: agentCtx.CommandBody,
|
||||
raw: ctx,
|
||||
}),
|
||||
resolveTurn: () => ({
|
||||
channel: "feishu",
|
||||
accountId: route.accountId,
|
||||
routeSessionKey: agentSessionKey,
|
||||
storePath: agentStorePath,
|
||||
ctxPayload: agentCtx,
|
||||
recordInboundSession: core.channel.session.recordInboundSession,
|
||||
record: agentRecord,
|
||||
runDispatch: () =>
|
||||
core.channel.reply.withReplyDispatcher({
|
||||
dispatcher: noopDispatcher,
|
||||
run: () =>
|
||||
core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: agentCtx,
|
||||
cfg,
|
||||
dispatcher: noopDispatcher,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1445,49 +1475,66 @@ export async function handleFeishuMessage(params: {
|
||||
});
|
||||
|
||||
log(`feishu[${account.accountId}]: dispatching to agent (session=${route.sessionKey})`);
|
||||
const { dispatchResult } = await core.channel.turn.runPrepared({
|
||||
const turnResult = await core.channel.turn.run({
|
||||
channel: "feishu",
|
||||
accountId: route.accountId,
|
||||
routeSessionKey: route.sessionKey,
|
||||
storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession: core.channel.session.recordInboundSession,
|
||||
record: {
|
||||
onRecordError: (err) => {
|
||||
log(
|
||||
`feishu[${account.accountId}]: failed to record inbound session ${route.sessionKey}: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
onPreDispatchFailure: () =>
|
||||
core.channel.reply.settleReplyDispatcher({
|
||||
dispatcher,
|
||||
onSettled: () => markDispatchIdle(),
|
||||
raw: ctx,
|
||||
adapter: {
|
||||
ingest: () => ({
|
||||
id: ctx.messageId,
|
||||
timestamp: messageCreateTimeMs,
|
||||
rawText: ctx.content,
|
||||
textForAgent: ctxPayload.BodyForAgent,
|
||||
textForCommands: ctxPayload.CommandBody,
|
||||
raw: ctx,
|
||||
}),
|
||||
runDispatch: () =>
|
||||
core.channel.reply.withReplyDispatcher({
|
||||
dispatcher,
|
||||
onSettled: () => {
|
||||
markDispatchIdle();
|
||||
resolveTurn: () => ({
|
||||
channel: "feishu",
|
||||
accountId: route.accountId,
|
||||
routeSessionKey: route.sessionKey,
|
||||
storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession: core.channel.session.recordInboundSession,
|
||||
record: {
|
||||
onRecordError: (err) => {
|
||||
log(
|
||||
`feishu[${account.accountId}]: failed to record inbound session ${route.sessionKey}: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
run: () =>
|
||||
core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
history: {
|
||||
isGroup,
|
||||
historyKey,
|
||||
historyMap: chatHistories,
|
||||
limit: historyLimit,
|
||||
},
|
||||
onPreDispatchFailure: () =>
|
||||
core.channel.reply.settleReplyDispatcher({
|
||||
dispatcher,
|
||||
replyOptions,
|
||||
onSettled: () => markDispatchIdle(),
|
||||
}),
|
||||
runDispatch: () =>
|
||||
core.channel.reply.withReplyDispatcher({
|
||||
dispatcher,
|
||||
onSettled: () => {
|
||||
markDispatchIdle();
|
||||
},
|
||||
run: () =>
|
||||
core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyOptions,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
const { queuedFinal, counts } = dispatchResult;
|
||||
|
||||
if (isGroup && historyKey && chatHistories) {
|
||||
clearHistoryEntriesIfEnabled({
|
||||
historyMap: chatHistories,
|
||||
historyKey,
|
||||
limit: historyLimit,
|
||||
});
|
||||
if (!turnResult.dispatched) {
|
||||
return;
|
||||
}
|
||||
const { dispatchResult } = turnResult;
|
||||
const { queuedFinal, counts } = dispatchResult;
|
||||
|
||||
log(
|
||||
`feishu[${account.accountId}]: dispatch complete (queuedFinal=${queuedFinal}, replies=${counts.final})`,
|
||||
|
||||
@@ -134,6 +134,26 @@ function createTestRuntime(overrides?: {
|
||||
recordInboundSession,
|
||||
},
|
||||
turn: {
|
||||
run: vi.fn(async (params: Parameters<PluginRuntime["channel"]["turn"]["run"]>[0]) => {
|
||||
const input = await params.adapter.ingest(params.raw);
|
||||
if (!input) {
|
||||
return {
|
||||
admission: { kind: "drop" as const, reason: "ingest-null" },
|
||||
dispatched: false,
|
||||
};
|
||||
}
|
||||
const eventClass = {
|
||||
kind: "message" as const,
|
||||
canStartAgentTurn: true,
|
||||
};
|
||||
const turn = await params.adapter.resolveTurn(input, eventClass, {});
|
||||
if (!("runDispatch" in turn)) {
|
||||
throw new Error("feishu comment test runtime only supports prepared turns");
|
||||
}
|
||||
return await runPrepared(
|
||||
turn as Parameters<PluginRuntime["channel"]["turn"]["runPrepared"]>[0],
|
||||
);
|
||||
}) as unknown as PluginRuntime["channel"]["turn"]["run"],
|
||||
runPrepared: runPrepared as unknown as PluginRuntime["channel"]["turn"]["runPrepared"],
|
||||
},
|
||||
pairing: {
|
||||
|
||||
@@ -241,42 +241,58 @@ export async function handleFeishuCommentEvent(
|
||||
`feishu[${account.accountId}]: dispatching drive comment to agent ` +
|
||||
`(session=${commentSessionKey} comment=${turn.commentId} type=${turn.noticeType})`,
|
||||
);
|
||||
const { dispatchResult } = await core.channel.turn.runPrepared({
|
||||
const turnResult = await core.channel.turn.run({
|
||||
channel: "feishu",
|
||||
accountId: route.accountId,
|
||||
routeSessionKey: commentSessionKey,
|
||||
storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession: core.channel.session.recordInboundSession,
|
||||
record: {
|
||||
onRecordError: (err) => {
|
||||
error(
|
||||
`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
onPreDispatchFailure: async () => {
|
||||
dispatchSettledBeforeStart = true;
|
||||
await core.channel.reply.settleReplyDispatcher({
|
||||
dispatcher,
|
||||
onSettled: () => {
|
||||
markRunComplete();
|
||||
markDispatchIdle();
|
||||
raw: turn,
|
||||
adapter: {
|
||||
ingest: () => ({
|
||||
id: turn.messageId,
|
||||
timestamp: parseTimestampMs(turn.timestamp),
|
||||
rawText: ctxPayload.RawBody ?? "",
|
||||
textForAgent: ctxPayload.BodyForAgent,
|
||||
textForCommands: ctxPayload.CommandBody,
|
||||
raw: turn,
|
||||
}),
|
||||
resolveTurn: () => ({
|
||||
channel: "feishu",
|
||||
accountId: route.accountId,
|
||||
routeSessionKey: commentSessionKey,
|
||||
storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession: core.channel.session.recordInboundSession,
|
||||
record: {
|
||||
onRecordError: (err) => {
|
||||
error(
|
||||
`feishu[${account.accountId}]: failed to record comment inbound session ${commentSessionKey}: ${String(err)}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
runDispatch: () =>
|
||||
core.channel.reply.withReplyDispatcher({
|
||||
dispatcher,
|
||||
run: () =>
|
||||
core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
cfg: effectiveCfg,
|
||||
onPreDispatchFailure: async () => {
|
||||
dispatchSettledBeforeStart = true;
|
||||
await core.channel.reply.settleReplyDispatcher({
|
||||
dispatcher,
|
||||
replyOptions,
|
||||
onSettled: () => {
|
||||
markRunComplete();
|
||||
markDispatchIdle();
|
||||
},
|
||||
});
|
||||
},
|
||||
runDispatch: () =>
|
||||
core.channel.reply.withReplyDispatcher({
|
||||
dispatcher,
|
||||
run: () =>
|
||||
core.channel.reply.dispatchReplyFromConfig({
|
||||
ctx: ctxPayload,
|
||||
cfg: effectiveCfg,
|
||||
dispatcher,
|
||||
replyOptions,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
const dispatchResult = turnResult.dispatched ? turnResult.dispatchResult : undefined;
|
||||
const queuedFinal = dispatchResult?.queuedFinal ?? false;
|
||||
const counts = dispatchResult?.counts ?? { tool: 0, block: 0, final: 0 };
|
||||
log(
|
||||
|
||||
Reference in New Issue
Block a user