






























@@ -0,0 +1,287 @@
1+import type { OpenClawConfig, ReplyToMode } from "openclaw/plugin-sdk/config-types";
2+import { resolveChannelModelOverride } from "openclaw/plugin-sdk/model-session-runtime";
3+import { buildAgentSessionKey } from "openclaw/plugin-sdk/routing";
4+import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
5+import {
6+normalizeOptionalString,
7+normalizeOptionalStringifiedId,
8+} from "openclaw/plugin-sdk/text-runtime";
9+import {
10+ChannelType,
11+createThread,
12+editChannel,
13+getChannelMessage,
14+type Client,
15+} from "../internal/discord.js";
16+import { resolveDiscordMessageChannelId } from "./message-utils.js";
17+import { generateThreadTitle } from "./thread-title.js";
18+import { resolveDiscordReplyDeliveryPlan, sanitizeDiscordThreadName } from "./threading.starter.js";
19+import type {
20+DiscordAutoThreadContext,
21+DiscordAutoThreadReplyPlan,
22+DiscordMessageEvent,
23+MaybeCreateDiscordAutoThreadParams,
24+} from "./threading.types.js";
25+26+function resolveTrimmedDiscordMessageChannelId(params: {
27+message: DiscordMessageEvent["message"];
28+messageChannelId?: string;
29+}) {
30+return (
31+params.messageChannelId ||
32+resolveDiscordMessageChannelId({
33+message: params.message,
34+})
35+).trim();
36+}
37+38+export function resolveDiscordAutoThreadContext(params: {
39+agentId: string;
40+channel: string;
41+messageChannelId: string;
42+createdThreadId?: string | null;
43+parentInheritanceEnabled?: boolean;
44+}): DiscordAutoThreadContext | null {
45+const createdThreadId = normalizeOptionalStringifiedId(params.createdThreadId) ?? "";
46+if (!createdThreadId) {
47+return null;
48+}
49+const messageChannelId = normalizeOptionalString(params.messageChannelId) ?? "";
50+if (!messageChannelId) {
51+return null;
52+}
53+54+const threadSessionKey = buildAgentSessionKey({
55+agentId: params.agentId,
56+channel: params.channel,
57+peer: { kind: "channel", id: createdThreadId },
58+});
59+const parentSessionKey = buildAgentSessionKey({
60+agentId: params.agentId,
61+channel: params.channel,
62+peer: { kind: "channel", id: messageChannelId },
63+});
64+65+return {
66+ createdThreadId,
67+From: `${params.channel}:channel:${createdThreadId}`,
68+To: `channel:${createdThreadId}`,
69+OriginatingTo: `channel:${createdThreadId}`,
70+SessionKey: threadSessionKey,
71+ModelParentSessionKey: parentSessionKey,
72+ ...(params.parentInheritanceEnabled === true ? { ParentSessionKey: parentSessionKey } : {}),
73+};
74+}
75+76+export async function resolveDiscordAutoThreadReplyPlan(
77+params: MaybeCreateDiscordAutoThreadParams & {
78+replyToMode: ReplyToMode;
79+agentId: string;
80+channel: string;
81+cfg: OpenClawConfig;
82+threadParentInheritanceEnabled?: boolean;
83+},
84+): Promise<DiscordAutoThreadReplyPlan> {
85+const messageChannelId = resolveTrimmedDiscordMessageChannelId(params);
86+const targetChannelId = params.threadChannel?.id ?? (messageChannelId || "unknown");
87+const originalReplyTarget = `channel:${targetChannelId}`;
88+const createdThreadId = await maybeCreateDiscordAutoThread({
89+client: params.client,
90+message: params.message,
91+messageChannelId: messageChannelId || undefined,
92+channel: params.channel,
93+isGuildMessage: params.isGuildMessage,
94+channelConfig: params.channelConfig,
95+threadChannel: params.threadChannel,
96+channelType: params.channelType,
97+channelName: params.channelName,
98+channelDescription: params.channelDescription,
99+baseText: params.baseText,
100+combinedBody: params.combinedBody,
101+cfg: params.cfg,
102+agentId: params.agentId,
103+});
104+const deliveryPlan = resolveDiscordReplyDeliveryPlan({
105+replyTarget: originalReplyTarget,
106+replyToMode: params.replyToMode,
107+messageId: params.message.id,
108+threadChannel: params.threadChannel,
109+ createdThreadId,
110+});
111+const autoThreadContext = params.isGuildMessage
112+ ? resolveDiscordAutoThreadContext({
113+agentId: params.agentId,
114+channel: params.channel,
115+ messageChannelId,
116+ createdThreadId,
117+parentInheritanceEnabled: params.threadParentInheritanceEnabled,
118+})
119+ : null;
120+return { ...deliveryPlan, createdThreadId, autoThreadContext };
121+}
122+123+export async function maybeCreateDiscordAutoThread(
124+params: MaybeCreateDiscordAutoThreadParams,
125+): Promise<string | undefined> {
126+if (!params.isGuildMessage) {
127+return undefined;
128+}
129+if (!params.channelConfig?.autoThread) {
130+return undefined;
131+}
132+if (params.threadChannel) {
133+return undefined;
134+}
135+if (
136+params.channelType === ChannelType.GuildForum ||
137+params.channelType === ChannelType.GuildMedia ||
138+params.channelType === ChannelType.GuildVoice ||
139+params.channelType === ChannelType.GuildStageVoice
140+) {
141+return undefined;
142+}
143+144+const messageChannelId = resolveTrimmedDiscordMessageChannelId(params);
145+if (!messageChannelId) {
146+return undefined;
147+}
148+try {
149+const rawThreadSource = params.baseText || params.combinedBody || "Thread";
150+const threadName = sanitizeDiscordThreadName(rawThreadSource, params.message.id);
151+const archiveDuration = params.channelConfig?.autoArchiveDuration
152+ ? Number(params.channelConfig.autoArchiveDuration)
153+ : 60;
154+155+const created = await createThread<{ id?: string }>(
156+params.client.rest,
157+messageChannelId,
158+{
159+body: {
160+name: threadName,
161+auto_archive_duration: archiveDuration,
162+},
163+},
164+params.message.id,
165+);
166+const createdId = created?.id || "";
167+if (
168+createdId &&
169+params.channelConfig?.autoThreadName === "generated" &&
170+params.cfg &&
171+params.agentId
172+) {
173+const modelRef = resolveDiscordThreadTitleModelRef({
174+cfg: params.cfg,
175+channel: params.channel,
176+agentId: params.agentId,
177+threadId: createdId,
178+ messageChannelId,
179+channelName: params.channelName,
180+});
181+void maybeRenameDiscordAutoThread({
182+client: params.client,
183+threadId: createdId,
184+currentName: threadName,
185+fallbackId: params.message.id,
186+sourceText: rawThreadSource,
187+ modelRef,
188+channelName: params.channelName,
189+channelDescription: params.channelDescription,
190+cfg: params.cfg,
191+agentId: params.agentId,
192+});
193+}
194+return createdId || undefined;
195+} catch (err) {
196+logVerbose(
197+`discord: autoThread creation failed for ${messageChannelId}/${params.message.id}: ${String(err)}`,
198+);
199+try {
200+const msg = (await getChannelMessage(
201+params.client.rest,
202+messageChannelId,
203+params.message.id,
204+)) as {
205+thread?: { id?: string };
206+};
207+const existingThreadId = msg?.thread?.id || "";
208+if (existingThreadId) {
209+logVerbose(
210+`discord: autoThread reusing existing thread ${existingThreadId} on ${messageChannelId}/${params.message.id}`,
211+);
212+return existingThreadId;
213+}
214+} catch {
215+// If the refetch also fails, fall through to return undefined.
216+}
217+return undefined;
218+}
219+}
220+221+function resolveDiscordThreadTitleModelRef(params: {
222+cfg: OpenClawConfig;
223+channel?: string;
224+agentId: string;
225+threadId: string;
226+messageChannelId: string;
227+channelName?: string;
228+}): string | undefined {
229+const channel = params.channel?.trim();
230+if (!channel) {
231+return undefined;
232+}
233+const parentSessionKey = buildAgentSessionKey({
234+agentId: params.agentId,
235+ channel,
236+peer: { kind: "channel", id: params.messageChannelId },
237+});
238+const channelLabel = params.channelName?.trim();
239+const groupChannel = channelLabel ? `#${channelLabel}` : undefined;
240+const channelOverride = resolveChannelModelOverride({
241+cfg: params.cfg,
242+ channel,
243+groupId: params.threadId,
244+groupChatType: "channel",
245+ groupChannel,
246+groupSubject: groupChannel,
247+ parentSessionKey,
248+});
249+return channelOverride?.model;
250+}
251+252+async function maybeRenameDiscordAutoThread(params: {
253+client: Client;
254+threadId: string;
255+currentName: string;
256+fallbackId: string;
257+sourceText: string;
258+modelRef?: string;
259+channelName?: string;
260+channelDescription?: string;
261+cfg: OpenClawConfig;
262+agentId: string;
263+}): Promise<void> {
264+try {
265+const fallbackName = sanitizeDiscordThreadName("", params.fallbackId);
266+const generated = await generateThreadTitle({
267+cfg: params.cfg,
268+agentId: params.agentId,
269+messageText: params.sourceText,
270+modelRef: params.modelRef,
271+channelName: params.channelName,
272+channelDescription: params.channelDescription,
273+});
274+if (!generated) {
275+return;
276+}
277+const nextName = sanitizeDiscordThreadName(generated, params.fallbackId);
278+if (!nextName || nextName === params.currentName || nextName === fallbackName) {
279+return;
280+}
281+await editChannel(params.client.rest, params.threadId, {
282+body: { name: nextName },
283+});
284+} catch (err) {
285+logVerbose(`discord: autoThread rename failed for ${params.threadId}: ${String(err)}`);
286+}
287+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。