@shadowob/openclaw-shadowob 1.1.7 → 1.1.8
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.
|
@@ -216,10 +216,13 @@ async function getDataDir() {
|
|
|
216
216
|
// src/monitor/media.ts
|
|
217
217
|
function isShadowResolvableMediaUrl(url) {
|
|
218
218
|
if (url.startsWith("/")) {
|
|
219
|
-
return url.includes("/uploads/") || url.startsWith("/api/media/signed/");
|
|
219
|
+
return url.includes("/uploads/") || url.includes("/voice/") || url.startsWith("/api/media/signed/");
|
|
220
220
|
}
|
|
221
221
|
return url.startsWith("http");
|
|
222
222
|
}
|
|
223
|
+
function visibleMessageText(text) {
|
|
224
|
+
return text.replace(/\u200B/gu, "").trim();
|
|
225
|
+
}
|
|
223
226
|
function inferMimeType(filename, headerType) {
|
|
224
227
|
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
225
228
|
const map = {
|
|
@@ -240,7 +243,12 @@ function inferMimeType(filename, headerType) {
|
|
|
240
243
|
}
|
|
241
244
|
async function resolveShadowInboundMediaContext(params) {
|
|
242
245
|
const { account, message, rawBody, runtime } = params;
|
|
243
|
-
const
|
|
246
|
+
const attachments = message.attachments ?? [];
|
|
247
|
+
const attachmentUrls = attachments.map((a) => a.url).filter(Boolean);
|
|
248
|
+
const voiceAttachment = attachments.find(
|
|
249
|
+
(attachment) => attachment.kind === "voice" || String(attachment.contentType ?? "").startsWith("audio/")
|
|
250
|
+
);
|
|
251
|
+
const voiceTranscript = voiceAttachment?.transcript?.status === "ready" && voiceAttachment.transcript.text?.trim() ? voiceAttachment.transcript.text.trim() : "";
|
|
244
252
|
const markdownMediaRegex = /!?\[[^\]]*\]\(([^)]+)\)/g;
|
|
245
253
|
const markdownUrls = [];
|
|
246
254
|
for (const mdMatch of rawBody.matchAll(markdownMediaRegex)) {
|
|
@@ -280,10 +288,22 @@ async function resolveShadowInboundMediaContext(params) {
|
|
|
280
288
|
runtime.error?.(`[media] Failed to download ${rawUrl}: ${String(err)}`);
|
|
281
289
|
}
|
|
282
290
|
}
|
|
291
|
+
const bodyWithoutMedia = rawBody.replace(
|
|
292
|
+
/!?\[[^\]]*\]\((?:[^)]*\/uploads\/[^)]+|[^)]*\/voice\/[^)]+|[^)]*\/api\/media\/signed\/[^)]+)\)/g,
|
|
293
|
+
""
|
|
294
|
+
).replace(/\u200B/gu, "").replace(/\n{2,}/g, "\n").trim();
|
|
295
|
+
const cleanBody = bodyWithoutMedia || (voiceTranscript && !visibleMessageText(rawBody) ? voiceTranscript : "") || voiceTranscript || "[Media attached]";
|
|
296
|
+
const voiceFields = voiceAttachment ? {
|
|
297
|
+
VoiceMessage: true,
|
|
298
|
+
VoiceAttachmentId: voiceAttachment.id,
|
|
299
|
+
VoiceDurationMs: voiceAttachment.durationMs ?? null,
|
|
300
|
+
VoiceWaveformPeaks: voiceAttachment.waveformPeaks ?? null,
|
|
301
|
+
VoiceTranscript: voiceAttachment.transcript?.text ?? null,
|
|
302
|
+
VoiceTranscriptStatus: voiceAttachment.transcript?.status ?? null
|
|
303
|
+
} : {};
|
|
283
304
|
if (localMediaPaths.length === 0) {
|
|
284
|
-
return { cleanBody
|
|
305
|
+
return { cleanBody, fields: voiceFields };
|
|
285
306
|
}
|
|
286
|
-
const cleanBody = rawBody.replace(/!?\[[^\]]*\]\((?:[^)]*\/uploads\/[^)]+|[^)]*\/api\/media\/signed\/[^)]+)\)/g, "").replace(/\n{2,}/g, "\n").trim() || "[Media attached]";
|
|
287
307
|
return {
|
|
288
308
|
cleanBody,
|
|
289
309
|
fields: {
|
|
@@ -292,7 +312,8 @@ async function resolveShadowInboundMediaContext(params) {
|
|
|
292
312
|
MediaUrl: resolvedMediaUrls[0],
|
|
293
313
|
MediaUrls: resolvedMediaUrls,
|
|
294
314
|
MediaType: localMediaTypes[0],
|
|
295
|
-
MediaTypes: localMediaTypes
|
|
315
|
+
MediaTypes: localMediaTypes,
|
|
316
|
+
...voiceFields
|
|
296
317
|
}
|
|
297
318
|
};
|
|
298
319
|
}
|
|
@@ -303,6 +324,20 @@ function normalizeTriggerUserIds(policyConfig) {
|
|
|
303
324
|
if (!Array.isArray(value)) return null;
|
|
304
325
|
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
305
326
|
}
|
|
327
|
+
function messageHasActiveTaskForBot(message, botUserId) {
|
|
328
|
+
const cards = message.metadata?.cards;
|
|
329
|
+
if (!Array.isArray(cards)) return false;
|
|
330
|
+
return cards.some((card) => {
|
|
331
|
+
if (!card || typeof card !== "object" || Array.isArray(card)) return false;
|
|
332
|
+
const record = card;
|
|
333
|
+
if (record.kind !== "task") return false;
|
|
334
|
+
if (record.status === "completed" || record.status === "failed" || record.status === "canceled" || record.status === "transferred") {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
const assignee = record.assignee && typeof record.assignee === "object" && !Array.isArray(record.assignee) ? record.assignee : null;
|
|
338
|
+
return assignee?.userId === botUserId;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
306
341
|
function evaluateShadowMessagePreflight(params) {
|
|
307
342
|
const { message, botUserId, botUsername, channelPolicies, runtime } = params;
|
|
308
343
|
const senderLabel = message.author?.username ?? message.authorId;
|
|
@@ -375,7 +410,7 @@ function evaluateShadowMessagePreflight(params) {
|
|
|
375
410
|
const structuredMentions = getShadowMessageMentions(message);
|
|
376
411
|
const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
377
412
|
const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
|
|
378
|
-
const wasMentionedExplicitly = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || mentionRegex.test(message.content);
|
|
413
|
+
const wasMentionedExplicitly = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || messageHasActiveTaskForBot(message, botUserId) || mentionRegex.test(message.content);
|
|
379
414
|
if (policy?.mentionOnly && !wasMentionedExplicitly) {
|
|
380
415
|
return {
|
|
381
416
|
ok: false,
|
|
@@ -506,7 +541,7 @@ async function deliverShadowReply(params) {
|
|
|
506
541
|
await withDeliveryRetry({
|
|
507
542
|
label: "reply-media",
|
|
508
543
|
runtime,
|
|
509
|
-
operation: () => client.uploadMediaFromUrl(mediaUrl, messageId)
|
|
544
|
+
operation: () => client.uploadMediaFromUrl(mediaUrl, { messageId })
|
|
510
545
|
});
|
|
511
546
|
runtime.log?.("[reply] Media uploaded successfully");
|
|
512
547
|
} catch (err) {
|
|
@@ -1187,6 +1222,50 @@ function normalizeStringList(value) {
|
|
|
1187
1222
|
if (!Array.isArray(value)) return [];
|
|
1188
1223
|
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
1189
1224
|
}
|
|
1225
|
+
function isRuntimeTaskCard(card) {
|
|
1226
|
+
return card.kind === "task" && typeof card.id === "string" && typeof card.title === "string" && typeof card.status === "string";
|
|
1227
|
+
}
|
|
1228
|
+
function isTerminalTaskStatus(status) {
|
|
1229
|
+
return status === "completed" || status === "failed" || status === "canceled" || status === "transferred";
|
|
1230
|
+
}
|
|
1231
|
+
function taskClaimExpired(card) {
|
|
1232
|
+
if (!card.claim?.expiresAt) return true;
|
|
1233
|
+
return new Date(card.claim.expiresAt).getTime() <= Date.now();
|
|
1234
|
+
}
|
|
1235
|
+
function findRuntimeTaskCard(message, botUserId) {
|
|
1236
|
+
const cards = message.metadata?.cards;
|
|
1237
|
+
if (!Array.isArray(cards)) return null;
|
|
1238
|
+
return cards.find(
|
|
1239
|
+
(card) => isRuntimeTaskCard(card) && card.assignee?.userId === botUserId && !isTerminalTaskStatus(card.status)
|
|
1240
|
+
) ?? null;
|
|
1241
|
+
}
|
|
1242
|
+
function findTaskCardById(message, cardId) {
|
|
1243
|
+
const cards = message?.metadata?.cards;
|
|
1244
|
+
if (!Array.isArray(cards)) return null;
|
|
1245
|
+
return cards.find((card) => isRuntimeTaskCard(card) && card.id === cardId) ?? null;
|
|
1246
|
+
}
|
|
1247
|
+
function taskCardPrompt(message, card) {
|
|
1248
|
+
const workspaceId = card.data?.task && typeof card.data.task.workspaceId === "string" ? card.data.task.workspaceId : void 0;
|
|
1249
|
+
const claimId = typeof card.claim?.id === "string" ? card.claim.id : void 0;
|
|
1250
|
+
return [
|
|
1251
|
+
"Shadow Inbox task:",
|
|
1252
|
+
`Task message id: ${message.id}`,
|
|
1253
|
+
`Task card id: ${card.id}`,
|
|
1254
|
+
claimId ? `Task claim id: ${claimId}` : "",
|
|
1255
|
+
workspaceId ? `Task workspace id: ${workspaceId}` : "",
|
|
1256
|
+
`Task status: ${card.status}`,
|
|
1257
|
+
`Task title: ${card.title}`,
|
|
1258
|
+
card.priority ? `Task priority: ${card.priority}` : "",
|
|
1259
|
+
card.body ? `Task body:
|
|
1260
|
+
${card.body}` : "",
|
|
1261
|
+
card.source ? `Task source: ${JSON.stringify(card.source)}` : "",
|
|
1262
|
+
claimId ? [
|
|
1263
|
+
"When calling Shadow Server App commands for this task, bind the call with:",
|
|
1264
|
+
`--task-message-id ${message.id} --task-card-id ${card.id} --task-claim-id ${claimId}`
|
|
1265
|
+
].join("\n") : "",
|
|
1266
|
+
"When you complete useful work for this task, reply with the concrete result and any next action."
|
|
1267
|
+
].filter(Boolean).join("\n");
|
|
1268
|
+
}
|
|
1190
1269
|
function isSenderCommandAuthorized(policyConfig, senderId) {
|
|
1191
1270
|
const triggerUserIds = normalizeStringList(
|
|
1192
1271
|
policyConfig?.allowedTriggerUserIds ?? policyConfig?.triggerUserIds
|
|
@@ -1361,6 +1440,31 @@ async function processShadowMessage(params) {
|
|
|
1361
1440
|
});
|
|
1362
1441
|
runtime.log?.(`[routing] Resolved agent: ${route.agentId} (account ${accountId})`);
|
|
1363
1442
|
const mediaClient = new ShadowClient2(account.serverUrl, account.token);
|
|
1443
|
+
let runtimeTaskCard = findRuntimeTaskCard(message, botUserId);
|
|
1444
|
+
if (runtimeTaskCard && (runtimeTaskCard.status === "queued" || (runtimeTaskCard.status === "claimed" || runtimeTaskCard.status === "running") && taskClaimExpired(runtimeTaskCard))) {
|
|
1445
|
+
try {
|
|
1446
|
+
const claimed = await mediaClient.claimTaskCard(message.id, runtimeTaskCard.id, {
|
|
1447
|
+
ttlSeconds: 3600,
|
|
1448
|
+
note: "OpenClaw runtime claimed task"
|
|
1449
|
+
});
|
|
1450
|
+
runtimeTaskCard = findTaskCardById(claimed, runtimeTaskCard.id) ?? runtimeTaskCard;
|
|
1451
|
+
} catch (err) {
|
|
1452
|
+
runtime.error?.(`[task] Failed claiming task card ${runtimeTaskCard.id}: ${String(err)}`);
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
if (runtimeTaskCard && runtimeTaskCard.status === "claimed") {
|
|
1457
|
+
await mediaClient.updateTaskCard(message.id, runtimeTaskCard.id, {
|
|
1458
|
+
status: "running",
|
|
1459
|
+
note: "OpenClaw runtime started work"
|
|
1460
|
+
}).then((updated) => {
|
|
1461
|
+
runtimeTaskCard = findTaskCardById(updated, runtimeTaskCard.id) ?? runtimeTaskCard;
|
|
1462
|
+
}).catch((err) => {
|
|
1463
|
+
runtime.error?.(
|
|
1464
|
+
`[task] Failed marking task card ${runtimeTaskCard?.id} running: ${String(err)}`
|
|
1465
|
+
);
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1364
1468
|
const mediaContext = await resolveShadowInboundMediaContext({
|
|
1365
1469
|
account,
|
|
1366
1470
|
message,
|
|
@@ -1425,6 +1529,7 @@ async function processShadowMessage(params) {
|
|
|
1425
1529
|
viewerCommerceContext,
|
|
1426
1530
|
mentionContext,
|
|
1427
1531
|
serverAppContext.prompt,
|
|
1532
|
+
runtimeTaskCard ? taskCardPrompt(message, runtimeTaskCard) : "",
|
|
1428
1533
|
messageBodyForAgent
|
|
1429
1534
|
].filter(Boolean).join("\n\n");
|
|
1430
1535
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
@@ -1437,6 +1542,7 @@ async function processShadowMessage(params) {
|
|
|
1437
1542
|
const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1438
1543
|
const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
|
|
1439
1544
|
const wasMentioned = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || Boolean(slashCommandMatch) || mentionRegex.test(message.content);
|
|
1545
|
+
const taskSessionKey = runtimeTaskCard ? `${route.sessionKey}:task:${runtimeTaskCard.id}` : route.sessionKey;
|
|
1440
1546
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
1441
1547
|
Body: body,
|
|
1442
1548
|
BodyForAgent: bodyForAgent,
|
|
@@ -1447,7 +1553,7 @@ async function processShadowMessage(params) {
|
|
|
1447
1553
|
CommandSource: "text",
|
|
1448
1554
|
From: `shadowob:user:${senderId}`,
|
|
1449
1555
|
To: `shadowob:channel:${channelId}`,
|
|
1450
|
-
SessionKey:
|
|
1556
|
+
SessionKey: taskSessionKey,
|
|
1451
1557
|
AccountId: route.accountId,
|
|
1452
1558
|
ChatType: chatType,
|
|
1453
1559
|
ConversationLabel: conversationLabel,
|
|
@@ -1473,6 +1579,12 @@ async function processShadowMessage(params) {
|
|
|
1473
1579
|
} : {},
|
|
1474
1580
|
BotUserId: botUserId,
|
|
1475
1581
|
BotUsername: botUsername,
|
|
1582
|
+
...runtimeTaskCard ? {
|
|
1583
|
+
TaskCardId: runtimeTaskCard.id,
|
|
1584
|
+
TaskCardTitle: runtimeTaskCard.title,
|
|
1585
|
+
TaskCardStatus: runtimeTaskCard.status,
|
|
1586
|
+
TaskCardPriority: runtimeTaskCard.priority
|
|
1587
|
+
} : {},
|
|
1476
1588
|
AgentId: route.agentId,
|
|
1477
1589
|
ChannelId: channelId,
|
|
1478
1590
|
...slashCommandMatch ? {
|
|
@@ -1593,9 +1705,29 @@ async function processShadowMessage(params) {
|
|
|
1593
1705
|
}).catch((err) => {
|
|
1594
1706
|
runtime.error?.(`[usage] Failed to report usage snapshot for ${message.id}: ${String(err)}`);
|
|
1595
1707
|
});
|
|
1708
|
+
if (runtimeTaskCard) {
|
|
1709
|
+
await client.updateTaskCard(message.id, runtimeTaskCard.id, {
|
|
1710
|
+
status: "completed",
|
|
1711
|
+
note: "OpenClaw runtime completed reply dispatch"
|
|
1712
|
+
}).catch((err) => {
|
|
1713
|
+
runtime.error?.(
|
|
1714
|
+
`[task] Failed marking task card ${runtimeTaskCard?.id} completed: ${String(err)}`
|
|
1715
|
+
);
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1596
1718
|
socket.updateActivity(channelId, "ready");
|
|
1597
1719
|
} catch (err) {
|
|
1598
1720
|
runtime.error?.(`[msg] AI dispatch failed for message ${message.id}: ${String(err)}`);
|
|
1721
|
+
if (runtimeTaskCard) {
|
|
1722
|
+
await client.updateTaskCard(message.id, runtimeTaskCard.id, {
|
|
1723
|
+
status: "failed",
|
|
1724
|
+
note: `OpenClaw runtime failed: ${String(err)}`.slice(0, 4e3)
|
|
1725
|
+
}).catch((updateErr) => {
|
|
1726
|
+
runtime.error?.(
|
|
1727
|
+
`[task] Failed marking task card ${runtimeTaskCard?.id} failed: ${String(updateErr)}`
|
|
1728
|
+
);
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1599
1731
|
socket.updateActivity(channelId, null);
|
|
1600
1732
|
throw err;
|
|
1601
1733
|
} finally {
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
resolveOutboundMentions,
|
|
16
16
|
setShadowRuntime,
|
|
17
17
|
tryGetShadowRuntime
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-6B7WPA2D.js";
|
|
19
19
|
|
|
20
20
|
// index.ts
|
|
21
21
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
@@ -151,7 +151,7 @@ async function sendMediaToShadow(params) {
|
|
|
151
151
|
const uploadErrors = [];
|
|
152
152
|
for (const mediaUrl of mediaUrls) {
|
|
153
153
|
try {
|
|
154
|
-
await params.client.uploadMediaFromUrl(mediaUrl, sent.message.id);
|
|
154
|
+
await params.client.uploadMediaFromUrl(mediaUrl, { messageId: sent.message.id });
|
|
155
155
|
} catch (err) {
|
|
156
156
|
const fallback = await sendTextChunks({
|
|
157
157
|
client: params.client,
|
|
@@ -507,6 +507,20 @@ var shadowMessageToolSchemaProperties = {
|
|
|
507
507
|
filename: optionalSchema(stringSchema("Attachment filename when buffer is used.")),
|
|
508
508
|
contentType: optionalSchema(stringSchema("Attachment MIME type when buffer is used.")),
|
|
509
509
|
mimeType: optionalSchema(stringSchema("Alias for contentType.")),
|
|
510
|
+
attachmentKind: optionalSchema(
|
|
511
|
+
enumSchema(["file", "image", "voice"], "Use voice when sending a Shadow voice message.")
|
|
512
|
+
),
|
|
513
|
+
durationMs: optionalSchema(numberSchema("Voice message duration in milliseconds.")),
|
|
514
|
+
waveformPeaks: optionalSchema(
|
|
515
|
+
arraySchema(numberSchema("Voice waveform peak from 0 to 100."), {
|
|
516
|
+
minItems: 32,
|
|
517
|
+
maxItems: 96
|
|
518
|
+
})
|
|
519
|
+
),
|
|
520
|
+
transcript: optionalSchema(stringSchema("Optional transcript text for a voice message.")),
|
|
521
|
+
transcriptLanguage: optionalSchema(
|
|
522
|
+
stringSchema("Optional BCP-47 transcript language, e.g. zh-CN.")
|
|
523
|
+
),
|
|
510
524
|
caption: optionalSchema(stringSchema("Optional text sent with an attachment.")),
|
|
511
525
|
commerceOfferId: optionalSchema(
|
|
512
526
|
stringSchema("Shadow CommerceOfferId to attach as a purchasable product card.")
|
|
@@ -533,7 +547,14 @@ function buildShadowMessageToolSchemaProperties(input) {
|
|
|
533
547
|
}
|
|
534
548
|
|
|
535
549
|
// src/channel/actions.ts
|
|
536
|
-
var SHADOW_DISCOVERED_ACTIONS = [
|
|
550
|
+
var SHADOW_DISCOVERED_ACTIONS = [
|
|
551
|
+
"send",
|
|
552
|
+
"upload-file",
|
|
553
|
+
"send-voice",
|
|
554
|
+
"react",
|
|
555
|
+
"edit",
|
|
556
|
+
"delete"
|
|
557
|
+
];
|
|
537
558
|
var SHADOW_HANDLED_ACTIONS = [...SHADOW_DISCOVERED_ACTIONS, "get-connection-status"];
|
|
538
559
|
function textResult(value) {
|
|
539
560
|
return {
|
|
@@ -571,6 +592,39 @@ function readAttachmentFilename(params) {
|
|
|
571
592
|
function readCommerceOfferId(params) {
|
|
572
593
|
return firstString(params.commerceOfferId, params.offerId);
|
|
573
594
|
}
|
|
595
|
+
function readNumber(params, ...keys) {
|
|
596
|
+
for (const key of keys) {
|
|
597
|
+
const value = params[key];
|
|
598
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
599
|
+
if (typeof value === "string" && value.trim()) {
|
|
600
|
+
const parsed = Number(value);
|
|
601
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return void 0;
|
|
605
|
+
}
|
|
606
|
+
function readWaveformPeaks(params) {
|
|
607
|
+
const value = params.waveformPeaks ?? params.waveform_peaks;
|
|
608
|
+
if (Array.isArray(value)) {
|
|
609
|
+
const peaks = value.map((item) => Number(item));
|
|
610
|
+
return peaks.every((item) => Number.isInteger(item) && item >= 0 && item <= 100) ? peaks : void 0;
|
|
611
|
+
}
|
|
612
|
+
if (typeof value === "string" && value.trim()) {
|
|
613
|
+
try {
|
|
614
|
+
const parsed = JSON.parse(value);
|
|
615
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
616
|
+
const peaks = parsed.map((item) => Number(item));
|
|
617
|
+
return peaks.every((item) => Number.isInteger(item) && item >= 0 && item <= 100) ? peaks : void 0;
|
|
618
|
+
} catch {
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return void 0;
|
|
623
|
+
}
|
|
624
|
+
function readAttachmentKind(params) {
|
|
625
|
+
const kind = firstString(params.attachmentKind, params.kind);
|
|
626
|
+
return kind === "voice" || kind === "image" || kind === "file" ? kind : void 0;
|
|
627
|
+
}
|
|
574
628
|
function buildSendMetadata(params) {
|
|
575
629
|
const metadata = {};
|
|
576
630
|
if (params.interactiveBlock) metadata.interactive = params.interactiveBlock;
|
|
@@ -585,16 +639,36 @@ async function uploadShadowAttachment(params) {
|
|
|
585
639
|
const uploadTarget = params.messageId;
|
|
586
640
|
const base64Buffer = firstString(params.actionParams.buffer);
|
|
587
641
|
const mediaUrl = readAttachmentSource(params.actionParams);
|
|
642
|
+
const kind = readAttachmentKind(params.actionParams);
|
|
643
|
+
const durationMs = readNumber(params.actionParams, "durationMs", "duration_ms");
|
|
644
|
+
const waveformPeaks = readWaveformPeaks(params.actionParams);
|
|
645
|
+
const transcriptText = firstString(
|
|
646
|
+
params.actionParams.transcript,
|
|
647
|
+
params.actionParams.transcriptText
|
|
648
|
+
);
|
|
649
|
+
const transcriptLanguage = firstString(
|
|
650
|
+
params.actionParams.transcriptLanguage,
|
|
651
|
+
params.actionParams.transcript_language
|
|
652
|
+
);
|
|
653
|
+
const uploadOptions = { messageId: uploadTarget };
|
|
654
|
+
if (kind) uploadOptions.kind = kind;
|
|
655
|
+
if (typeof durationMs === "number") uploadOptions.durationMs = durationMs;
|
|
656
|
+
if (waveformPeaks) uploadOptions.waveformPeaks = waveformPeaks;
|
|
657
|
+
if (transcriptText) {
|
|
658
|
+
uploadOptions.transcriptText = transcriptText;
|
|
659
|
+
uploadOptions.transcriptSource = "runtime";
|
|
660
|
+
}
|
|
661
|
+
if (transcriptLanguage) uploadOptions.transcriptLanguage = transcriptLanguage;
|
|
588
662
|
if (base64Buffer) {
|
|
589
663
|
const raw = base64Buffer.includes(",") ? base64Buffer.split(",")[1] ?? "" : base64Buffer;
|
|
590
664
|
if (!raw) throw new Error("Invalid base64 attachment payload");
|
|
591
665
|
const bytes = Buffer.from(raw, "base64");
|
|
592
666
|
const blob = new Blob([Uint8Array.from(bytes)], { type: contentType });
|
|
593
|
-
await params.client.uploadMedia(blob, filename, contentType,
|
|
667
|
+
await params.client.uploadMedia(blob, filename, contentType, uploadOptions);
|
|
594
668
|
return { filename, contentType, source: "buffer" };
|
|
595
669
|
}
|
|
596
670
|
if (mediaUrl) {
|
|
597
|
-
await params.client.uploadMediaFromUrl(mediaUrl,
|
|
671
|
+
await params.client.uploadMediaFromUrl(mediaUrl, uploadOptions);
|
|
598
672
|
return { filename, contentType, source: "media", mediaUrl };
|
|
599
673
|
}
|
|
600
674
|
throw new Error("No buffer or media URL provided for attachment");
|
|
@@ -624,12 +698,14 @@ var shadowMessageActions = {
|
|
|
624
698
|
"file",
|
|
625
699
|
"fileUrl",
|
|
626
700
|
"buffer"
|
|
627
|
-
]
|
|
701
|
+
],
|
|
702
|
+
"send-voice": ["media", "mediaUrl", "url", "path", "filePath", "file", "fileUrl", "buffer"]
|
|
628
703
|
}
|
|
629
704
|
};
|
|
630
705
|
},
|
|
631
706
|
messageActionTargetAliases: {
|
|
632
|
-
"upload-file": { aliases: ["recipient", "to", "channelId"] }
|
|
707
|
+
"upload-file": { aliases: ["recipient", "to", "channelId"] },
|
|
708
|
+
"send-voice": { aliases: ["recipient", "to", "channelId"] }
|
|
633
709
|
},
|
|
634
710
|
supportsAction: ({ action }) => SHADOW_HANDLED_ACTIONS.includes(action),
|
|
635
711
|
handleAction: async (ctx) => {
|
|
@@ -686,7 +762,7 @@ var shadowMessageActions = {
|
|
|
686
762
|
return textResult({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
687
763
|
}
|
|
688
764
|
}
|
|
689
|
-
if (action === "upload-file") {
|
|
765
|
+
if (action === "upload-file" || action === "send-voice") {
|
|
690
766
|
try {
|
|
691
767
|
const client = new ShadowClient2(account.serverUrl, account.token);
|
|
692
768
|
const to = readMessageTarget(params);
|
|
@@ -697,24 +773,35 @@ var shadowMessageActions = {
|
|
|
697
773
|
error: "upload-file requires buffer, media, path, or filePath"
|
|
698
774
|
});
|
|
699
775
|
}
|
|
700
|
-
const
|
|
776
|
+
const attachmentParams = action === "send-voice" ? { ...params, kind: "voice", attachmentKind: "voice" } : params;
|
|
777
|
+
if (action === "send-voice") {
|
|
778
|
+
if (readNumber(attachmentParams, "durationMs", "duration_ms") === void 0) {
|
|
779
|
+
return textResult({ ok: false, error: "send-voice requires durationMs" });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
const text = firstString(
|
|
783
|
+
attachmentParams.message,
|
|
784
|
+
attachmentParams.content,
|
|
785
|
+
attachmentParams.text,
|
|
786
|
+
attachmentParams.caption
|
|
787
|
+
) ?? "";
|
|
701
788
|
const message = await sendShadowMessage({
|
|
702
789
|
client,
|
|
703
790
|
to,
|
|
704
791
|
content: text || "\u200B",
|
|
705
|
-
threadId:
|
|
706
|
-
replyToId:
|
|
792
|
+
threadId: attachmentParams.threadId,
|
|
793
|
+
replyToId: attachmentParams.replyTo ?? attachmentParams.replyToId
|
|
707
794
|
});
|
|
708
795
|
const attachment = await uploadShadowAttachment({
|
|
709
796
|
client,
|
|
710
797
|
to,
|
|
711
798
|
messageId: message.id,
|
|
712
|
-
actionParams:
|
|
799
|
+
actionParams: attachmentParams
|
|
713
800
|
});
|
|
714
801
|
return textResult({
|
|
715
802
|
ok: true,
|
|
716
803
|
action: requestedAction,
|
|
717
|
-
canonicalAction:
|
|
804
|
+
canonicalAction: action,
|
|
718
805
|
messageId: message.id,
|
|
719
806
|
filename: attachment.filename
|
|
720
807
|
});
|
|
@@ -960,7 +1047,7 @@ shadowPlugin.gateway = {
|
|
|
960
1047
|
lastError: null
|
|
961
1048
|
});
|
|
962
1049
|
ctx.log?.info(`Starting Shadow connection for account ${accountId}`);
|
|
963
|
-
const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-
|
|
1050
|
+
const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-5FSCOBE4.js");
|
|
964
1051
|
await monitorShadowProvider2({
|
|
965
1052
|
account,
|
|
966
1053
|
accountId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowob/openclaw-shadowob",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.8",
|
|
4
4
|
"description": "OpenClaw Shadow channel plugin — enables AI agents to interact in Shadow server channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"zod": "^3.25.67",
|
|
60
60
|
"openclaw": "^2026.5.7",
|
|
61
|
-
"@shadowob/sdk": "1.1.
|
|
61
|
+
"@shadowob/sdk": "1.1.8"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@types/node": "^22.15.21",
|
package/skills/shadowob/SKILL.md
CHANGED
|
@@ -282,12 +282,19 @@ shadowob app discover --server <server-id-or-slug> --json
|
|
|
282
282
|
shadowob app inspect <app-key> --server <server-id-or-slug> --json
|
|
283
283
|
shadowob app skills <app-key> --server <server-id-or-slug>
|
|
284
284
|
shadowob app call <app-key> <command> --server <server-id-or-slug> --channel-id <channel-id> --json-input '<raw-command-input-json>' --json
|
|
285
|
+
shadowob app call <app-key> <command> --server <server-id-or-slug> --help
|
|
286
|
+
shadowob app call <app-key> <command> --server <server-id-or-slug> --file <path> --json-input '<raw-command-input-json>' --json
|
|
287
|
+
shadowob app events <app-key> --server <server-id-or-slug> --json
|
|
285
288
|
```
|
|
286
289
|
|
|
287
290
|
For server App commands, use the `shadowob app` CLI path only. Do not use curl, fetch, raw HTTP
|
|
288
291
|
routes, or the JavaScript SDK to call server App commands. Pass the command input object directly
|
|
289
292
|
to `--json-input`, for example `{"title":"Example","priority":"high"}`; the CLI wraps the HTTP
|
|
290
293
|
request for you and binds Shadow OAuth identity, server membership, App grants, and command policy.
|
|
294
|
+
Use progressive disclosure: start with `shadowob app skills` or `shadowob app discover`, then call
|
|
295
|
+
`shadowob app call <app-key> <command> --server <server> --help` only when you need that command's
|
|
296
|
+
full schema, file-upload support, or examples. For realtime app updates, subscribe with
|
|
297
|
+
`shadowob app events <app-key> --server <server> --json` instead of polling.
|
|
291
298
|
When a channel message mentions a server App, use the mentioned app key/server id directly and pass
|
|
292
299
|
the current channel id with `--channel-id` when available. If a server App command requires
|
|
293
300
|
approval, do not send a chat form or call the approval endpoint yourself as a Buddy. Wait for a
|