mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 22:09:57 +00:00
refactor(runtime): consolidate followup, gateway, and provider dedupe paths
This commit is contained in:
@@ -99,6 +99,11 @@ type SlackModalEventBase = {
|
||||
};
|
||||
|
||||
type SlackModalInteractionKind = "view_submission" | "view_closed";
|
||||
type SlackModalEventHandlerArgs = { ack: () => Promise<void>; body: unknown };
|
||||
type RegisterSlackModalHandler = (
|
||||
matcher: RegExp,
|
||||
handler: (args: SlackModalEventHandlerArgs) => Promise<void>,
|
||||
) => void;
|
||||
|
||||
function readOptionValues(options: unknown): string[] | undefined {
|
||||
if (!Array.isArray(options)) {
|
||||
@@ -483,6 +488,24 @@ function emitSlackModalLifecycleEvent(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function registerModalLifecycleHandler(params: {
|
||||
register: RegisterSlackModalHandler;
|
||||
matcher: RegExp;
|
||||
ctx: SlackMonitorContext;
|
||||
interactionType: SlackModalInteractionKind;
|
||||
contextPrefix: "slack:interaction:view" | "slack:interaction:view-closed";
|
||||
}) {
|
||||
params.register(params.matcher, async ({ ack, body }: SlackModalEventHandlerArgs) => {
|
||||
await ack();
|
||||
emitSlackModalLifecycleEvent({
|
||||
ctx: params.ctx,
|
||||
body: body as SlackModalBody,
|
||||
interactionType: params.interactionType,
|
||||
contextPrefix: params.contextPrefix,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContext }) {
|
||||
const { ctx } = params;
|
||||
if (typeof ctx.app.action !== "function") {
|
||||
@@ -646,27 +669,20 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
if (typeof ctx.app.view !== "function") {
|
||||
return;
|
||||
}
|
||||
const modalMatcher = new RegExp(`^${OPENCLAW_ACTION_PREFIX}`);
|
||||
|
||||
// Handle OpenClaw modal submissions with callback_ids scoped by our prefix.
|
||||
ctx.app.view(
|
||||
new RegExp(`^${OPENCLAW_ACTION_PREFIX}`),
|
||||
async ({ ack, body }: { ack: () => Promise<void>; body: unknown }) => {
|
||||
await ack();
|
||||
emitSlackModalLifecycleEvent({
|
||||
ctx,
|
||||
body: body as SlackModalBody,
|
||||
interactionType: "view_submission",
|
||||
contextPrefix: "slack:interaction:view",
|
||||
});
|
||||
},
|
||||
);
|
||||
registerModalLifecycleHandler({
|
||||
register: (matcher, handler) => ctx.app.view(matcher, handler),
|
||||
matcher: modalMatcher,
|
||||
ctx,
|
||||
interactionType: "view_submission",
|
||||
contextPrefix: "slack:interaction:view",
|
||||
});
|
||||
|
||||
const viewClosed = (
|
||||
ctx.app as unknown as {
|
||||
viewClosed?: (
|
||||
matcher: RegExp,
|
||||
handler: (args: { ack: () => Promise<void>; body: unknown }) => Promise<void>,
|
||||
) => void;
|
||||
viewClosed?: RegisterSlackModalHandler;
|
||||
}
|
||||
).viewClosed;
|
||||
if (typeof viewClosed !== "function") {
|
||||
@@ -674,16 +690,11 @@ export function registerSlackInteractionEvents(params: { ctx: SlackMonitorContex
|
||||
}
|
||||
|
||||
// Handle modal close events so agent workflows can react to cancelled forms.
|
||||
viewClosed(
|
||||
new RegExp(`^${OPENCLAW_ACTION_PREFIX}`),
|
||||
async ({ ack, body }: { ack: () => Promise<void>; body: unknown }) => {
|
||||
await ack();
|
||||
emitSlackModalLifecycleEvent({
|
||||
ctx,
|
||||
body: body as SlackModalBody,
|
||||
interactionType: "view_closed",
|
||||
contextPrefix: "slack:interaction:view-closed",
|
||||
});
|
||||
},
|
||||
);
|
||||
registerModalLifecycleHandler({
|
||||
register: viewClosed,
|
||||
matcher: modalMatcher,
|
||||
ctx,
|
||||
interactionType: "view_closed",
|
||||
contextPrefix: "slack:interaction:view-closed",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -292,17 +292,7 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
hasStreamedMessage = false;
|
||||
}
|
||||
|
||||
const replyThreadTs = replyPlan.nextThreadTs();
|
||||
await deliverReplies({
|
||||
replies: [payload],
|
||||
target: prepared.replyTarget,
|
||||
token: ctx.botToken,
|
||||
accountId: account.accountId,
|
||||
runtime,
|
||||
textLimit: ctx.textLimit,
|
||||
replyThreadTs,
|
||||
});
|
||||
replyPlan.markSent();
|
||||
await deliverNormally(payload);
|
||||
},
|
||||
onError: (err, info) => {
|
||||
runtime.error?.(danger(`slack ${info.kind} reply failed: ${String(err)}`));
|
||||
@@ -362,6 +352,18 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
draftStream.update(trimmed);
|
||||
hasStreamedMessage = true;
|
||||
};
|
||||
const onDraftBoundary =
|
||||
useStreaming || !previewStreamingEnabled
|
||||
? undefined
|
||||
: async () => {
|
||||
if (hasStreamedMessage) {
|
||||
draftStream.forceNewMessage();
|
||||
hasStreamedMessage = false;
|
||||
appendRenderedText = "";
|
||||
appendSourceText = "";
|
||||
statusUpdateCount = 0;
|
||||
}
|
||||
};
|
||||
|
||||
const { queuedFinal, counts } = await dispatchInboundMessage({
|
||||
ctx: prepared.ctxPayload,
|
||||
@@ -384,32 +386,8 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
: async (payload) => {
|
||||
updateDraftFromPartial(payload.text);
|
||||
},
|
||||
onAssistantMessageStart: useStreaming
|
||||
? undefined
|
||||
: !previewStreamingEnabled
|
||||
? undefined
|
||||
: async () => {
|
||||
if (hasStreamedMessage) {
|
||||
draftStream.forceNewMessage();
|
||||
hasStreamedMessage = false;
|
||||
appendRenderedText = "";
|
||||
appendSourceText = "";
|
||||
statusUpdateCount = 0;
|
||||
}
|
||||
},
|
||||
onReasoningEnd: useStreaming
|
||||
? undefined
|
||||
: !previewStreamingEnabled
|
||||
? undefined
|
||||
: async () => {
|
||||
if (hasStreamedMessage) {
|
||||
draftStream.forceNewMessage();
|
||||
hasStreamedMessage = false;
|
||||
appendRenderedText = "";
|
||||
appendSourceText = "";
|
||||
statusUpdateCount = 0;
|
||||
}
|
||||
},
|
||||
onAssistantMessageStart: onDraftBoundary,
|
||||
onReasoningEnd: onDraftBoundary,
|
||||
},
|
||||
});
|
||||
await draftStream.flush();
|
||||
|
||||
@@ -168,6 +168,19 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
};
|
||||
}
|
||||
|
||||
function createThreadReplyMessage(overrides: Partial<SlackMessageEvent>): SlackMessageEvent {
|
||||
return createSlackMessage({
|
||||
channel: "C123",
|
||||
channel_type: "channel",
|
||||
thread_ts: "100.000",
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function prepareThreadMessage(ctx: SlackMonitorContext, overrides: Partial<SlackMessageEvent>) {
|
||||
return prepareMessageWith(ctx, createThreadAccount(), createThreadReplyMessage(overrides));
|
||||
}
|
||||
|
||||
it("produces a finalized MsgContext", async () => {
|
||||
const message: SlackMessageEvent = {
|
||||
channel: "D123",
|
||||
@@ -298,17 +311,10 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
});
|
||||
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
createThreadAccount(),
|
||||
createSlackMessage({
|
||||
channel: "C123",
|
||||
channel_type: "channel",
|
||||
text: "current message",
|
||||
ts: "101.000",
|
||||
thread_ts: "100.000",
|
||||
}),
|
||||
);
|
||||
const prepared = await prepareThreadMessage(slackCtx, {
|
||||
text: "current message",
|
||||
ts: "101.000",
|
||||
});
|
||||
|
||||
expect(prepared).toBeTruthy();
|
||||
expect(prepared!.ctxPayload.IsFirstThreadTurn).toBe(true);
|
||||
@@ -347,17 +353,11 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
slackCtx.resolveUserName = async () => ({ name: "Alice" });
|
||||
slackCtx.resolveChannelName = async () => ({ name: "general", type: "channel" });
|
||||
|
||||
const prepared = await prepareMessageWith(
|
||||
slackCtx,
|
||||
createThreadAccount(),
|
||||
createSlackMessage({
|
||||
channel: "C123",
|
||||
channel_type: "channel",
|
||||
text: "reply in old thread",
|
||||
ts: "201.000",
|
||||
thread_ts: "200.000",
|
||||
}),
|
||||
);
|
||||
const prepared = await prepareThreadMessage(slackCtx, {
|
||||
text: "reply in old thread",
|
||||
ts: "201.000",
|
||||
thread_ts: "200.000",
|
||||
});
|
||||
|
||||
expect(prepared).toBeTruthy();
|
||||
expect(prepared!.ctxPayload.IsFirstThreadTurn).toBeUndefined();
|
||||
|
||||
@@ -147,6 +147,13 @@ function parseSlackCommandArgValue(raw?: string | null): {
|
||||
};
|
||||
}
|
||||
|
||||
function buildSlackArgMenuOptions(choices: EncodedMenuChoice[]) {
|
||||
return choices.map((choice) => ({
|
||||
text: { type: "plain_text", text: choice.label.slice(0, 75) },
|
||||
value: choice.value,
|
||||
}));
|
||||
}
|
||||
|
||||
function buildSlackCommandArgMenuBlocks(params: {
|
||||
title: string;
|
||||
command: string;
|
||||
@@ -185,10 +192,7 @@ function buildSlackCommandArgMenuBlocks(params: {
|
||||
type: "overflow",
|
||||
action_id: SLACK_COMMAND_ARG_ACTION_ID,
|
||||
confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }),
|
||||
options: encodedChoices.map((choice) => ({
|
||||
text: { type: "plain_text", text: choice.label.slice(0, 75) },
|
||||
value: choice.value,
|
||||
})),
|
||||
options: buildSlackArgMenuOptions(encodedChoices),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -238,10 +242,7 @@ function buildSlackCommandArgMenuBlocks(params: {
|
||||
text:
|
||||
index === 0 ? `Choose ${params.arg}` : `Choose ${params.arg} (${index + 1})`,
|
||||
},
|
||||
options: choices.map((choice) => ({
|
||||
text: { type: "plain_text", text: choice.label.slice(0, 75) },
|
||||
value: choice.value,
|
||||
})),
|
||||
options: buildSlackArgMenuOptions(choices),
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user