@shadowob/openclaw-shadowob 1.1.0 → 1.1.3-dev.251
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2R7AKKRH.js +1959 -0
- package/dist/chunk-NBNZ7NVR.js +175 -0
- package/dist/index.js +1011 -4
- package/dist/monitor-ZZHGWMZF.js +16 -0
- package/dist/setup-entry.js +3 -3
- package/openclaw.plugin.json +118 -0
- package/package.json +4 -10
- package/skills/shadowob/SKILL.md +37 -57
- package/dist/chunk-QFUUQPJZ.js +0 -1062
- package/dist/chunk-XPNVTXKL.js +0 -712
- package/dist/monitor-G2J667KY.js +0 -6
package/dist/chunk-QFUUQPJZ.js
DELETED
|
@@ -1,1062 +0,0 @@
|
|
|
1
|
-
// src/monitor.ts
|
|
2
|
-
import { ShadowClient, ShadowSocket } from "@shadowob/sdk";
|
|
3
|
-
|
|
4
|
-
// src/runtime.ts
|
|
5
|
-
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
6
|
-
var store = createPluginRuntimeStore(
|
|
7
|
-
"Shadow runtime not initialized \u2014 plugin not registered yet"
|
|
8
|
-
);
|
|
9
|
-
var setShadowRuntime = store.setRuntime;
|
|
10
|
-
var getShadowRuntime = store.getRuntime;
|
|
11
|
-
var tryGetShadowRuntime = store.tryGetRuntime;
|
|
12
|
-
|
|
13
|
-
// src/monitor.ts
|
|
14
|
-
async function getDataDir() {
|
|
15
|
-
const nodeOs = await import("os");
|
|
16
|
-
const nodePath = await import("path");
|
|
17
|
-
const dataDir = process.env.OPENCLAW_DATA_DIR;
|
|
18
|
-
return dataDir || nodePath.join(nodeOs.homedir(), ".openclaw");
|
|
19
|
-
}
|
|
20
|
-
function createTypingCallbacks(params) {
|
|
21
|
-
const {
|
|
22
|
-
start,
|
|
23
|
-
stop,
|
|
24
|
-
onStartError,
|
|
25
|
-
onStopError,
|
|
26
|
-
keepaliveIntervalMs = 2e3,
|
|
27
|
-
maxDurationMs = 12e4
|
|
28
|
-
} = params;
|
|
29
|
-
let keepaliveTimer = null;
|
|
30
|
-
let maxDurationTimer = null;
|
|
31
|
-
const cleanup = () => {
|
|
32
|
-
if (keepaliveTimer) {
|
|
33
|
-
clearInterval(keepaliveTimer);
|
|
34
|
-
keepaliveTimer = null;
|
|
35
|
-
}
|
|
36
|
-
if (maxDurationTimer) {
|
|
37
|
-
clearTimeout(maxDurationTimer);
|
|
38
|
-
maxDurationTimer = null;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
return {
|
|
42
|
-
onReplyStart: async () => {
|
|
43
|
-
try {
|
|
44
|
-
await start();
|
|
45
|
-
} catch (err) {
|
|
46
|
-
onStartError(err);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
keepaliveTimer = setInterval(async () => {
|
|
50
|
-
try {
|
|
51
|
-
await start();
|
|
52
|
-
} catch (err) {
|
|
53
|
-
onStartError(err);
|
|
54
|
-
}
|
|
55
|
-
}, keepaliveIntervalMs);
|
|
56
|
-
maxDurationTimer = setTimeout(() => {
|
|
57
|
-
cleanup();
|
|
58
|
-
stop?.().catch((err) => onStopError?.(err));
|
|
59
|
-
}, maxDurationMs);
|
|
60
|
-
},
|
|
61
|
-
onIdle: () => {
|
|
62
|
-
cleanup();
|
|
63
|
-
},
|
|
64
|
-
onCleanup: () => {
|
|
65
|
-
cleanup();
|
|
66
|
-
stop?.().catch((err) => onStopError?.(err));
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
async function processShadowMessage(params) {
|
|
71
|
-
const {
|
|
72
|
-
message,
|
|
73
|
-
account,
|
|
74
|
-
accountId,
|
|
75
|
-
config,
|
|
76
|
-
runtime,
|
|
77
|
-
core,
|
|
78
|
-
botUserId,
|
|
79
|
-
botUsername,
|
|
80
|
-
agentId,
|
|
81
|
-
channelPolicies,
|
|
82
|
-
channelServerMap,
|
|
83
|
-
socket
|
|
84
|
-
} = params;
|
|
85
|
-
const cfg = config;
|
|
86
|
-
const senderLabel = message.author?.username ?? message.authorId;
|
|
87
|
-
if (message.authorId === botUserId) {
|
|
88
|
-
runtime.log?.(`[msg] Skipping own message ${message.id}`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
let isProcessingBuddyMessage = false;
|
|
92
|
-
if (message.author?.isBot) {
|
|
93
|
-
const policy2 = channelPolicies.get(message.channelId);
|
|
94
|
-
const policyConfig2 = policy2?.config;
|
|
95
|
-
if (!policyConfig2?.replyToBuddy) {
|
|
96
|
-
runtime.log?.(
|
|
97
|
-
`[msg] Skipping bot message from ${senderLabel} (replyToBuddy=false) (${message.id})`
|
|
98
|
-
);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const maxDepth = policyConfig2.maxBuddyChainDepth ?? 3;
|
|
102
|
-
const chainMeta = message.metadata?.agentChain;
|
|
103
|
-
if (chainMeta) {
|
|
104
|
-
if (chainMeta.depth >= maxDepth) {
|
|
105
|
-
runtime.log?.(
|
|
106
|
-
`[msg] Buddy chain depth ${chainMeta.depth} >= max ${maxDepth}, stopping loop (${message.id})`
|
|
107
|
-
);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (chainMeta.participants?.includes(botUserId)) {
|
|
111
|
-
runtime.log?.(
|
|
112
|
-
`[msg] Already in buddy chain [${chainMeta.participants.join(", ")}], skipping to prevent loop (${message.id})`
|
|
113
|
-
);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
const senderAgentId = message.author?.id;
|
|
117
|
-
if (senderAgentId && policyConfig2.buddyBlacklist?.includes(senderAgentId)) {
|
|
118
|
-
runtime.log?.(
|
|
119
|
-
`[msg] Sender agent ${senderAgentId} is in blacklist, skipping (${message.id})`
|
|
120
|
-
);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (senderAgentId && policyConfig2.buddyWhitelist?.length && !policyConfig2.buddyWhitelist.includes(senderAgentId)) {
|
|
124
|
-
runtime.log?.(
|
|
125
|
-
`[msg] Sender agent ${senderAgentId} not in whitelist, skipping (${message.id})`
|
|
126
|
-
);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
isProcessingBuddyMessage = true;
|
|
131
|
-
runtime.log?.(
|
|
132
|
-
`[msg] Processing bot message from ${senderLabel} (replyToBuddy=true) (${message.id})`
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
const channelId = message.channelId;
|
|
136
|
-
const policy = channelPolicies.get(channelId);
|
|
137
|
-
if (policy && !policy.listen) {
|
|
138
|
-
runtime.log?.(`[msg] Policy blocks listen for channel ${channelId}, skipping`);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (policy && !policy.reply) {
|
|
142
|
-
runtime.log?.(`[msg] Policy blocks reply for channel ${channelId}, skipping (${message.id})`);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
let wasMentionedExplicitly = false;
|
|
146
|
-
if (policy?.mentionOnly) {
|
|
147
|
-
const escapedUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
148
|
-
const mentionRegex2 = new RegExp(`@${escapedUsername}(?:\\s|$)`, "i");
|
|
149
|
-
wasMentionedExplicitly = mentionRegex2.test(message.content);
|
|
150
|
-
if (!wasMentionedExplicitly) {
|
|
151
|
-
runtime.log?.(
|
|
152
|
-
`[msg] mentionOnly policy \u2014 no @${botUsername} mention found, skipping (${message.id})`
|
|
153
|
-
);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
runtime.log?.(
|
|
157
|
-
`[msg] mentionOnly policy \u2014 @${botUsername} mentioned, processing (${message.id})`
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
const policyConfig = policy?.config;
|
|
161
|
-
if (policyConfig?.replyToUsers?.length) {
|
|
162
|
-
const allowedUsers = policyConfig.replyToUsers.map((u) => u.toLowerCase());
|
|
163
|
-
const senderUser = (message.author?.username ?? "").toLowerCase();
|
|
164
|
-
if (!allowedUsers.includes(senderUser)) {
|
|
165
|
-
runtime.log?.(
|
|
166
|
-
`[msg] replyToUsers policy \u2014 sender "${senderUser}" not in allowed list, skipping (${message.id})`
|
|
167
|
-
);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (policyConfig?.keywords?.length) {
|
|
172
|
-
const lowerContent = message.content.toLowerCase();
|
|
173
|
-
const matched = policyConfig.keywords.some((kw) => lowerContent.includes(kw.toLowerCase()));
|
|
174
|
-
if (!matched) {
|
|
175
|
-
runtime.log?.(`[msg] keywords policy \u2014 no matching keyword found, skipping (${message.id})`);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
runtime.log?.(`[msg] keywords policy \u2014 keyword matched, processing (${message.id})`);
|
|
179
|
-
}
|
|
180
|
-
const smartReplyEnabled = policyConfig?.smartReply !== false;
|
|
181
|
-
if (smartReplyEnabled && !isProcessingBuddyMessage && !wasMentionedExplicitly) {
|
|
182
|
-
const mentionPattern = /@([a-zA-Z0-9_\-\u4e00-\u9fa5]+)/g;
|
|
183
|
-
const allMentions = message.content.match(mentionPattern) || [];
|
|
184
|
-
const mentionsWithoutSelf = allMentions.filter((m) => {
|
|
185
|
-
const mentionedUser = m.slice(1).toLowerCase();
|
|
186
|
-
return mentionedUser !== botUsername.toLowerCase();
|
|
187
|
-
});
|
|
188
|
-
if (allMentions.length > 0 && mentionsWithoutSelf.length === allMentions.length) {
|
|
189
|
-
runtime.log?.(
|
|
190
|
-
`[msg] Smart reply: message @mentions others (${allMentions.join(", ")}) but not @${botUsername}, skipping (${message.id})`
|
|
191
|
-
);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const replyToData = message.replyTo;
|
|
195
|
-
if (replyToData?.authorId && replyToData.authorId !== botUserId) {
|
|
196
|
-
const selfMentioned = allMentions.some((m) => {
|
|
197
|
-
const mentionedUser = m.slice(1).toLowerCase();
|
|
198
|
-
return mentionedUser === botUsername.toLowerCase();
|
|
199
|
-
});
|
|
200
|
-
if (!selfMentioned) {
|
|
201
|
-
runtime.log?.(
|
|
202
|
-
`[msg] Smart reply: message is a reply to another user (${replyToData.authorId}), not this Buddy, skipping (${message.id})`
|
|
203
|
-
);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
runtime.log?.(
|
|
209
|
-
`[msg] Processing message from ${senderLabel}: "${message.content.slice(0, 80)}" (${message.id})`
|
|
210
|
-
);
|
|
211
|
-
const senderName = message.author?.displayName ?? message.author?.username ?? "Unknown";
|
|
212
|
-
const senderUsername = message.author?.username ?? "";
|
|
213
|
-
const senderId = message.authorId;
|
|
214
|
-
const rawBody = message.content;
|
|
215
|
-
const chatType = message.threadId ? "thread" : "channel";
|
|
216
|
-
const peerId = message.threadId ? `${channelId}:thread:${message.threadId}` : channelId;
|
|
217
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
218
|
-
cfg,
|
|
219
|
-
channel: "shadowob",
|
|
220
|
-
accountId,
|
|
221
|
-
peer: { kind: "group", id: peerId }
|
|
222
|
-
});
|
|
223
|
-
runtime.log?.(`[routing] Resolved agent: ${route.agentId} (account ${accountId})`);
|
|
224
|
-
const body = core.channel.reply.formatAgentEnvelope({
|
|
225
|
-
channel: "Shadow",
|
|
226
|
-
from: senderName,
|
|
227
|
-
timestamp: new Date(message.createdAt).getTime(),
|
|
228
|
-
envelope: core.channel.reply.resolveEnvelopeFormatOptions(cfg),
|
|
229
|
-
body: rawBody
|
|
230
|
-
});
|
|
231
|
-
const attachmentUrls = (message.attachments ?? []).map((a) => a.url).filter(Boolean);
|
|
232
|
-
const markdownMediaRegex = /!?\[[^\]]*\]\(([^)]+)\)/g;
|
|
233
|
-
const markdownUrls = [];
|
|
234
|
-
for (const mdMatch of rawBody.matchAll(markdownMediaRegex)) {
|
|
235
|
-
const url = mdMatch[1];
|
|
236
|
-
if (url.startsWith("/") && url.includes("/uploads/")) {
|
|
237
|
-
markdownUrls.push(url);
|
|
238
|
-
} else if (url.startsWith("http")) {
|
|
239
|
-
markdownUrls.push(url);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
const allRawUrls = [.../* @__PURE__ */ new Set([...attachmentUrls, ...markdownUrls])];
|
|
243
|
-
const mediaClient = new ShadowClient(account.serverUrl, account.token);
|
|
244
|
-
const localMediaPaths = [];
|
|
245
|
-
const localMediaTypes = [];
|
|
246
|
-
const resolvedMediaUrls = [];
|
|
247
|
-
const inferMimeType = (filename, headerType) => {
|
|
248
|
-
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
249
|
-
const map = {
|
|
250
|
-
jpg: "image/jpeg",
|
|
251
|
-
jpeg: "image/jpeg",
|
|
252
|
-
png: "image/png",
|
|
253
|
-
gif: "image/gif",
|
|
254
|
-
webp: "image/webp",
|
|
255
|
-
svg: "image/svg+xml",
|
|
256
|
-
mp4: "video/mp4",
|
|
257
|
-
webm: "video/webm",
|
|
258
|
-
mp3: "audio/mpeg",
|
|
259
|
-
wav: "audio/wav",
|
|
260
|
-
ogg: "audio/ogg",
|
|
261
|
-
pdf: "application/pdf"
|
|
262
|
-
};
|
|
263
|
-
return map[ext] ?? headerType ?? "application/octet-stream";
|
|
264
|
-
};
|
|
265
|
-
if (allRawUrls.length > 0) {
|
|
266
|
-
const fsPromises = await import("fs/promises");
|
|
267
|
-
const nodePath = await import("path");
|
|
268
|
-
const nodeCrypto = await import("crypto");
|
|
269
|
-
const dataDir = await getDataDir();
|
|
270
|
-
const mediaDir = nodePath.join(dataDir, "media", "inbound");
|
|
271
|
-
await fsPromises.mkdir(mediaDir, { recursive: true });
|
|
272
|
-
for (const rawUrl of allRawUrls) {
|
|
273
|
-
try {
|
|
274
|
-
const downloaded = await mediaClient.downloadFile(rawUrl);
|
|
275
|
-
const uuid = nodeCrypto.randomUUID();
|
|
276
|
-
const ext = nodePath.extname(downloaded.filename) || ".bin";
|
|
277
|
-
const safeBase = downloaded.filename.replace(/[^a-zA-Z0-9._\u4e00-\u9fff-]/g, "_").slice(0, 100);
|
|
278
|
-
const localFilename = `${safeBase}---${uuid}${ext.startsWith(".") ? "" : "."}${ext}`;
|
|
279
|
-
const localPath = nodePath.join(mediaDir, localFilename);
|
|
280
|
-
await fsPromises.writeFile(localPath, new Uint8Array(downloaded.buffer));
|
|
281
|
-
localMediaPaths.push(localPath);
|
|
282
|
-
localMediaTypes.push(inferMimeType(downloaded.filename, downloaded.contentType));
|
|
283
|
-
const baseUrl = account.serverUrl.replace(/\/$/, "");
|
|
284
|
-
resolvedMediaUrls.push(rawUrl.startsWith("/") ? `${baseUrl}${rawUrl}` : rawUrl);
|
|
285
|
-
runtime.log?.(
|
|
286
|
-
`[media] Downloaded ${rawUrl} \u2192 ${localPath} (${downloaded.buffer.byteLength} bytes)`
|
|
287
|
-
);
|
|
288
|
-
} catch (err) {
|
|
289
|
-
runtime.error?.(`[media] Failed to download ${rawUrl}: ${String(err)}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
const mediaCtx = {};
|
|
294
|
-
if (localMediaPaths.length > 0) {
|
|
295
|
-
mediaCtx.MediaPath = localMediaPaths[0];
|
|
296
|
-
mediaCtx.MediaPaths = localMediaPaths;
|
|
297
|
-
mediaCtx.MediaUrl = resolvedMediaUrls[0];
|
|
298
|
-
mediaCtx.MediaUrls = resolvedMediaUrls;
|
|
299
|
-
mediaCtx.MediaType = localMediaTypes[0];
|
|
300
|
-
mediaCtx.MediaTypes = localMediaTypes;
|
|
301
|
-
}
|
|
302
|
-
let cleanBody = rawBody;
|
|
303
|
-
if (localMediaPaths.length > 0) {
|
|
304
|
-
cleanBody = rawBody.replace(/!?\[[^\]]*\]\([^)]*\/uploads\/[^)]+\)/g, "").replace(/\n{2,}/g, "\n").trim();
|
|
305
|
-
if (!cleanBody) cleanBody = "[Media attached]";
|
|
306
|
-
}
|
|
307
|
-
const serverInfo = channelServerMap.get(channelId);
|
|
308
|
-
const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
309
|
-
const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
|
|
310
|
-
const wasMentioned = mentionRegex.test(message.content);
|
|
311
|
-
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
312
|
-
Body: body,
|
|
313
|
-
BodyForAgent: cleanBody,
|
|
314
|
-
RawBody: rawBody,
|
|
315
|
-
CommandBody: cleanBody,
|
|
316
|
-
From: `shadowob:user:${senderId}`,
|
|
317
|
-
To: `shadowob:channel:${channelId}`,
|
|
318
|
-
SessionKey: route.sessionKey,
|
|
319
|
-
AccountId: route.accountId,
|
|
320
|
-
ChatType: chatType,
|
|
321
|
-
ConversationLabel: peerId,
|
|
322
|
-
SenderName: senderName,
|
|
323
|
-
SenderId: senderId,
|
|
324
|
-
SenderUsername: senderUsername,
|
|
325
|
-
Provider: "shadowob",
|
|
326
|
-
Surface: "shadowob",
|
|
327
|
-
MessageSid: message.id,
|
|
328
|
-
WasMentioned: wasMentioned,
|
|
329
|
-
OriginatingChannel: "shadowob",
|
|
330
|
-
OriginatingTo: `shadowob:channel:${channelId}`,
|
|
331
|
-
...serverInfo ? {
|
|
332
|
-
ServerId: serverInfo.serverId,
|
|
333
|
-
ServerSlug: serverInfo.serverSlug,
|
|
334
|
-
ServerName: serverInfo.serverName,
|
|
335
|
-
ChannelName: serverInfo.channelName
|
|
336
|
-
} : {},
|
|
337
|
-
BotUserId: botUserId,
|
|
338
|
-
BotUsername: botUsername,
|
|
339
|
-
AgentId: route.agentId,
|
|
340
|
-
ChannelId: channelId,
|
|
341
|
-
...message.threadId ? { ThreadId: message.threadId } : {},
|
|
342
|
-
...message.replyToId ? { ReplyToId: message.replyToId } : {},
|
|
343
|
-
...mediaCtx
|
|
344
|
-
});
|
|
345
|
-
const storePath = core.channel.session.resolveStorePath(
|
|
346
|
-
cfg.session,
|
|
347
|
-
{ agentId: route.agentId }
|
|
348
|
-
);
|
|
349
|
-
await core.channel.session.recordInboundSession({
|
|
350
|
-
storePath,
|
|
351
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
352
|
-
ctx: ctxPayload,
|
|
353
|
-
onRecordError: (err) => {
|
|
354
|
-
runtime.error?.(`Failed updating session meta: ${String(err)}`);
|
|
355
|
-
}
|
|
356
|
-
});
|
|
357
|
-
if (policy && !policy.reply) {
|
|
358
|
-
runtime.log?.(`[msg] Policy blocks reply for channel ${channelId}, skipping dispatch`);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
runtime.log?.(`[msg] Dispatching to AI pipeline for message ${message.id}`);
|
|
362
|
-
const client = new ShadowClient(account.serverUrl, account.token);
|
|
363
|
-
const triggerChain = message.metadata?.agentChain;
|
|
364
|
-
const typingCbs = createTypingCallbacks({
|
|
365
|
-
start: async () => {
|
|
366
|
-
socket.sendTyping(channelId);
|
|
367
|
-
},
|
|
368
|
-
onStartError: (err) => {
|
|
369
|
-
runtime.error?.(`[typing] Failed to send typing indicator: ${String(err)}`);
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
socket.updateActivity(channelId, "thinking");
|
|
373
|
-
typingCbs.onReplyStart().catch(() => {
|
|
374
|
-
});
|
|
375
|
-
try {
|
|
376
|
-
if (core.channel.reply.createReplyDispatcherWithTyping) {
|
|
377
|
-
const { markDispatchIdle, markRunComplete } = core.channel.reply.createReplyDispatcherWithTyping({
|
|
378
|
-
typingCallbacks: typingCbs,
|
|
379
|
-
deliver: async (payload) => {
|
|
380
|
-
socket.updateActivity(channelId, "working");
|
|
381
|
-
await deliverShadowReply({
|
|
382
|
-
payload,
|
|
383
|
-
channelId,
|
|
384
|
-
threadId: message.threadId ?? void 0,
|
|
385
|
-
replyToId: message.id,
|
|
386
|
-
client,
|
|
387
|
-
runtime,
|
|
388
|
-
agentChain: triggerChain,
|
|
389
|
-
agentId,
|
|
390
|
-
botUserId
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
});
|
|
394
|
-
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
395
|
-
ctx: ctxPayload,
|
|
396
|
-
cfg,
|
|
397
|
-
dispatcherOptions: {
|
|
398
|
-
deliver: async (payload) => {
|
|
399
|
-
socket.updateActivity(channelId, "working");
|
|
400
|
-
await deliverShadowReply({
|
|
401
|
-
payload,
|
|
402
|
-
channelId,
|
|
403
|
-
threadId: message.threadId ?? void 0,
|
|
404
|
-
replyToId: message.id,
|
|
405
|
-
client,
|
|
406
|
-
runtime,
|
|
407
|
-
agentChain: triggerChain,
|
|
408
|
-
agentId,
|
|
409
|
-
botUserId
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
});
|
|
414
|
-
markDispatchIdle();
|
|
415
|
-
markRunComplete();
|
|
416
|
-
} else {
|
|
417
|
-
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
418
|
-
ctx: ctxPayload,
|
|
419
|
-
cfg,
|
|
420
|
-
dispatcherOptions: {
|
|
421
|
-
deliver: async (payload) => {
|
|
422
|
-
socket.updateActivity(channelId, "working");
|
|
423
|
-
socket.sendTyping(channelId);
|
|
424
|
-
await deliverShadowReply({
|
|
425
|
-
payload,
|
|
426
|
-
channelId,
|
|
427
|
-
threadId: message.threadId ?? void 0,
|
|
428
|
-
replyToId: message.id,
|
|
429
|
-
client,
|
|
430
|
-
runtime,
|
|
431
|
-
agentChain: triggerChain,
|
|
432
|
-
agentId,
|
|
433
|
-
botUserId
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
socket.updateActivity(channelId, "ready");
|
|
440
|
-
} catch (err) {
|
|
441
|
-
runtime.error?.(`[msg] AI dispatch failed for message ${message.id}: ${String(err)}`);
|
|
442
|
-
socket.updateActivity(channelId, null);
|
|
443
|
-
throw err;
|
|
444
|
-
} finally {
|
|
445
|
-
typingCbs.onCleanup?.();
|
|
446
|
-
setTimeout(() => {
|
|
447
|
-
socket.updateActivity(channelId, null);
|
|
448
|
-
}, 3e3);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
async function deliverShadowReply(params) {
|
|
452
|
-
const {
|
|
453
|
-
payload,
|
|
454
|
-
channelId,
|
|
455
|
-
threadId,
|
|
456
|
-
replyToId,
|
|
457
|
-
client,
|
|
458
|
-
runtime,
|
|
459
|
-
agentChain,
|
|
460
|
-
agentId,
|
|
461
|
-
botUserId
|
|
462
|
-
} = params;
|
|
463
|
-
try {
|
|
464
|
-
if (!payload.text && !(payload.mediaUrl || payload.mediaUrls?.length)) {
|
|
465
|
-
runtime.error?.("[reply] No text or media in reply payload");
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
const text = payload.text ?? "";
|
|
469
|
-
runtime.log?.(`[reply] Sending reply to channel ${channelId}: "${text.slice(0, 80)}"`);
|
|
470
|
-
const mediaUrls = [payload.mediaUrl, ...payload.mediaUrls ?? []].filter(Boolean);
|
|
471
|
-
const newAgentChain = agentId ? {
|
|
472
|
-
agentId,
|
|
473
|
-
depth: (agentChain?.depth ?? 0) + 1,
|
|
474
|
-
participants: [...agentChain?.participants ?? [], botUserId].filter(
|
|
475
|
-
Boolean
|
|
476
|
-
),
|
|
477
|
-
startedAt: agentChain?.startedAt ?? Date.now(),
|
|
478
|
-
rootMessageId: agentChain?.rootMessageId ?? replyToId
|
|
479
|
-
} : void 0;
|
|
480
|
-
let sentMessage = null;
|
|
481
|
-
if (text || mediaUrls.length > 0) {
|
|
482
|
-
const contentToSend = text || "\u200B";
|
|
483
|
-
if (threadId) {
|
|
484
|
-
sentMessage = await client.sendToThread(threadId, contentToSend);
|
|
485
|
-
} else {
|
|
486
|
-
sentMessage = await client.sendMessage(channelId, contentToSend, {
|
|
487
|
-
replyToId,
|
|
488
|
-
metadata: newAgentChain ? { agentChain: newAgentChain } : void 0
|
|
489
|
-
});
|
|
490
|
-
}
|
|
491
|
-
runtime.log?.(
|
|
492
|
-
`[reply] Message created (${sentMessage.id})${text ? "" : " [media-only placeholder]"}${newAgentChain ? ` [chain depth: ${newAgentChain.depth}]` : ""}`
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
if (mediaUrls.length > 0) {
|
|
496
|
-
const messageId = sentMessage?.id;
|
|
497
|
-
for (const mediaUrl of mediaUrls) {
|
|
498
|
-
try {
|
|
499
|
-
runtime.log?.(`[reply] Uploading media: ${mediaUrl}`);
|
|
500
|
-
await client.uploadMediaFromUrl(mediaUrl, messageId);
|
|
501
|
-
runtime.log?.(`[reply] Media uploaded successfully`);
|
|
502
|
-
} catch (err) {
|
|
503
|
-
runtime.error?.(`[reply] Failed to upload media ${mediaUrl}: ${String(err)}`);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
runtime.log?.(`[reply] Reply delivered successfully`);
|
|
508
|
-
} catch (err) {
|
|
509
|
-
runtime.error?.(`[reply] Failed to send reply: ${String(err)}`);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
async function processShadowDmMessage(params) {
|
|
513
|
-
const {
|
|
514
|
-
dmMessage,
|
|
515
|
-
account,
|
|
516
|
-
accountId,
|
|
517
|
-
config,
|
|
518
|
-
runtime,
|
|
519
|
-
core,
|
|
520
|
-
botUserId,
|
|
521
|
-
botUsername,
|
|
522
|
-
shadowAgentId,
|
|
523
|
-
socket
|
|
524
|
-
} = params;
|
|
525
|
-
const cfg = config;
|
|
526
|
-
const senderLabel = dmMessage.author?.username ?? dmMessage.senderId;
|
|
527
|
-
if (dmMessage.senderId === botUserId || dmMessage.authorId === botUserId) {
|
|
528
|
-
runtime.log?.(`[dm] Skipping own DM message ${dmMessage.id}`);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
if (dmMessage.author?.isBot) {
|
|
532
|
-
runtime.log?.(`[dm] Skipping bot DM from ${senderLabel} (${dmMessage.id})`);
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
runtime.log?.(
|
|
536
|
-
`[dm] Processing DM from ${senderLabel}: "${dmMessage.content.slice(0, 80)}" (${dmMessage.id})`
|
|
537
|
-
);
|
|
538
|
-
const senderName = dmMessage.author?.displayName ?? dmMessage.author?.username ?? "Unknown";
|
|
539
|
-
const senderUsername = dmMessage.author?.username ?? "";
|
|
540
|
-
const senderId = dmMessage.senderId;
|
|
541
|
-
const rawBody = dmMessage.content;
|
|
542
|
-
const dmChannelId = dmMessage.dmChannelId;
|
|
543
|
-
const attachments = dmMessage.attachments ?? [];
|
|
544
|
-
let bodyWithAttachments = rawBody;
|
|
545
|
-
if (attachments.length > 0) {
|
|
546
|
-
const attachmentLines = attachments.map(
|
|
547
|
-
(a) => `[Attachment: ${a.filename} (${a.contentType}): ${a.url}]`
|
|
548
|
-
);
|
|
549
|
-
bodyWithAttachments = rawBody ? `${rawBody}
|
|
550
|
-
${attachmentLines.join("\n")}` : attachmentLines.join("\n");
|
|
551
|
-
}
|
|
552
|
-
const peerId = `dm:${dmChannelId}`;
|
|
553
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
554
|
-
cfg,
|
|
555
|
-
channel: "shadowob",
|
|
556
|
-
accountId,
|
|
557
|
-
peer: { kind: "private", id: peerId }
|
|
558
|
-
});
|
|
559
|
-
runtime.log?.(`[routing] DM resolved agent: ${route.agentId} (account ${accountId})`);
|
|
560
|
-
const body = core.channel.reply.formatAgentEnvelope({
|
|
561
|
-
channel: "Shadow DM",
|
|
562
|
-
from: senderName,
|
|
563
|
-
timestamp: new Date(dmMessage.createdAt).getTime(),
|
|
564
|
-
envelope: core.channel.reply.resolveEnvelopeFormatOptions(cfg),
|
|
565
|
-
body: bodyWithAttachments
|
|
566
|
-
});
|
|
567
|
-
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
568
|
-
Body: body,
|
|
569
|
-
BodyForAgent: bodyWithAttachments,
|
|
570
|
-
RawBody: rawBody,
|
|
571
|
-
CommandBody: rawBody,
|
|
572
|
-
From: `shadowob:user:${senderId}`,
|
|
573
|
-
To: `shadowob:dm:${dmChannelId}`,
|
|
574
|
-
SessionKey: route.sessionKey,
|
|
575
|
-
AccountId: route.accountId,
|
|
576
|
-
ChatType: "dm",
|
|
577
|
-
ConversationLabel: peerId,
|
|
578
|
-
SenderName: senderName,
|
|
579
|
-
SenderId: senderId,
|
|
580
|
-
SenderUsername: senderUsername,
|
|
581
|
-
Provider: "shadowob",
|
|
582
|
-
Surface: "shadowob",
|
|
583
|
-
MessageSid: dmMessage.id,
|
|
584
|
-
WasMentioned: true,
|
|
585
|
-
OriginatingChannel: "shadowob",
|
|
586
|
-
OriginatingTo: `shadowob:dm:${dmChannelId}`,
|
|
587
|
-
BotUserId: botUserId,
|
|
588
|
-
BotUsername: botUsername,
|
|
589
|
-
AgentId: route.agentId,
|
|
590
|
-
ChannelId: dmChannelId
|
|
591
|
-
});
|
|
592
|
-
const storePath = core.channel.session.resolveStorePath(
|
|
593
|
-
cfg.session,
|
|
594
|
-
{ agentId: route.agentId }
|
|
595
|
-
);
|
|
596
|
-
await core.channel.session.recordInboundSession({
|
|
597
|
-
storePath,
|
|
598
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
599
|
-
ctx: ctxPayload,
|
|
600
|
-
onRecordError: (err) => {
|
|
601
|
-
runtime.error?.(`Failed updating DM session meta: ${String(err)}`);
|
|
602
|
-
}
|
|
603
|
-
});
|
|
604
|
-
runtime.log?.(`[dm] Dispatching to AI pipeline for DM message ${dmMessage.id}`);
|
|
605
|
-
const client = new ShadowClient(account.serverUrl, account.token);
|
|
606
|
-
const triggerChain = dmMessage.metadata?.agentChain;
|
|
607
|
-
const typingCbs = createTypingCallbacks({
|
|
608
|
-
start: async () => {
|
|
609
|
-
socket.sendDmTyping(dmChannelId);
|
|
610
|
-
},
|
|
611
|
-
onStartError: (err) => {
|
|
612
|
-
runtime.error?.(`[dm-typing] Failed to send typing indicator: ${String(err)}`);
|
|
613
|
-
}
|
|
614
|
-
});
|
|
615
|
-
typingCbs.onReplyStart().catch(() => {
|
|
616
|
-
});
|
|
617
|
-
try {
|
|
618
|
-
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
619
|
-
ctx: ctxPayload,
|
|
620
|
-
cfg,
|
|
621
|
-
dispatcherOptions: {
|
|
622
|
-
deliver: async (payload) => {
|
|
623
|
-
socket.sendDmTyping(dmChannelId);
|
|
624
|
-
await deliverShadowDmReply({
|
|
625
|
-
payload,
|
|
626
|
-
dmChannelId,
|
|
627
|
-
replyToId: dmMessage.id,
|
|
628
|
-
client,
|
|
629
|
-
runtime,
|
|
630
|
-
agentChain: triggerChain,
|
|
631
|
-
agentId: shadowAgentId,
|
|
632
|
-
botUserId
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
} catch (err) {
|
|
638
|
-
runtime.error?.(`[dm] AI dispatch failed for DM message ${dmMessage.id}: ${String(err)}`);
|
|
639
|
-
throw err;
|
|
640
|
-
} finally {
|
|
641
|
-
typingCbs.onCleanup?.();
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
async function deliverShadowDmReply(params) {
|
|
645
|
-
const { payload, dmChannelId, replyToId, client, runtime, agentChain, agentId, botUserId } = params;
|
|
646
|
-
try {
|
|
647
|
-
if (!payload.text && !(payload.mediaUrl || payload.mediaUrls?.length)) {
|
|
648
|
-
runtime.error?.("[dm-reply] No text or media in DM reply payload");
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
const text = payload.text ?? "";
|
|
652
|
-
runtime.log?.(`[dm-reply] Sending DM reply to channel ${dmChannelId}: "${text.slice(0, 80)}"`);
|
|
653
|
-
const mediaUrls = [payload.mediaUrl, ...payload.mediaUrls ?? []].filter(Boolean);
|
|
654
|
-
const newAgentChain = agentId ? {
|
|
655
|
-
agentId,
|
|
656
|
-
depth: (agentChain?.depth ?? 0) + 1,
|
|
657
|
-
participants: [...agentChain?.participants ?? [], botUserId].filter(
|
|
658
|
-
Boolean
|
|
659
|
-
),
|
|
660
|
-
startedAt: agentChain?.startedAt ?? Date.now(),
|
|
661
|
-
rootMessageId: agentChain?.rootMessageId ?? replyToId
|
|
662
|
-
} : void 0;
|
|
663
|
-
let sentMessage = null;
|
|
664
|
-
if (text || mediaUrls.length > 0) {
|
|
665
|
-
const contentToSend = text || "\u200B";
|
|
666
|
-
sentMessage = await client.sendDmMessage(dmChannelId, contentToSend, {
|
|
667
|
-
replyToId,
|
|
668
|
-
metadata: newAgentChain ? { agentChain: newAgentChain } : void 0
|
|
669
|
-
});
|
|
670
|
-
runtime.log?.(
|
|
671
|
-
`[dm-reply] DM message created (${sentMessage.id})${text ? "" : " [media-only placeholder]"}${newAgentChain ? ` [chain depth: ${newAgentChain.depth}]` : ""}`
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
if (mediaUrls.length > 0) {
|
|
675
|
-
const messageId = sentMessage?.id;
|
|
676
|
-
for (const mediaUrl of mediaUrls) {
|
|
677
|
-
try {
|
|
678
|
-
runtime.log?.(`[dm-reply] Uploading media: ${mediaUrl}`);
|
|
679
|
-
await client.uploadMediaFromUrl(mediaUrl, messageId);
|
|
680
|
-
runtime.log?.(`[dm-reply] Media uploaded successfully`);
|
|
681
|
-
} catch (err) {
|
|
682
|
-
runtime.error?.(`[dm-reply] Failed to upload media ${mediaUrl}: ${String(err)}`);
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
runtime.log?.(`[dm-reply] DM reply delivered successfully`);
|
|
687
|
-
} catch (err) {
|
|
688
|
-
runtime.error?.(`[dm-reply] Failed to send DM reply: ${String(err)}`);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
async function getSessionCachePath(accountId) {
|
|
692
|
-
const nodePath = await import("path");
|
|
693
|
-
const dataDir = await getDataDir();
|
|
694
|
-
return nodePath.join(dataDir, "shadow", `session-cache-${accountId}.json`);
|
|
695
|
-
}
|
|
696
|
-
async function saveSessionCache(accountId, data) {
|
|
697
|
-
try {
|
|
698
|
-
const fsPromises = await import("fs/promises");
|
|
699
|
-
const nodePath = await import("path");
|
|
700
|
-
const cachePath = await getSessionCachePath(accountId);
|
|
701
|
-
await fsPromises.mkdir(nodePath.dirname(cachePath), { recursive: true });
|
|
702
|
-
await fsPromises.writeFile(cachePath, JSON.stringify(data), "utf-8");
|
|
703
|
-
} catch {
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
async function loadSessionCache(accountId) {
|
|
707
|
-
try {
|
|
708
|
-
const fsPromises = await import("fs/promises");
|
|
709
|
-
const cachePath = await getSessionCachePath(accountId);
|
|
710
|
-
const raw = await fsPromises.readFile(cachePath, "utf-8");
|
|
711
|
-
return JSON.parse(raw);
|
|
712
|
-
} catch {
|
|
713
|
-
return null;
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
async function monitorShadowProvider(options) {
|
|
717
|
-
const { account, accountId, config, runtime, abortSignal } = options;
|
|
718
|
-
const core = getShadowRuntime();
|
|
719
|
-
let stopped = false;
|
|
720
|
-
const client = new ShadowClient(account.serverUrl, account.token);
|
|
721
|
-
const me = await client.getMe();
|
|
722
|
-
const botUserId = me.id;
|
|
723
|
-
runtime.log?.(`Shadow bot connected as ${me.username} (${botUserId})`);
|
|
724
|
-
const agentId = account.agentId ?? me.agentId ?? null;
|
|
725
|
-
if (!agentId) {
|
|
726
|
-
runtime.error?.(
|
|
727
|
-
"[config] Cannot resolve agentId \u2014 heartbeat and remote config will be unavailable"
|
|
728
|
-
);
|
|
729
|
-
} else {
|
|
730
|
-
runtime.log?.(`[config] Resolved agentId: ${agentId}`);
|
|
731
|
-
}
|
|
732
|
-
let remoteConfig = null;
|
|
733
|
-
const channelPolicies = /* @__PURE__ */ new Map();
|
|
734
|
-
const channelServerMap = /* @__PURE__ */ new Map();
|
|
735
|
-
const allChannelIds = [];
|
|
736
|
-
if (agentId) {
|
|
737
|
-
try {
|
|
738
|
-
remoteConfig = await client.getAgentConfig(agentId);
|
|
739
|
-
runtime.log?.(`[config] Fetched remote config: ${remoteConfig.servers.length} server(s)`);
|
|
740
|
-
for (const server of remoteConfig.servers) {
|
|
741
|
-
runtime.log?.(
|
|
742
|
-
`[config] Server "${server.name}" (${server.id}) \u2014 ${server.channels.length} channel(s)`
|
|
743
|
-
);
|
|
744
|
-
for (const ch of server.channels) {
|
|
745
|
-
channelPolicies.set(ch.id, ch.policy);
|
|
746
|
-
channelServerMap.set(ch.id, {
|
|
747
|
-
serverId: server.id,
|
|
748
|
-
serverSlug: server.slug ?? server.id,
|
|
749
|
-
serverName: server.name,
|
|
750
|
-
channelName: ch.name
|
|
751
|
-
});
|
|
752
|
-
if (ch.policy.listen) {
|
|
753
|
-
allChannelIds.push(ch.id);
|
|
754
|
-
runtime.log?.(
|
|
755
|
-
`[config] \u2713 #${ch.name} (${ch.id}) \u2014 listen=true reply=${ch.policy.reply} mentionOnly=${ch.policy.mentionOnly}`
|
|
756
|
-
);
|
|
757
|
-
} else {
|
|
758
|
-
runtime.log?.(`[config] \u2717 #${ch.name} (${ch.id}) \u2014 listen=false, skipping`);
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
runtime.log?.(
|
|
763
|
-
`[config] Monitoring ${allChannelIds.length} channel(s) across ${remoteConfig.servers.length} server(s)`
|
|
764
|
-
);
|
|
765
|
-
void saveSessionCache(accountId, { remoteConfig, botUserId, botUsername: me.username });
|
|
766
|
-
} catch (err) {
|
|
767
|
-
runtime.error?.(`[config] Failed to fetch remote config: ${String(err)}`);
|
|
768
|
-
const cached = await loadSessionCache(accountId);
|
|
769
|
-
if (cached) {
|
|
770
|
-
runtime.log?.("[config] Loaded session from cache \u2014 using cached config");
|
|
771
|
-
remoteConfig = cached.remoteConfig;
|
|
772
|
-
for (const server of remoteConfig.servers) {
|
|
773
|
-
for (const ch of server.channels) {
|
|
774
|
-
channelPolicies.set(ch.id, ch.policy);
|
|
775
|
-
channelServerMap.set(ch.id, {
|
|
776
|
-
serverId: server.id,
|
|
777
|
-
serverSlug: server.slug ?? server.id,
|
|
778
|
-
serverName: server.name,
|
|
779
|
-
channelName: ch.name
|
|
780
|
-
});
|
|
781
|
-
if (ch.policy.listen) allChannelIds.push(ch.id);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
runtime.log?.(`[config] Restored ${allChannelIds.length} channel(s) from cache`);
|
|
785
|
-
} else {
|
|
786
|
-
runtime.log?.("[config] No cached session \u2014 falling back to monitoring no channels");
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
let heartbeatInterval = null;
|
|
791
|
-
if (agentId) {
|
|
792
|
-
const sendHeartbeat = async () => {
|
|
793
|
-
try {
|
|
794
|
-
await client.sendHeartbeat(agentId);
|
|
795
|
-
runtime.log?.("[heartbeat] Heartbeat sent");
|
|
796
|
-
} catch (err) {
|
|
797
|
-
runtime.error?.(`[heartbeat] Heartbeat failed: ${String(err)}`);
|
|
798
|
-
}
|
|
799
|
-
};
|
|
800
|
-
void sendHeartbeat();
|
|
801
|
-
heartbeatInterval = setInterval(sendHeartbeat, 3e4);
|
|
802
|
-
}
|
|
803
|
-
runtime.log?.(`[ws] Connecting to Shadow WebSocket at ${account.serverUrl}`);
|
|
804
|
-
const socket = new ShadowSocket({
|
|
805
|
-
serverUrl: account.serverUrl,
|
|
806
|
-
token: account.token,
|
|
807
|
-
transports: ["websocket", "polling"]
|
|
808
|
-
});
|
|
809
|
-
socket.onConnect(() => {
|
|
810
|
-
runtime.log?.(`[ws] Connected (sid=${socket.raw.id})`);
|
|
811
|
-
if (allChannelIds.length === 0) {
|
|
812
|
-
runtime.log?.("[ws] No channels to join \u2014 allChannelIds is empty");
|
|
813
|
-
}
|
|
814
|
-
for (const chId of allChannelIds) {
|
|
815
|
-
runtime.log?.(`[ws] Emitting channel:join for ${chId}`);
|
|
816
|
-
socket.joinChannel(chId).then((ack) => {
|
|
817
|
-
if (ack?.ok) runtime.log?.(`[ws] \u2713 Joined channel room ${chId} (server confirmed)`);
|
|
818
|
-
else runtime.log?.(`[ws] channel:join for ${chId} \u2014 no ack received (older server?)`);
|
|
819
|
-
});
|
|
820
|
-
}
|
|
821
|
-
runtime.log?.(
|
|
822
|
-
`[ws] Emitted channel:join for ${allChannelIds.length} channel(s), listening for messages`
|
|
823
|
-
);
|
|
824
|
-
(async () => {
|
|
825
|
-
try {
|
|
826
|
-
const dmChannels = await client.listDmChannels();
|
|
827
|
-
for (const ch of dmChannels) {
|
|
828
|
-
socket.joinDmChannel(ch.id);
|
|
829
|
-
runtime.log?.(`[ws] Joined DM room dm:${ch.id}`);
|
|
830
|
-
}
|
|
831
|
-
runtime.log?.(`[ws] Joined ${dmChannels.length} DM channel room(s)`);
|
|
832
|
-
} catch (err) {
|
|
833
|
-
runtime.error?.(`[ws] Failed to join DM rooms: ${String(err)}`);
|
|
834
|
-
}
|
|
835
|
-
})();
|
|
836
|
-
});
|
|
837
|
-
socket.onConnectError((err) => {
|
|
838
|
-
runtime.error?.(`[ws] Connection error: ${err.message}`);
|
|
839
|
-
});
|
|
840
|
-
socket.onDisconnect((reason) => {
|
|
841
|
-
runtime.log?.(`[ws] Disconnected: ${reason}`);
|
|
842
|
-
});
|
|
843
|
-
socket.raw.io.on("reconnect", (attempt) => {
|
|
844
|
-
runtime.log?.(`[ws] Reconnected after ${attempt} attempt(s)`);
|
|
845
|
-
});
|
|
846
|
-
socket.raw.io.on("reconnect_attempt", (attempt) => {
|
|
847
|
-
runtime.log?.(`[ws] Reconnect attempt #${attempt}`);
|
|
848
|
-
});
|
|
849
|
-
socket.on("server:joined", async (data) => {
|
|
850
|
-
if (!agentId) return;
|
|
851
|
-
runtime.log?.(`[ws] Received server:joined for server ${data.serverId} \u2014 refreshing channels`);
|
|
852
|
-
try {
|
|
853
|
-
const updatedConfig = await client.getAgentConfig(agentId);
|
|
854
|
-
runtime.log?.(`[config] Refreshed config: ${updatedConfig.servers.length} server(s)`);
|
|
855
|
-
for (const server of updatedConfig.servers) {
|
|
856
|
-
for (const ch of server.channels) {
|
|
857
|
-
channelServerMap.set(ch.id, {
|
|
858
|
-
serverId: server.id,
|
|
859
|
-
serverSlug: server.slug ?? server.id,
|
|
860
|
-
serverName: server.name,
|
|
861
|
-
channelName: ch.name
|
|
862
|
-
});
|
|
863
|
-
if (!channelPolicies.has(ch.id)) {
|
|
864
|
-
channelPolicies.set(ch.id, ch.policy);
|
|
865
|
-
if (ch.policy.listen) {
|
|
866
|
-
allChannelIds.push(ch.id);
|
|
867
|
-
runtime.log?.(`[config] New channel: #${ch.name} (${ch.id}) \u2014 joining`);
|
|
868
|
-
socket.joinChannel(ch.id).then((ack) => {
|
|
869
|
-
if (ack?.ok) runtime.log?.(`[ws] \u2713 Joined new channel room ${ch.id}`);
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
} else {
|
|
873
|
-
channelPolicies.set(ch.id, ch.policy);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
remoteConfig = updatedConfig;
|
|
878
|
-
} catch (err) {
|
|
879
|
-
runtime.error?.(`[config] Failed to refresh config after server:joined: ${String(err)}`);
|
|
880
|
-
}
|
|
881
|
-
});
|
|
882
|
-
socket.on(
|
|
883
|
-
"channel:created",
|
|
884
|
-
async (data) => {
|
|
885
|
-
runtime.log?.(
|
|
886
|
-
`[ws] Received channel:created: #${data.name} (${data.id}) in server ${data.serverId} \u2014 ignoring (bot must be explicitly added)`
|
|
887
|
-
);
|
|
888
|
-
}
|
|
889
|
-
);
|
|
890
|
-
socket.on(
|
|
891
|
-
"agent:policy-changed",
|
|
892
|
-
(data) => {
|
|
893
|
-
if (data.agentId !== agentId) return;
|
|
894
|
-
if (!data.channelId) return;
|
|
895
|
-
const mentionOnly = data.mentionOnly ?? false;
|
|
896
|
-
runtime.log?.(
|
|
897
|
-
`[ws] Received agent:policy-changed for channel ${data.channelId}: mentionOnly=${mentionOnly}, reply=${data.reply}, config=${JSON.stringify(data.config ?? {})}`
|
|
898
|
-
);
|
|
899
|
-
const existing = channelPolicies.get(data.channelId);
|
|
900
|
-
if (existing) {
|
|
901
|
-
channelPolicies.set(data.channelId, {
|
|
902
|
-
...existing,
|
|
903
|
-
mentionOnly,
|
|
904
|
-
reply: data.reply ?? existing.reply,
|
|
905
|
-
config: data.config ?? existing.config
|
|
906
|
-
});
|
|
907
|
-
} else {
|
|
908
|
-
channelPolicies.set(data.channelId, {
|
|
909
|
-
listen: true,
|
|
910
|
-
reply: data.reply ?? true,
|
|
911
|
-
mentionOnly,
|
|
912
|
-
config: data.config ?? {}
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
);
|
|
917
|
-
socket.on("channel:member-added", (data) => {
|
|
918
|
-
runtime.log?.(
|
|
919
|
-
`[ws] Received channel:member-added: channel ${data.channelId} in server ${data.serverId}`
|
|
920
|
-
);
|
|
921
|
-
if (!channelPolicies.has(data.channelId)) {
|
|
922
|
-
const defaultPolicy = {
|
|
923
|
-
listen: true,
|
|
924
|
-
reply: true,
|
|
925
|
-
mentionOnly: false,
|
|
926
|
-
config: {}
|
|
927
|
-
};
|
|
928
|
-
channelPolicies.set(data.channelId, defaultPolicy);
|
|
929
|
-
allChannelIds.push(data.channelId);
|
|
930
|
-
}
|
|
931
|
-
socket.joinChannel(data.channelId).then((ack) => {
|
|
932
|
-
if (ack?.ok) runtime.log?.(`[ws] \u2713 Joined channel room ${data.channelId} after member-added`);
|
|
933
|
-
});
|
|
934
|
-
});
|
|
935
|
-
socket.on("channel:member-removed", (data) => {
|
|
936
|
-
runtime.log?.(
|
|
937
|
-
`[ws] Received channel:member-removed: channel ${data.channelId} in server ${data.serverId}`
|
|
938
|
-
);
|
|
939
|
-
channelPolicies.delete(data.channelId);
|
|
940
|
-
const idx = allChannelIds.indexOf(data.channelId);
|
|
941
|
-
if (idx !== -1) allChannelIds.splice(idx, 1);
|
|
942
|
-
socket.leaveChannel(data.channelId);
|
|
943
|
-
runtime.log?.(`[ws] Left channel room ${data.channelId} after member-removed`);
|
|
944
|
-
});
|
|
945
|
-
const processedDmIds = /* @__PURE__ */ new Set();
|
|
946
|
-
socket.on(
|
|
947
|
-
"dm:message:new",
|
|
948
|
-
(dmMessage) => {
|
|
949
|
-
if (processedDmIds.has(dmMessage.id)) {
|
|
950
|
-
runtime.log?.(`[ws] Skipping duplicate dm:message:new ${dmMessage.id}`);
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
processedDmIds.add(dmMessage.id);
|
|
954
|
-
if (processedDmIds.size > 500) {
|
|
955
|
-
const first = processedDmIds.values().next().value;
|
|
956
|
-
if (first) processedDmIds.delete(first);
|
|
957
|
-
}
|
|
958
|
-
const senderLabel = dmMessage.author?.username ?? dmMessage.senderId;
|
|
959
|
-
runtime.log?.(
|
|
960
|
-
`[ws] \u2190 dm:message:new from ${senderLabel} in DM ${dmMessage.dmChannelId}: "${dmMessage.content?.slice(0, 60)}" (${dmMessage.id})`
|
|
961
|
-
);
|
|
962
|
-
if (stopped) {
|
|
963
|
-
runtime.log?.("[ws] Monitor stopped, ignoring DM message");
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
const processWithRetry = async (attempt = 0) => {
|
|
967
|
-
try {
|
|
968
|
-
await processShadowDmMessage({
|
|
969
|
-
dmMessage,
|
|
970
|
-
account,
|
|
971
|
-
accountId,
|
|
972
|
-
config,
|
|
973
|
-
runtime,
|
|
974
|
-
core,
|
|
975
|
-
botUserId,
|
|
976
|
-
botUsername: me.username,
|
|
977
|
-
shadowAgentId: agentId,
|
|
978
|
-
socket
|
|
979
|
-
});
|
|
980
|
-
} catch (err) {
|
|
981
|
-
const MAX_RETRIES = 2;
|
|
982
|
-
runtime.error?.(`[ws] DM processing failed (attempt ${attempt + 1}): ${String(err)}`);
|
|
983
|
-
if (attempt < MAX_RETRIES) {
|
|
984
|
-
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
985
|
-
return processWithRetry(attempt + 1);
|
|
986
|
-
}
|
|
987
|
-
runtime.error?.(
|
|
988
|
-
`[ws] DM permanently failed after ${MAX_RETRIES + 1} attempts: ${dmMessage.id}`
|
|
989
|
-
);
|
|
990
|
-
}
|
|
991
|
-
};
|
|
992
|
-
void processWithRetry();
|
|
993
|
-
}
|
|
994
|
-
);
|
|
995
|
-
socket.on("message:new", (message) => {
|
|
996
|
-
const senderLabel = message.author?.username ?? message.authorId;
|
|
997
|
-
runtime.log?.(
|
|
998
|
-
`[ws] \u2190 message:new from ${senderLabel} in channel ${message.channelId}: "${message.content?.slice(0, 60)}" (${message.id})`
|
|
999
|
-
);
|
|
1000
|
-
if (stopped) {
|
|
1001
|
-
runtime.log?.("[ws] Monitor stopped, ignoring message");
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
if (allChannelIds.length > 0 && !allChannelIds.includes(message.channelId)) {
|
|
1005
|
-
runtime.log?.(`[ws] Message from unmonitored channel ${message.channelId}, ignoring`);
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
const processWithRetry = async (attempt = 0) => {
|
|
1009
|
-
try {
|
|
1010
|
-
await processShadowMessage({
|
|
1011
|
-
message,
|
|
1012
|
-
account,
|
|
1013
|
-
accountId,
|
|
1014
|
-
config,
|
|
1015
|
-
runtime,
|
|
1016
|
-
core,
|
|
1017
|
-
botUserId,
|
|
1018
|
-
botUsername: me.username,
|
|
1019
|
-
agentId,
|
|
1020
|
-
channelPolicies,
|
|
1021
|
-
channelServerMap,
|
|
1022
|
-
socket
|
|
1023
|
-
});
|
|
1024
|
-
} catch (err) {
|
|
1025
|
-
const MAX_RETRIES = 2;
|
|
1026
|
-
runtime.error?.(`[ws] Message processing failed (attempt ${attempt + 1}): ${String(err)}`);
|
|
1027
|
-
if (attempt < MAX_RETRIES) {
|
|
1028
|
-
await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
|
|
1029
|
-
return processWithRetry(attempt + 1);
|
|
1030
|
-
}
|
|
1031
|
-
runtime.error?.(
|
|
1032
|
-
`[ws] Message permanently failed after ${MAX_RETRIES + 1} attempts: ${message.id}`
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
};
|
|
1036
|
-
void processWithRetry();
|
|
1037
|
-
});
|
|
1038
|
-
socket.connect();
|
|
1039
|
-
const stop = () => {
|
|
1040
|
-
runtime.log?.("[lifecycle] Stopping Shadow monitor...");
|
|
1041
|
-
stopped = true;
|
|
1042
|
-
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
1043
|
-
socket.disconnect();
|
|
1044
|
-
runtime.log?.("[lifecycle] Shadow monitor stopped");
|
|
1045
|
-
};
|
|
1046
|
-
abortSignal.addEventListener("abort", stop, { once: true });
|
|
1047
|
-
await new Promise((resolve) => {
|
|
1048
|
-
if (abortSignal.aborted) {
|
|
1049
|
-
resolve();
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
abortSignal.addEventListener("abort", () => resolve(), { once: true });
|
|
1053
|
-
});
|
|
1054
|
-
return { stop };
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
export {
|
|
1058
|
-
setShadowRuntime,
|
|
1059
|
-
getShadowRuntime,
|
|
1060
|
-
tryGetShadowRuntime,
|
|
1061
|
-
monitorShadowProvider
|
|
1062
|
-
};
|