mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
Expose paged channel action results (#88993)
* fix(discord): expose paged thread list results * fix(discord): keep thread pagination local * fix(discord): use archive timestamps for thread cursors * test(discord): cover thread pagination results --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
co-authored by
Peter Steinberger
parent
4a6666796f
commit
6c88811b4b
@@ -24,6 +24,75 @@ function hasDiscordComponentObjectKeys(value: unknown): value is Record<string,
|
||||
);
|
||||
}
|
||||
|
||||
function readDiscordThreadArchiveTimestamp(thread: unknown): string | undefined {
|
||||
if (!thread || typeof thread !== "object" || Array.isArray(thread)) {
|
||||
return undefined;
|
||||
}
|
||||
const record = thread as Record<string, unknown>;
|
||||
const metadata = record.thread_metadata;
|
||||
if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) {
|
||||
const archiveTimestamp = (metadata as Record<string, unknown>).archive_timestamp;
|
||||
if (typeof archiveTimestamp === "string" && archiveTimestamp.trim()) {
|
||||
return archiveTimestamp;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type DiscordThreadListActionResult = {
|
||||
ok: true;
|
||||
threads: unknown;
|
||||
complete: boolean;
|
||||
hasMore: boolean;
|
||||
returnedCount: number;
|
||||
source: "discord.threadList.archived" | "discord.threadList.active";
|
||||
query: {
|
||||
guildId: string;
|
||||
channelId?: string;
|
||||
includeArchived: boolean;
|
||||
before?: string;
|
||||
limit?: number;
|
||||
};
|
||||
nextBefore?: string;
|
||||
};
|
||||
|
||||
function normalizeDiscordThreadListActionResult(params: {
|
||||
value: unknown;
|
||||
includeArchived: boolean;
|
||||
channelId?: string;
|
||||
guildId: string;
|
||||
limit?: number;
|
||||
before?: string;
|
||||
}): DiscordThreadListActionResult {
|
||||
const record =
|
||||
params.value && typeof params.value === "object" && !Array.isArray(params.value)
|
||||
? (params.value as Record<string, unknown>)
|
||||
: undefined;
|
||||
const threadItems = Array.isArray(record?.threads) ? record.threads : [];
|
||||
const hasMore = record?.has_more === true;
|
||||
const nextBefore =
|
||||
params.includeArchived && hasMore
|
||||
? readDiscordThreadArchiveTimestamp(threadItems[threadItems.length - 1])
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
threads: params.value,
|
||||
complete: !hasMore,
|
||||
hasMore,
|
||||
returnedCount: threadItems.length,
|
||||
source: params.includeArchived ? "discord.threadList.archived" : "discord.threadList.active",
|
||||
query: {
|
||||
guildId: params.guildId,
|
||||
...(params.channelId ? { channelId: params.channelId } : {}),
|
||||
includeArchived: params.includeArchived,
|
||||
...(params.before ? { before: params.before } : {}),
|
||||
...(params.limit !== undefined ? { limit: params.limit } : {}),
|
||||
},
|
||||
...(nextBefore ? { nextBefore } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function appendDiscordThreadRenameResult(
|
||||
ctx: DiscordMessagingActionContext,
|
||||
params: {
|
||||
@@ -306,7 +375,16 @@ export async function handleDiscordMessageSendAction(ctx: DiscordMessagingAction
|
||||
},
|
||||
ctx.withOpts(),
|
||||
);
|
||||
return jsonResult({ ok: true, threads });
|
||||
return jsonResult(
|
||||
normalizeDiscordThreadListActionResult({
|
||||
value: threads,
|
||||
guildId,
|
||||
channelId,
|
||||
includeArchived: includeArchived === true,
|
||||
before,
|
||||
limit,
|
||||
}),
|
||||
);
|
||||
}
|
||||
case "threadReply": {
|
||||
if (!ctx.isActionEnabled("threads")) {
|
||||
|
||||
@@ -101,6 +101,7 @@ const {
|
||||
kickMemberDiscord,
|
||||
listGuildChannelsDiscord,
|
||||
listPinsDiscord,
|
||||
listThreadsDiscord,
|
||||
moveChannelDiscord,
|
||||
reactMessageDiscord,
|
||||
readMessagesDiscord,
|
||||
@@ -271,6 +272,138 @@ describe("handleDiscordMessagingAction", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("surfaces incomplete archived thread pages at the action boundary", async () => {
|
||||
listThreadsDiscord.mockResolvedValueOnce({
|
||||
threads: [
|
||||
{
|
||||
id: "thread-1",
|
||||
name: "Old project",
|
||||
thread_metadata: {
|
||||
archive_timestamp: "2026-05-25T17:00:00.000Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
members: [],
|
||||
has_more: true,
|
||||
});
|
||||
|
||||
const result = await handleMessagingAction(
|
||||
"threadList",
|
||||
{
|
||||
guildId: "G1",
|
||||
channelId: "C1",
|
||||
includeArchived: true,
|
||||
before: "2026-05-26T17:00:00.000Z",
|
||||
limit: 1,
|
||||
},
|
||||
enableAllActions,
|
||||
);
|
||||
|
||||
expect(mockCall(listThreadsDiscord, "listThreadsDiscord")).toEqual([
|
||||
{
|
||||
guildId: "G1",
|
||||
channelId: "C1",
|
||||
includeArchived: true,
|
||||
before: "2026-05-26T17:00:00.000Z",
|
||||
limit: 1,
|
||||
},
|
||||
{ cfg: DISCORD_TEST_CFG },
|
||||
]);
|
||||
expect(result.details).toMatchObject({
|
||||
ok: true,
|
||||
complete: false,
|
||||
hasMore: true,
|
||||
returnedCount: 1,
|
||||
source: "discord.threadList.archived",
|
||||
nextBefore: "2026-05-25T17:00:00.000Z",
|
||||
query: {
|
||||
guildId: "G1",
|
||||
channelId: "C1",
|
||||
includeArchived: true,
|
||||
before: "2026-05-26T17:00:00.000Z",
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
expect((result.details as { threads?: unknown }).threads).toEqual({
|
||||
threads: [
|
||||
{
|
||||
id: "thread-1",
|
||||
name: "Old project",
|
||||
thread_metadata: {
|
||||
archive_timestamp: "2026-05-25T17:00:00.000Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
members: [],
|
||||
has_more: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("omits archived thread pagination cursors when Discord omits archive timestamps", async () => {
|
||||
listThreadsDiscord.mockResolvedValueOnce({
|
||||
threads: [
|
||||
{
|
||||
id: "thread-without-archive-timestamp",
|
||||
name: "Legacy project",
|
||||
},
|
||||
],
|
||||
members: [],
|
||||
has_more: true,
|
||||
});
|
||||
|
||||
const result = await handleMessagingAction(
|
||||
"threadList",
|
||||
{
|
||||
guildId: "G1",
|
||||
channelId: "C1",
|
||||
includeArchived: true,
|
||||
limit: 1,
|
||||
},
|
||||
enableAllActions,
|
||||
);
|
||||
|
||||
expect(result.details).toMatchObject({
|
||||
ok: true,
|
||||
complete: false,
|
||||
hasMore: true,
|
||||
returnedCount: 1,
|
||||
source: "discord.threadList.archived",
|
||||
});
|
||||
expect(result.details).not.toHaveProperty("nextBefore");
|
||||
});
|
||||
|
||||
it("marks active thread results complete when Discord returns no pagination state", async () => {
|
||||
listThreadsDiscord.mockResolvedValueOnce({
|
||||
threads: [{ id: "thread-active", name: "Current project" }],
|
||||
members: [{ id: "member-1" }],
|
||||
});
|
||||
|
||||
const result = await handleMessagingAction(
|
||||
"threadList",
|
||||
{
|
||||
guildId: "G1",
|
||||
},
|
||||
enableAllActions,
|
||||
);
|
||||
|
||||
expect(result.details).toMatchObject({
|
||||
ok: true,
|
||||
complete: true,
|
||||
hasMore: false,
|
||||
returnedCount: 1,
|
||||
source: "discord.threadList.active",
|
||||
query: {
|
||||
guildId: "G1",
|
||||
includeArchived: false,
|
||||
},
|
||||
});
|
||||
expect((result.details as { threads?: unknown }).threads).toEqual({
|
||||
threads: [{ id: "thread-active", name: "Current project" }],
|
||||
members: [{ id: "member-1" }],
|
||||
});
|
||||
expect(result.details).not.toHaveProperty("nextBefore");
|
||||
});
|
||||
|
||||
it("resolves Discord DM targets for reaction adds", async () => {
|
||||
const resolveReactionTarget = vi.fn(async () => "DM1");
|
||||
discordMessagingActionRuntime.resolveDiscordReactionTargetChannelId = resolveReactionTarget;
|
||||
|
||||
Reference in New Issue
Block a user