@agent-wechat/agent-wechat 1.0.0-beta.11 → 1.0.0-beta.12
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/README.md +30 -0
- package/dist/index.js +641 -152
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -114,13 +114,26 @@ import { delimiter, dirname, isAbsolute, join, toNamespacedPath } from "node:pat
|
|
|
114
114
|
import { createConnection } from "node:net";
|
|
115
115
|
|
|
116
116
|
// ../shared/dist/native.generated.js
|
|
117
|
-
var NATIVE_MAX_SOURCE_BYTES =
|
|
117
|
+
var NATIVE_MAX_SOURCE_BYTES = 536870912;
|
|
118
|
+
var NATIVE_MAX_ORDINARY_SOURCE_BYTES = 26214383;
|
|
119
|
+
var NATIVE_MAX_VIDEO_SOURCE_BYTES = 52428800;
|
|
118
120
|
|
|
119
121
|
// ../shared/dist/native-client.js
|
|
120
122
|
function isNativeTombstone(value) {
|
|
121
123
|
const item = value;
|
|
122
124
|
return item?.deleted === true && typeof item.id === "string" && typeof item.version === "string";
|
|
123
125
|
}
|
|
126
|
+
function validateNativeRecallMetadata(value) {
|
|
127
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
128
|
+
throw new Error("Invalid native recall metadata");
|
|
129
|
+
const recall = value;
|
|
130
|
+
if (recall.replacementText != null && (typeof recall.replacementText !== "string" || !recall.replacementText || recall.replacementText.length > 16384 || new TextEncoder().encode(recall.replacementText).length > 16384 || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\ufffe\uffff]/.test(recall.replacementText)))
|
|
131
|
+
throw new Error("Invalid native recall metadata");
|
|
132
|
+
}
|
|
133
|
+
function validateMessageRecall(message) {
|
|
134
|
+
if (message.recall != null)
|
|
135
|
+
validateNativeRecallMetadata(message.recall);
|
|
136
|
+
}
|
|
124
137
|
function listQuery(input) {
|
|
125
138
|
const options = typeof input === "string" ? { cursor: input } : input;
|
|
126
139
|
if (!options)
|
|
@@ -169,6 +182,7 @@ var NativeWeChatClient = class {
|
|
|
169
182
|
token;
|
|
170
183
|
fetchImpl;
|
|
171
184
|
timeoutMs;
|
|
185
|
+
mediaTimeoutMs;
|
|
172
186
|
streamIdleTimeoutMs;
|
|
173
187
|
constructor(options) {
|
|
174
188
|
this.baseUrl = validateNativeBaseUrl(options.baseUrl);
|
|
@@ -180,16 +194,19 @@ var NativeWeChatClient = class {
|
|
|
180
194
|
this.token = options.token;
|
|
181
195
|
this.fetchImpl = options.fetch ?? fetch;
|
|
182
196
|
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
197
|
+
this.mediaTimeoutMs = options.mediaTimeoutMs ?? 60 * 6e4;
|
|
198
|
+
if (!Number.isInteger(this.mediaTimeoutMs) || this.mediaTimeoutMs < 1 || this.mediaTimeoutMs > 60 * 6e4)
|
|
199
|
+
throw new Error("Invalid native media timeout");
|
|
183
200
|
this.streamIdleTimeoutMs = options.streamIdleTimeoutMs ?? 45e3;
|
|
184
201
|
if (!Number.isInteger(this.streamIdleTimeoutMs) || this.streamIdleTimeoutMs < 1 || this.streamIdleTimeoutMs > 3e5)
|
|
185
202
|
throw new Error("Invalid native stream idle timeout");
|
|
186
203
|
}
|
|
187
|
-
async response(path, init = {}, stream = false) {
|
|
204
|
+
async response(path, init = {}, stream = false, timeoutMs = this.timeoutMs) {
|
|
188
205
|
const headers = new Headers(init.headers);
|
|
189
206
|
headers.set("Authorization", `Bearer ${this.token}`);
|
|
190
207
|
if (init.body != null && !(init.body instanceof FormData))
|
|
191
208
|
headers.set("Content-Type", "application/json");
|
|
192
|
-
const timeout = stream ? void 0 : AbortSignal.timeout(
|
|
209
|
+
const timeout = stream ? void 0 : AbortSignal.timeout(timeoutMs);
|
|
193
210
|
const signal = init.signal && timeout ? AbortSignal.any([init.signal, timeout]) : init.signal ?? timeout;
|
|
194
211
|
const response = await this.fetchImpl(new URL(path, this.baseUrl), { ...init, headers, redirect: "error", signal });
|
|
195
212
|
if (!response.ok) {
|
|
@@ -344,11 +361,51 @@ var NativeWeChatClient = class {
|
|
|
344
361
|
contact(accountId, username) {
|
|
345
362
|
return this.request(`v1/accounts/${id(accountId)}/contacts/${id(username)}`);
|
|
346
363
|
}
|
|
347
|
-
group(accountId, roomId) {
|
|
348
|
-
return this.request(`v1/accounts/${id(accountId)}/groups/${id(roomId)}
|
|
364
|
+
group(accountId, roomId, signal) {
|
|
365
|
+
return this.request(`v1/accounts/${id(accountId)}/groups/${id(roomId)}`, { signal });
|
|
366
|
+
}
|
|
367
|
+
groupMembers(accountId, roomId, options, signal) {
|
|
368
|
+
return this.request(`v1/accounts/${id(accountId)}/groups/${id(roomId)}/members${listQuery(options)}`, { signal });
|
|
369
|
+
}
|
|
370
|
+
groupMember(accountId, roomId, username, signal) {
|
|
371
|
+
return this.request(`v1/accounts/${id(accountId)}/groups/${id(roomId)}/members/${id(username)}`, { signal });
|
|
372
|
+
}
|
|
373
|
+
async groupMutation(accountId, path, method, body, key, action, target, signal) {
|
|
374
|
+
if (typeof key !== "string" || !key || key.length > 200 || /[\u0000-\u001f\u007f]/.test(key))
|
|
375
|
+
throw new Error("A stable group idempotency key is required");
|
|
376
|
+
const operation = await this.request(`v1/accounts/${id(accountId)}/${path}`, { method, body: JSON.stringify(body), headers: { "Idempotency-Key": key }, signal });
|
|
377
|
+
const result = operation?.result;
|
|
378
|
+
if (!operation || operation.accountId !== accountId || operation.kind !== "group_mutation" || typeof operation.id !== "string" || !operation.id || !result || result.action !== action || result.targetId !== target)
|
|
379
|
+
throw new Error("Native group operation identity is invalid");
|
|
380
|
+
return operation;
|
|
381
|
+
}
|
|
382
|
+
/** A repeated key returns the same operation. Uncertain mutations must be inspected, never repeated with a new key. */
|
|
383
|
+
createGroup(accountId, input, key, signal) {
|
|
384
|
+
return this.groupMutation(accountId, "groups", "POST", input, key, "create", null, signal);
|
|
385
|
+
}
|
|
386
|
+
renameGroup(accountId, groupId, name3, key, signal) {
|
|
387
|
+
return this.groupMutation(accountId, `groups/${id(groupId)}`, "PATCH", { name: name3 }, key, "rename", groupId, signal);
|
|
388
|
+
}
|
|
389
|
+
leaveGroup(accountId, groupId, key, signal) {
|
|
390
|
+
return this.groupMutation(accountId, `groups/${id(groupId)}/leave`, "POST", {}, key, "leave", groupId, signal);
|
|
391
|
+
}
|
|
392
|
+
addGroupMembers(accountId, groupId, members, key, signal) {
|
|
393
|
+
return this.groupMutation(accountId, `groups/${id(groupId)}/members`, "POST", { members }, key, "add_members", groupId, signal);
|
|
394
|
+
}
|
|
395
|
+
removeGroupMembers(accountId, groupId, members, key, signal) {
|
|
396
|
+
return this.groupMutation(accountId, `groups/${id(groupId)}/members`, "DELETE", { members }, key, "remove_members", groupId, signal);
|
|
397
|
+
}
|
|
398
|
+
setMyGroupName(accountId, groupId, displayName, key, signal) {
|
|
399
|
+
return this.groupMutation(accountId, `groups/${id(groupId)}/my-name`, "PUT", { displayName }, key, "my_name", groupId, signal);
|
|
400
|
+
}
|
|
401
|
+
clearGroupAnnouncement(accountId, groupId, key, signal) {
|
|
402
|
+
return this.groupMutation(accountId, `groups/${id(groupId)}/announcement`, "DELETE", {}, key, "clear_announcement", groupId, signal);
|
|
349
403
|
}
|
|
350
|
-
|
|
351
|
-
return this.
|
|
404
|
+
setThreadMuted(accountId, threadId2, muted, key, signal) {
|
|
405
|
+
return this.groupMutation(accountId, `threads/${id(threadId2)}/notifications`, "PUT", { muted }, key, "mute", threadId2, signal);
|
|
406
|
+
}
|
|
407
|
+
groupMutationByKey(accountId, key, signal) {
|
|
408
|
+
return this.request(`v1/accounts/${id(accountId)}/group-mutations/by-idempotency-key?${new URLSearchParams({ key })}`, { signal });
|
|
352
409
|
}
|
|
353
410
|
refreshAvatar(accountId, username) {
|
|
354
411
|
return this.request(`v1/accounts/${id(accountId)}/avatars/${id(username)}/refresh`, { method: "POST", body: "{}" });
|
|
@@ -384,6 +441,7 @@ var NativeWeChatClient = class {
|
|
|
384
441
|
continue;
|
|
385
442
|
if (typeof msg.id !== "string" || msg.accountId !== accountId || msg.threadId !== threadId2 || msg.serverId != null && (typeof msg.serverId !== "string" || !/^\d+$/.test(msg.serverId)))
|
|
386
443
|
throw new Error("Invalid native message identity");
|
|
444
|
+
validateMessageRecall(msg);
|
|
387
445
|
}
|
|
388
446
|
return result;
|
|
389
447
|
}
|
|
@@ -391,13 +449,16 @@ var NativeWeChatClient = class {
|
|
|
391
449
|
const message = await this.request(`v1/accounts/${id(accountId)}/messages/${id(messageId)}`);
|
|
392
450
|
if (message.id !== messageId || message.accountId !== accountId || message.serverId != null && (typeof message.serverId !== "string" || !/^\d+$/.test(message.serverId)))
|
|
393
451
|
throw new Error("Invalid native message identity");
|
|
452
|
+
validateMessageRecall(message);
|
|
394
453
|
return message;
|
|
395
454
|
}
|
|
396
455
|
/** Resolve only the stored original in this message's own conversation. */
|
|
397
456
|
async quotedMessage(accountId, messageId) {
|
|
398
457
|
const quote = await this.request(`v1/accounts/${id(accountId)}/messages/${id(messageId)}/quote`);
|
|
399
|
-
if (quote.accountId !== accountId || quote.messageId !== messageId || typeof quote.threadId !== "string" || !["resolved", "embedded", "missing", "invalid", "unsupported", "ambiguous"].includes(quote.state) || (quote.state === "resolved" ? !quote.message || quote.message.accountId !== accountId || quote.message.threadId !== quote.threadId || quote.message.id === messageId : quote.message != null))
|
|
458
|
+
if (quote.accountId !== accountId || quote.messageId !== messageId || typeof quote.threadId !== "string" || !["resolved", "embedded", "missing", "invalid", "unsupported", "ambiguous", "recalled"].includes(quote.state) || (quote.state === "resolved" ? !quote.message || quote.message.accountId !== accountId || quote.message.threadId !== quote.threadId || quote.message.id === messageId : quote.message != null))
|
|
400
459
|
throw new Error("Invalid native quote identity");
|
|
460
|
+
if (quote.message)
|
|
461
|
+
validateMessageRecall(quote.message);
|
|
401
462
|
return quote;
|
|
402
463
|
}
|
|
403
464
|
/** Account-wide local search, with the daemon's query-bound opaque cursor. */
|
|
@@ -408,13 +469,20 @@ var NativeWeChatClient = class {
|
|
|
408
469
|
continue;
|
|
409
470
|
if (typeof message.id !== "string" || message.accountId !== accountId || typeof message.threadId !== "string" || !message.threadId || message.serverId != null && (typeof message.serverId !== "string" || !/^\d+$/.test(message.serverId)))
|
|
410
471
|
throw new Error("Invalid native archive message identity");
|
|
472
|
+
validateMessageRecall(message);
|
|
411
473
|
}
|
|
412
474
|
return result;
|
|
413
475
|
}
|
|
414
|
-
|
|
476
|
+
/** Quote a locally stored text, image, voice, video, link, file or quoted reply in the same chat.
|
|
477
|
+
* Live acceptance and recipient rendering remain unverified; other source kinds are rejected.
|
|
478
|
+
*/
|
|
479
|
+
sendText(accountId, threadId2, text3, idempotencyKey, quote) {
|
|
415
480
|
if (!idempotencyKey || /[\r\n]/.test(idempotencyKey))
|
|
416
481
|
throw new Error("An idempotency key is required");
|
|
417
|
-
|
|
482
|
+
if (quote !== void 0 && (!quote || typeof quote !== "object" || Array.isArray(quote) || Object.keys(quote).some((key) => key !== "messageId" && key !== "serverId") || typeof quote.messageId !== "string" || !quote.messageId || quote.messageId.length > 8192 || typeof quote.serverId !== "string" || !/^[1-9]\d{0,19}$/.test(quote.serverId) || BigInt(quote.serverId) > 18446744073709551615n))
|
|
483
|
+
throw new Error("Invalid quote message identity");
|
|
484
|
+
const input = quote === void 0 ? { threadId: threadId2, text: text3 } : { kind: "text", threadId: threadId2, text: text3, quote };
|
|
485
|
+
return this.request(`v1/accounts/${id(accountId)}/sends`, { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(input) });
|
|
418
486
|
}
|
|
419
487
|
send(accountId, sendId, signal) {
|
|
420
488
|
return this.request(`v1/accounts/${id(accountId)}/sends/${id(sendId)}`, { signal });
|
|
@@ -432,12 +500,16 @@ var NativeWeChatClient = class {
|
|
|
432
500
|
cancelSend(accountId, sendId) {
|
|
433
501
|
return this.request(`v1/accounts/${id(accountId)}/sends/${id(sendId)}/cancel`, { method: "POST", body: "{}" });
|
|
434
502
|
}
|
|
435
|
-
|
|
436
|
-
|
|
503
|
+
/** A disk-backed Blob avoids holding the source in memory before staging. */
|
|
504
|
+
async upload(accountId, bytes, name3, contentType = "application/octet-stream", signal) {
|
|
505
|
+
const size = bytes instanceof Blob ? bytes.size : bytes.byteLength;
|
|
506
|
+
if (!size || size > NATIVE_MAX_SOURCE_BYTES || !name3 || name3.length > 255 || /[\r\n/\\]/.test(name3))
|
|
437
507
|
throw new Error("Invalid bounded media upload");
|
|
508
|
+
const part = bytes instanceof Blob ? bytes.slice(0, bytes.size, contentType) : new Blob([bytes.buffer instanceof ArrayBuffer ? new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength) : new Uint8Array(bytes)], { type: contentType });
|
|
438
509
|
const form = new FormData();
|
|
439
|
-
form.set("file",
|
|
440
|
-
|
|
510
|
+
form.set("file", part, name3);
|
|
511
|
+
const response = await this.response(`v1/accounts/${id(accountId)}/uploads`, { method: "POST", body: form, signal }, false, this.mediaTimeoutMs);
|
|
512
|
+
return JSON.parse(new TextDecoder().decode(await this.readBounded(response)));
|
|
441
513
|
}
|
|
442
514
|
sendMedia(accountId, input, key) {
|
|
443
515
|
if (!key || /[\r\n]/.test(key))
|
|
@@ -459,26 +531,94 @@ var NativeWeChatClient = class {
|
|
|
459
531
|
cancelJob(accountId, jobId) {
|
|
460
532
|
return this.request(`v1/accounts/${id(accountId)}/jobs/${id(jobId)}/cancel`, { method: "POST", body: "{}" });
|
|
461
533
|
}
|
|
462
|
-
retryJob(accountId, jobId) {
|
|
463
|
-
|
|
534
|
+
retryJob(accountId, jobId, options = {}) {
|
|
535
|
+
if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some((key) => key !== "reuseCompletedUpload"))
|
|
536
|
+
throw new Error("Invalid media retry options");
|
|
537
|
+
const reuse = options.reuseCompletedUpload;
|
|
538
|
+
if (reuse !== void 0 && typeof reuse !== "boolean")
|
|
539
|
+
throw new Error("Invalid media retry options");
|
|
540
|
+
return this.request(`v1/accounts/${id(accountId)}/jobs/${id(jobId)}/${reuse === true ? "retry-completed-upload" : "retry"}`, { method: "POST", body: JSON.stringify(reuse === void 0 ? {} : { reuseCompletedUpload: reuse }) });
|
|
464
541
|
}
|
|
465
542
|
repairMediaWriteback(accountId, limit = 25) {
|
|
466
543
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
467
544
|
throw new Error("Invalid media writeback limit");
|
|
468
545
|
return this.request(`v1/accounts/${id(accountId)}/media-writeback/repair`, { method: "POST", body: JSON.stringify({ limit }) });
|
|
469
546
|
}
|
|
470
|
-
|
|
547
|
+
/** Consume the bounded stream once, or cancel it; this never starts a CDN transfer. */
|
|
548
|
+
async mediaContentStream(accountId, assetId, options = {}) {
|
|
471
549
|
const max = options.maxBytes ?? 128 * 1024 * 1024;
|
|
472
|
-
if (!Number.isInteger(max) || max < 1 || max > 128 * 1024 * 1024 || options.range && !/^bytes=(?:\d+-\d*|-\d+)$/.test(options.range) || options.representation != null && !["original", "preview"].includes(options.representation))
|
|
550
|
+
if (!Number.isInteger(max) || max < 1 || max > Math.max(128 * 1024 * 1024, NATIVE_MAX_SOURCE_BYTES) || options.range && !/^bytes=(?:\d+-\d*|-\d+)$/.test(options.range) || options.representation != null && !["original", "preview"].includes(options.representation))
|
|
473
551
|
throw new Error("Invalid bounded content request");
|
|
474
552
|
const query = options.representation === "preview" ? "?representation=preview" : "";
|
|
475
|
-
const
|
|
553
|
+
const cancellation = new AbortController();
|
|
554
|
+
const response = await this.response(`v1/accounts/${id(accountId)}/media/${id(assetId)}/content${query}`, { signal: options.signal ? AbortSignal.any([options.signal, cancellation.signal]) : cancellation.signal, headers: options.range ? { Range: options.range } : {} }, false, this.mediaTimeoutMs);
|
|
476
555
|
const length = response.headers.get("content-length");
|
|
477
556
|
if (length && (!/^\d+$/.test(length) || Number(length) > max)) {
|
|
478
557
|
await response.body?.cancel();
|
|
479
558
|
throw new Error("Native media response exceeds its bound");
|
|
480
559
|
}
|
|
481
|
-
|
|
560
|
+
if (!response.body && length && Number(length) > 0)
|
|
561
|
+
throw new Error("Native media response length mismatch");
|
|
562
|
+
let activeReader;
|
|
563
|
+
const iterate = async function* () {
|
|
564
|
+
cancellation.signal.throwIfAborted();
|
|
565
|
+
options.signal?.throwIfAborted();
|
|
566
|
+
const reader = response.body?.getReader();
|
|
567
|
+
if (!reader)
|
|
568
|
+
return;
|
|
569
|
+
activeReader = reader;
|
|
570
|
+
let size = 0;
|
|
571
|
+
try {
|
|
572
|
+
while (true) {
|
|
573
|
+
cancellation.signal.throwIfAborted();
|
|
574
|
+
options.signal?.throwIfAborted();
|
|
575
|
+
const { value, done } = await reader.read();
|
|
576
|
+
cancellation.signal.throwIfAborted();
|
|
577
|
+
options.signal?.throwIfAborted();
|
|
578
|
+
if (done)
|
|
579
|
+
break;
|
|
580
|
+
size += value.byteLength;
|
|
581
|
+
if (size > max)
|
|
582
|
+
throw new Error("Native media response exceeds its bound");
|
|
583
|
+
yield value;
|
|
584
|
+
}
|
|
585
|
+
if (length && size !== Number(length))
|
|
586
|
+
throw new Error("Native media response length mismatch");
|
|
587
|
+
} finally {
|
|
588
|
+
await reader.cancel().catch(() => {
|
|
589
|
+
});
|
|
590
|
+
reader.releaseLock();
|
|
591
|
+
activeReader = void 0;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
let claimed = false;
|
|
595
|
+
const stream = { [Symbol.asyncIterator]() {
|
|
596
|
+
if (claimed)
|
|
597
|
+
throw new Error("Native media stream was already consumed");
|
|
598
|
+
claimed = true;
|
|
599
|
+
return iterate();
|
|
600
|
+
} };
|
|
601
|
+
return { stream, cancel: async () => {
|
|
602
|
+
cancellation.abort();
|
|
603
|
+
await (activeReader ? activeReader.cancel() : response.body?.cancel())?.catch(() => {
|
|
604
|
+
});
|
|
605
|
+
}, contentType: response.headers.get("content-type") ?? "application/octet-stream", etag: response.headers.get("etag"), contentRange: response.headers.get("content-range"), sourceSha256: response.headers.get("x-source-sha256"), status: response.status };
|
|
606
|
+
}
|
|
607
|
+
async mediaContent(accountId, assetId, options = {}) {
|
|
608
|
+
const { stream, cancel: _cancel, ...metadata } = await this.mediaContentStream(accountId, assetId, options);
|
|
609
|
+
const chunks = [];
|
|
610
|
+
let size = 0;
|
|
611
|
+
for await (const chunk of stream) {
|
|
612
|
+
chunks.push(chunk);
|
|
613
|
+
size += chunk.byteLength;
|
|
614
|
+
}
|
|
615
|
+
const bytes = new Uint8Array(size);
|
|
616
|
+
let offset = 0;
|
|
617
|
+
for (const chunk of chunks) {
|
|
618
|
+
bytes.set(chunk, offset);
|
|
619
|
+
offset += chunk.byteLength;
|
|
620
|
+
}
|
|
621
|
+
return { ...metadata, bytes };
|
|
482
622
|
}
|
|
483
623
|
markRead(accountId, threadId2, expectedVersion) {
|
|
484
624
|
if (typeof expectedVersion !== "string" || !expectedVersion || expectedVersion.length > 128)
|
|
@@ -488,7 +628,12 @@ var NativeWeChatClient = class {
|
|
|
488
628
|
events(accountId, after = "0", limit = 100) {
|
|
489
629
|
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
490
630
|
throw new Error("Invalid event page limit");
|
|
491
|
-
return this.request(`v1/accounts/${id(accountId)}/events?after=${decimal(after)}&limit=${limit}`)
|
|
631
|
+
return this.request(`v1/accounts/${id(accountId)}/events?after=${decimal(after)}&limit=${limit}`).then((page) => {
|
|
632
|
+
for (const event of page.items)
|
|
633
|
+
if (event.kind === "message.recalled")
|
|
634
|
+
validateNativeRecallMetadata(event.data);
|
|
635
|
+
return page;
|
|
636
|
+
});
|
|
492
637
|
}
|
|
493
638
|
ack(accountId, consumerId, cursor) {
|
|
494
639
|
return this.request(`v1/accounts/${id(accountId)}/consumers/${id(consumerId)}/ack`, { method: "POST", body: JSON.stringify({ cursor: decimal(cursor) }) });
|
|
@@ -552,6 +697,8 @@ var NativeWeChatClient = class {
|
|
|
552
697
|
const event = payload;
|
|
553
698
|
if (!event || typeof event.kind !== "string" || !event.data || typeof event.data !== "object")
|
|
554
699
|
throw new Error("Invalid native event");
|
|
700
|
+
if (event.kind === "message.recalled")
|
|
701
|
+
validateNativeRecallMetadata(event.data);
|
|
555
702
|
event.cursor = decimal(event.cursor ?? eventCursor);
|
|
556
703
|
yield event;
|
|
557
704
|
}
|
|
@@ -598,7 +745,7 @@ function requireNativeIdentity(value) {
|
|
|
598
745
|
}
|
|
599
746
|
}
|
|
600
747
|
function nativeChildEnvironment(token, environment = process.env) {
|
|
601
|
-
return Object.fromEntries(["PATH", "HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "SystemRoot", "WINDIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "TZ", "SSL_CERT_FILE", "SSL_CERT_DIR"].filter((
|
|
748
|
+
return Object.fromEntries(["PATH", "HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "SystemRoot", "WINDIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "TMPDIR", "TMP", "TEMP", "LANG", "LC_ALL", "TZ", "SSL_CERT_FILE", "SSL_CERT_DIR"].filter((name3) => environment[name3] !== void 0 && environment[name3] !== token).map((name3) => [name3, environment[name3]]));
|
|
602
749
|
}
|
|
603
750
|
function nativeHostTarget(platform = process.platform, arch = process.arch) {
|
|
604
751
|
const cpu = arch === "arm64" ? "aarch64" : "x86_64";
|
|
@@ -1034,15 +1181,21 @@ function resolveWeChatIngress(params) {
|
|
|
1034
1181
|
});
|
|
1035
1182
|
}
|
|
1036
1183
|
|
|
1184
|
+
// src/outbound-options.ts
|
|
1185
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
1186
|
+
|
|
1037
1187
|
// src/outbound-media.ts
|
|
1038
1188
|
import { createHash as createHash5 } from "node:crypto";
|
|
1039
1189
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1040
1190
|
|
|
1041
1191
|
// src/native-media.ts
|
|
1042
1192
|
import { createHash as createHash4, randomUUID } from "node:crypto";
|
|
1043
|
-
import {
|
|
1044
|
-
import {
|
|
1193
|
+
import { openAsBlob } from "node:fs";
|
|
1194
|
+
import { mkdtemp, open, rm } from "node:fs/promises";
|
|
1195
|
+
import { tmpdir } from "node:os";
|
|
1196
|
+
import { basename, extname, join as join2 } from "node:path";
|
|
1045
1197
|
import { getImageMetadata, resizeToJpeg, IMAGE_REDUCE_QUALITY_STEPS } from "openclaw/plugin-sdk/media-runtime";
|
|
1198
|
+
import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
|
|
1046
1199
|
|
|
1047
1200
|
// src/attachment-store.ts
|
|
1048
1201
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -1062,6 +1215,12 @@ async function saveManagedAttachment(params) {
|
|
|
1062
1215
|
}
|
|
1063
1216
|
const buffer = Buffer.from(params.data, "base64");
|
|
1064
1217
|
if (buffer.byteLength !== size) throw new Error("Attachment decoding length mismatch");
|
|
1218
|
+
return saveManagedAttachmentBytes({ ...params, bytes: buffer });
|
|
1219
|
+
}
|
|
1220
|
+
async function saveManagedAttachmentBytes(params) {
|
|
1221
|
+
const size = params.bytes.byteLength;
|
|
1222
|
+
if (size > params.maxBytes) throw new AttachmentTooLargeError(`Attachment exceeds configured ${params.maxBytes} byte limit`);
|
|
1223
|
+
const buffer = Buffer.from(params.bytes.buffer, params.bytes.byteOffset, size);
|
|
1065
1224
|
const sha256 = createHash3("sha256").update(buffer).digest("hex");
|
|
1066
1225
|
const saved = await params.saveMediaBuffer(
|
|
1067
1226
|
buffer,
|
|
@@ -1072,31 +1231,59 @@ async function saveManagedAttachment(params) {
|
|
|
1072
1231
|
);
|
|
1073
1232
|
return { path: saved.path, size, sha256, filename: params.filename };
|
|
1074
1233
|
}
|
|
1234
|
+
async function saveVerifiedManagedAttachmentStream(params) {
|
|
1235
|
+
if (!Number.isSafeInteger(params.expectedBytes) || params.expectedBytes < 1 || params.expectedBytes > params.maxBytes || !/^[a-f0-9]{64}$/.test(params.expectedSha256)) throw new Error("Native attachment integrity metadata is invalid");
|
|
1236
|
+
let size = 0, finished = false;
|
|
1237
|
+
const digest2 = createHash3("sha256");
|
|
1238
|
+
const stream = (async function* () {
|
|
1239
|
+
for await (const chunk of params.stream) {
|
|
1240
|
+
params.assertCurrent();
|
|
1241
|
+
size += chunk.byteLength;
|
|
1242
|
+
if (size > params.maxBytes || size > params.expectedBytes) throw new AttachmentTooLargeError("Native attachment stream exceeds its bound");
|
|
1243
|
+
digest2.update(chunk);
|
|
1244
|
+
yield chunk;
|
|
1245
|
+
}
|
|
1246
|
+
params.assertCurrent();
|
|
1247
|
+
if (size !== params.expectedBytes || digest2.digest("hex") !== params.expectedSha256) throw new Error("Native attachment integrity did not verify");
|
|
1248
|
+
finished = true;
|
|
1249
|
+
})();
|
|
1250
|
+
const saved = await params.saveMediaStream(
|
|
1251
|
+
stream,
|
|
1252
|
+
params.mime,
|
|
1253
|
+
"inbound",
|
|
1254
|
+
params.maxBytes,
|
|
1255
|
+
params.filename,
|
|
1256
|
+
void 0,
|
|
1257
|
+
{ assertCommitAllowed: params.assertCurrent, durable: true }
|
|
1258
|
+
);
|
|
1259
|
+
if (!finished || saved.size != null && saved.size !== size) throw new Error("Native attachment stream was not fully saved");
|
|
1260
|
+
return { path: saved.path, size, sha256: params.expectedSha256, filename: params.filename };
|
|
1261
|
+
}
|
|
1075
1262
|
|
|
1076
1263
|
// src/media-delivery.ts
|
|
1077
1264
|
function appendReference(body, reference) {
|
|
1078
1265
|
return body ? `${body}
|
|
1079
1266
|
${reference}` : reference;
|
|
1080
1267
|
}
|
|
1081
|
-
function safeDisplayName(
|
|
1082
|
-
return
|
|
1268
|
+
function safeDisplayName(name3) {
|
|
1269
|
+
return name3.replace(/[\r\n\0]/g, " ").trim() || "attachment";
|
|
1083
1270
|
}
|
|
1084
1271
|
function renderAttachmentBody(originalBody, attachment, exposeNonVisualPath = true) {
|
|
1085
1272
|
if (!attachment) return originalBody;
|
|
1086
|
-
const
|
|
1273
|
+
const name3 = safeDisplayName(attachment.filename);
|
|
1087
1274
|
if (attachment.status !== "ready" || !attachment.path) {
|
|
1088
1275
|
const label = attachment.kind === "file" ? "File" : attachment.kind === "image" ? "Image" : attachment.kind === "audio" ? "Voice message" : "Video";
|
|
1089
1276
|
if (attachment.status === "pending") {
|
|
1090
|
-
return appendReference(originalBody, `[${label} pending: ${
|
|
1277
|
+
return appendReference(originalBody, `[${label} pending: ${name3}]`);
|
|
1091
1278
|
}
|
|
1092
1279
|
if (attachment.status === "expired") {
|
|
1093
|
-
return appendReference(originalBody, `[${label} expired: ${
|
|
1280
|
+
return appendReference(originalBody, `[${label} expired: ${name3}; no cached copy available. Ask the sender to resend.]`);
|
|
1094
1281
|
}
|
|
1095
|
-
return appendReference(originalBody, `[${label} unavailable: ${
|
|
1282
|
+
return appendReference(originalBody, `[${label} unavailable: ${name3} (${attachment.status})]`);
|
|
1096
1283
|
}
|
|
1097
1284
|
if (attachment.kind === "file") {
|
|
1098
|
-
const prefix = originalBody.trim() ===
|
|
1099
|
-
return appendReference(prefix, `[File: ${
|
|
1285
|
+
const prefix = originalBody.trim() === name3 ? "" : originalBody;
|
|
1286
|
+
return appendReference(prefix, `[File: ${name3}]
|
|
1100
1287
|
[Local file: <${attachment.path}>]`);
|
|
1101
1288
|
}
|
|
1102
1289
|
if (exposeNonVisualPath) {
|
|
@@ -1164,15 +1351,33 @@ async function submitNativeText(client, accountId, threadId2, text3, key, option
|
|
|
1164
1351
|
|
|
1165
1352
|
// src/native-recall-state.ts
|
|
1166
1353
|
function nativeRecallInfo(message) {
|
|
1354
|
+
if (message.recall) validateNativeRecallMetadata(message.recall);
|
|
1167
1355
|
return message.recall;
|
|
1168
1356
|
}
|
|
1169
1357
|
function nativeRecallIdentity(threadId2, serverId) {
|
|
1170
1358
|
return JSON.stringify([threadId2, serverId]);
|
|
1171
1359
|
}
|
|
1172
1360
|
function validateNativeRecall(value) {
|
|
1361
|
+
validateNativeRecallMetadata(value);
|
|
1173
1362
|
if (!value || typeof value.threadId !== "string" || !value.threadId || value.threadId.length > 4096 || typeof value.serverId !== "string" || !/^[1-9]\d{0,19}$/.test(value.serverId) || BigInt(value.serverId) > 18446744073709551615n || typeof value.observedAt !== "string" || value.observedAt.length > 128 || !Number.isFinite(Date.parse(value.observedAt)) || value.recalledAt !== void 0 && (typeof value.recalledAt !== "string" || value.recalledAt.length > 128 || !Number.isFinite(Date.parse(value.recalledAt))) || value.messageId !== void 0 && (typeof value.messageId !== "string" || !value.messageId || value.messageId.length > 4096)) throw Error("Invalid native recall observation; cursor retained");
|
|
1174
1363
|
}
|
|
1364
|
+
function mergeNativeRecall(prior, next) {
|
|
1365
|
+
validateNativeRecall(prior);
|
|
1366
|
+
validateNativeRecall(next);
|
|
1367
|
+
if (prior.threadId !== next.threadId || prior.serverId !== next.serverId) throw Error("Conflicting native recall identity; cursor retained");
|
|
1368
|
+
return {
|
|
1369
|
+
threadId: prior.threadId,
|
|
1370
|
+
serverId: prior.serverId,
|
|
1371
|
+
observedAt: prior.observedAt,
|
|
1372
|
+
...prior.messageId ?? next.messageId ? { messageId: prior.messageId ?? next.messageId } : {},
|
|
1373
|
+
...prior.recalledAt ?? next.recalledAt ? { recalledAt: prior.recalledAt ?? next.recalledAt } : {},
|
|
1374
|
+
...(prior.replacementText ?? next.replacementText) != null ? { replacementText: prior.replacementText ?? next.replacementText } : {}
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1175
1377
|
function recalledNativeMessage(message, recall) {
|
|
1378
|
+
validateNativeRecall(recall);
|
|
1379
|
+
const previous = nativeRecallInfo(message);
|
|
1380
|
+
const replacementText = previous?.replacementText ?? recall.replacementText;
|
|
1176
1381
|
return {
|
|
1177
1382
|
...message,
|
|
1178
1383
|
type: 1e4,
|
|
@@ -1180,8 +1385,14 @@ function recalledNativeMessage(message, recall) {
|
|
|
1180
1385
|
quote: void 0,
|
|
1181
1386
|
link: void 0,
|
|
1182
1387
|
media: [],
|
|
1388
|
+
senderId: message.senderId || null,
|
|
1389
|
+
direction: message.senderId ? message.direction : null,
|
|
1183
1390
|
mentions: { state: "unknown", userIds: [], self: null, all: null, source: null },
|
|
1184
|
-
recall: {
|
|
1391
|
+
recall: {
|
|
1392
|
+
observedAt: previous?.observedAt ?? recall.observedAt,
|
|
1393
|
+
...previous?.recalledAt ?? recall.recalledAt ? { recalledAt: previous?.recalledAt ?? recall.recalledAt } : {},
|
|
1394
|
+
...replacementText != null ? { replacementText } : {}
|
|
1395
|
+
}
|
|
1185
1396
|
};
|
|
1186
1397
|
}
|
|
1187
1398
|
|
|
@@ -1192,6 +1403,26 @@ function kind(type) {
|
|
|
1192
1403
|
var terminalJobs = /* @__PURE__ */ new Set(["accepted", "completed", "succeeded", "available", "failed", "partial", "cancelled", "expired", "needs_review"]);
|
|
1193
1404
|
var successfulSendJobs = /* @__PURE__ */ new Set(["accepted", "completed", "succeeded"]);
|
|
1194
1405
|
var terminalIdentityErrors = /* @__PURE__ */ new Set(["media_identity_unavailable", "media_source_changed"]);
|
|
1406
|
+
var NativeMediaJobInspectionError = class extends Error {
|
|
1407
|
+
code = "native_media_job_inspection_required";
|
|
1408
|
+
retryable = false;
|
|
1409
|
+
jobId;
|
|
1410
|
+
accountId;
|
|
1411
|
+
threadId;
|
|
1412
|
+
idempotencyKey;
|
|
1413
|
+
state;
|
|
1414
|
+
reason;
|
|
1415
|
+
constructor(job, accountId, threadId2, key, reason) {
|
|
1416
|
+
super(`Native media job ${job.id} requires inspection (${job.state}); retain the original job and idempotency key and do not automatically resend`);
|
|
1417
|
+
this.name = "NativeMediaJobInspectionError";
|
|
1418
|
+
this.jobId = job.id;
|
|
1419
|
+
this.accountId = accountId;
|
|
1420
|
+
this.threadId = threadId2;
|
|
1421
|
+
this.idempotencyKey = key;
|
|
1422
|
+
this.state = job.state;
|
|
1423
|
+
this.reason = reason;
|
|
1424
|
+
}
|
|
1425
|
+
};
|
|
1195
1426
|
function mediaSendResult(job) {
|
|
1196
1427
|
return job.result && typeof job.result === "object" && !Array.isArray(job.result) ? job.result : {};
|
|
1197
1428
|
}
|
|
@@ -1227,6 +1458,9 @@ async function waitNativeMediaSendReceipts(client, accountId, threadId2, initial
|
|
|
1227
1458
|
return job;
|
|
1228
1459
|
}
|
|
1229
1460
|
async function waitNativeMediaJob(client, accountId, initial, timeoutMs = 65e3, signal, assertCurrent) {
|
|
1461
|
+
return waitNativeMediaStatus(initial, timeoutMs, signal, assertCurrent, (job, pollSignal) => client.job(accountId, job.id, pollSignal));
|
|
1462
|
+
}
|
|
1463
|
+
async function waitNativeMediaStatus(initial, timeoutMs, signal, assertCurrent, readStatus) {
|
|
1230
1464
|
const deadline = Date.now() + timeoutMs;
|
|
1231
1465
|
let job = initial;
|
|
1232
1466
|
let delay = 250;
|
|
@@ -1238,11 +1472,27 @@ async function waitNativeMediaJob(client, accountId, initial, timeoutMs = 65e3,
|
|
|
1238
1472
|
if (remaining <= 0) break;
|
|
1239
1473
|
signal?.throwIfAborted();
|
|
1240
1474
|
assertCurrent?.();
|
|
1241
|
-
|
|
1475
|
+
const next = await readStatus(job, signal ? AbortSignal.any([signal, AbortSignal.timeout(remaining)]) : AbortSignal.timeout(remaining));
|
|
1476
|
+
if (!next) break;
|
|
1477
|
+
job = next;
|
|
1242
1478
|
delay = Math.min(delay * 2, 1e3);
|
|
1243
1479
|
}
|
|
1244
1480
|
return job;
|
|
1245
1481
|
}
|
|
1482
|
+
async function incomingMediaJobStatus(client, accountId, jobId, assetId, signal, assertCurrent) {
|
|
1483
|
+
let job;
|
|
1484
|
+
assertCurrent();
|
|
1485
|
+
try {
|
|
1486
|
+
job = await client.job(accountId, jobId, signal);
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
assertCurrent();
|
|
1489
|
+
if (error instanceof NativeApiError ? error.retryable && !terminalIdentityErrors.has(error.code) && error.code !== "message_recalled" : error instanceof TypeError || error instanceof DOMException && ["TimeoutError", "NetworkError", "AbortError"].includes(error.name)) return void 0;
|
|
1490
|
+
throw error;
|
|
1491
|
+
}
|
|
1492
|
+
assertCurrent();
|
|
1493
|
+
if (job.id !== jobId || job.accountId !== accountId || job.kind !== "download" || assetId != null && job.assetId != null && job.assetId !== assetId) throw Error("Native download job identity does not match its attachment");
|
|
1494
|
+
return job;
|
|
1495
|
+
}
|
|
1246
1496
|
function select(assets, type, quality = "best") {
|
|
1247
1497
|
const preferred = type === 34 ? ["normal"] : type === 43 ? ["normal", "original"] : ["original", "standard", "thumbnail", "encoded"];
|
|
1248
1498
|
const rank = (asset) => {
|
|
@@ -1419,7 +1669,10 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1419
1669
|
if (message.type === 49 && message.appMessageType != null && message.appMessageType !== 6) return { body: `[Unsupported WeChat app message subtype ${message.appMessageType}]`, pending: false, retryable: false };
|
|
1420
1670
|
if (!attachmentKind) return { body: message.type === 1 ? message.body ?? "" : `[Unsupported WeChat message type ${message.type}]`, pending: false };
|
|
1421
1671
|
const accountId = account.native.accountId;
|
|
1422
|
-
const maxBytes = Math.min(
|
|
1672
|
+
const maxBytes = Math.min(
|
|
1673
|
+
account.mediaMaxMb * 1024 * 1024,
|
|
1674
|
+
attachmentKind === "file" ? Math.max(NATIVE_MAX_SOURCE_BYTES, 128 * 1024 * 1024) : 128 * 1024 * 1024
|
|
1675
|
+
);
|
|
1423
1676
|
const scopedAssets = async () => {
|
|
1424
1677
|
await authorize();
|
|
1425
1678
|
const items = (await client.mediaAssets(accountId, message.id)).items;
|
|
@@ -1445,9 +1698,9 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1445
1698
|
let transferError;
|
|
1446
1699
|
if (!asset && initialCandidate?.jobId && initialCandidate.state !== "recalled") {
|
|
1447
1700
|
await authorize();
|
|
1448
|
-
job = await client
|
|
1701
|
+
job = await incomingMediaJobStatus(client, accountId, initialCandidate.jobId, initialCandidate.id, params.signal, guard);
|
|
1449
1702
|
guard();
|
|
1450
|
-
if (job
|
|
1703
|
+
if (!job) return { body: `[${attachmentKind} pending: retrieval is unavailable]`, pending: true, jobId: initialCandidate.jobId, retryable: true };
|
|
1451
1704
|
assets = await scopedAssets();
|
|
1452
1705
|
asset = select(assets, message.type, quality);
|
|
1453
1706
|
if (!asset && transferCandidate()?.state === "recalled") return { body: "[Message recalled]", pending: false, retryable: false, reason: "message_recalled" };
|
|
@@ -1458,7 +1711,13 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1458
1711
|
try {
|
|
1459
1712
|
job = await client.transferMedia(accountId, { messageId: message.id, assetId: null, quality: transferQuality });
|
|
1460
1713
|
guard();
|
|
1461
|
-
job = await
|
|
1714
|
+
job = await waitNativeMediaStatus(
|
|
1715
|
+
job,
|
|
1716
|
+
params.waitMs ?? 2e4,
|
|
1717
|
+
params.signal,
|
|
1718
|
+
guard,
|
|
1719
|
+
(known, pollSignal) => incomingMediaJobStatus(client, accountId, known.id, initialCandidate?.id, pollSignal, guard)
|
|
1720
|
+
);
|
|
1462
1721
|
guard();
|
|
1463
1722
|
} catch (error) {
|
|
1464
1723
|
guard();
|
|
@@ -1491,6 +1750,55 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1491
1750
|
return { body: renderAttachmentBody("", attachment2), attachment: attachment2, pending: false };
|
|
1492
1751
|
}
|
|
1493
1752
|
await authorize();
|
|
1753
|
+
if (attachmentKind === "file" && asset.byteLength != null && asset.byteLength > 128 * 1024 * 1024) {
|
|
1754
|
+
const content2 = await client.mediaContentStream(accountId, asset.id, { maxBytes, signal: params.signal });
|
|
1755
|
+
const mime2 = content2.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
1756
|
+
if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mime2)) {
|
|
1757
|
+
await content2.cancel();
|
|
1758
|
+
throw Error("Native attachment MIME type is unsupported");
|
|
1759
|
+
}
|
|
1760
|
+
let saved2;
|
|
1761
|
+
try {
|
|
1762
|
+
saved2 = await saveVerifiedManagedAttachmentStream({
|
|
1763
|
+
stream: content2.stream,
|
|
1764
|
+
mime: mime2,
|
|
1765
|
+
filename,
|
|
1766
|
+
maxBytes,
|
|
1767
|
+
expectedBytes: asset.byteLength,
|
|
1768
|
+
expectedSha256: asset.sha256 ?? "",
|
|
1769
|
+
assertCurrent: guard,
|
|
1770
|
+
saveMediaStream: params.saveMediaStream ?? saveMediaStream
|
|
1771
|
+
});
|
|
1772
|
+
} finally {
|
|
1773
|
+
await content2.cancel();
|
|
1774
|
+
}
|
|
1775
|
+
await authorize();
|
|
1776
|
+
const attachment2 = { kind: "file", filename, status: "ready", path: saved2.path, mime: mime2 };
|
|
1777
|
+
return {
|
|
1778
|
+
body: renderAttachmentBody("", attachment2),
|
|
1779
|
+
attachment: attachment2,
|
|
1780
|
+
mediaMime: mime2,
|
|
1781
|
+
pending: false,
|
|
1782
|
+
jobId: job?.id,
|
|
1783
|
+
retryable: false,
|
|
1784
|
+
variant: asset.actualVariant ?? asset.variant,
|
|
1785
|
+
suppliedAssets: [{
|
|
1786
|
+
id: asset.id,
|
|
1787
|
+
contextRole: "trigger",
|
|
1788
|
+
role: "file_reference",
|
|
1789
|
+
kind: "file",
|
|
1790
|
+
path: saved2.path,
|
|
1791
|
+
mime: mime2,
|
|
1792
|
+
sha256: saved2.sha256,
|
|
1793
|
+
byteLength: saved2.size,
|
|
1794
|
+
runtimeMedia: false,
|
|
1795
|
+
sourceSha256: saved2.sha256,
|
|
1796
|
+
sourceByteLength: saved2.size,
|
|
1797
|
+
sourceMime: mime2,
|
|
1798
|
+
sourcePath: saved2.path
|
|
1799
|
+
}]
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1494
1802
|
const content = await client.mediaContent(accountId, asset.id, { maxBytes, signal: params.signal });
|
|
1495
1803
|
guard();
|
|
1496
1804
|
const mime = content.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
@@ -1498,7 +1806,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1498
1806
|
const hash = createHash4("sha256").update(content.bytes).digest("hex");
|
|
1499
1807
|
if (!asset.sha256 || hash !== asset.sha256 || asset.byteLength != null && asset.byteLength !== content.bytes.length) throw new Error("Native attachment integrity did not verify");
|
|
1500
1808
|
await authorize();
|
|
1501
|
-
const saved = await
|
|
1809
|
+
const saved = await saveManagedAttachmentBytes({ bytes: content.bytes, mime, filename, maxBytes, saveMediaBuffer: core.channel.media.saveMediaBuffer });
|
|
1502
1810
|
const actual = asset.actualVariant ?? asset.variant;
|
|
1503
1811
|
const attachment = {
|
|
1504
1812
|
kind: attachmentKind,
|
|
@@ -1570,78 +1878,114 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1570
1878
|
pending: false
|
|
1571
1879
|
};
|
|
1572
1880
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1881
|
+
function validateNativeMediaSendOptions(options, audioAsVoice = false) {
|
|
1882
|
+
if (options.forceDocument != null && typeof options.forceDocument !== "boolean") throw new Error("Invalid WeChat document option");
|
|
1883
|
+
if (options.videoQuality != null && !["normal", "original"].includes(options.videoQuality)) throw new Error("Invalid WeChat video quality");
|
|
1884
|
+
if (options.forceDocument && (audioAsVoice || options.videoQuality != null)) throw new Error("Document sending cannot also request voice or video quality");
|
|
1885
|
+
}
|
|
1886
|
+
function outboundMediaKind(mime, audioAsVoice, options) {
|
|
1887
|
+
if (options.forceDocument) return "file";
|
|
1888
|
+
if (audioAsVoice && !mime.startsWith("audio/")) throw new Error("Voice sending requires an explicit audio attachment");
|
|
1889
|
+
const selected = audioAsVoice ? "voice" : mime.startsWith("image/") ? "image" : mime.startsWith("video/") ? "video" : "file";
|
|
1890
|
+
if (options.videoQuality != null && selected !== "video") throw new Error("Video quality requires a video attachment");
|
|
1891
|
+
return selected;
|
|
1892
|
+
}
|
|
1893
|
+
function outboundSourceLimit(account, selectedKind) {
|
|
1894
|
+
return Math.min(account.mediaMaxMb * 1024 * 1024, selectedKind === "file" ? NATIVE_MAX_SOURCE_BYTES : selectedKind === "video" ? NATIVE_MAX_VIDEO_SOURCE_BYTES : NATIVE_MAX_ORDINARY_SOURCE_BYTES);
|
|
1895
|
+
}
|
|
1896
|
+
async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioAsVoice = false, key = randomUUID(), beforeSend, options = {}) {
|
|
1897
|
+
validateNativeMediaSendOptions(options, audioAsVoice);
|
|
1898
|
+
await beforeSend?.();
|
|
1899
|
+
let filename, mime, selectedKind, sourceBytes, source;
|
|
1900
|
+
let temporary;
|
|
1901
|
+
const accountId = account.native.accountId;
|
|
1902
|
+
try {
|
|
1903
|
+
let blob;
|
|
1904
|
+
if (/^https?:/.test(mediaUrl)) {
|
|
1905
|
+
const response = await fetch(mediaUrl, { redirect: "error", signal: AbortSignal.timeout(60 * 6e4) });
|
|
1906
|
+
if (!response.ok) throw new Error(`Native outbound source unavailable (${response.status})`);
|
|
1907
|
+
filename = basename(new URL(mediaUrl).pathname) || "attachment";
|
|
1908
|
+
mime = response.headers.get("content-type")?.split(";")[0].trim().toLowerCase() ?? "application/octet-stream";
|
|
1909
|
+
try {
|
|
1910
|
+
selectedKind = outboundMediaKind(mime, audioAsVoice, options);
|
|
1911
|
+
} catch (error) {
|
|
1912
|
+
await response.body?.cancel();
|
|
1913
|
+
throw error;
|
|
1597
1914
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
}
|
|
1604
|
-
bytes = new Uint8Array(size);
|
|
1605
|
-
let offset = 0;
|
|
1606
|
-
for (const chunk of chunks2) {
|
|
1607
|
-
bytes.set(chunk, offset);
|
|
1608
|
-
offset += chunk.length;
|
|
1609
|
-
}
|
|
1610
|
-
filename = basename(new URL(mediaUrl).pathname) || "attachment";
|
|
1611
|
-
mime = response.headers.get("content-type")?.split(";")[0] ?? "application/octet-stream";
|
|
1612
|
-
} else {
|
|
1613
|
-
const path = mediaUrl.startsWith("file:") ? new URL(mediaUrl) : mediaUrl;
|
|
1614
|
-
const file = await open(path, "r");
|
|
1615
|
-
try {
|
|
1616
|
-
const stat = await file.stat();
|
|
1617
|
-
if (!stat.isFile() || stat.size === 0 || stat.size > max) throw new Error("Native outbound source must be a bounded regular file");
|
|
1618
|
-
const chunks2 = [];
|
|
1619
|
-
let size = 0;
|
|
1620
|
-
while (true) {
|
|
1621
|
-
const chunk = Buffer.alloc(Math.min(65536, max + 1 - size));
|
|
1622
|
-
const { bytesRead } = await file.read(chunk, 0, chunk.length, null);
|
|
1623
|
-
if (!bytesRead) break;
|
|
1624
|
-
size += bytesRead;
|
|
1625
|
-
if (size > max) throw new Error("Native outbound source exceeds configured size");
|
|
1626
|
-
chunks2.push(chunk.subarray(0, bytesRead));
|
|
1915
|
+
const max = outboundSourceLimit(account, selectedKind);
|
|
1916
|
+
const length = response.headers.get("content-length");
|
|
1917
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > max)) {
|
|
1918
|
+
await response.body?.cancel();
|
|
1919
|
+
throw new Error("Native outbound source exceeds configured size");
|
|
1627
1920
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1921
|
+
const reader = response.body?.getReader();
|
|
1922
|
+
if (!reader) throw new Error("Native outbound source has no bytes");
|
|
1923
|
+
let path, file, size = 0;
|
|
1924
|
+
try {
|
|
1925
|
+
temporary = await mkdtemp(join2(tmpdir(), "wechat-outbound-"));
|
|
1926
|
+
path = join2(temporary, "source");
|
|
1927
|
+
file = await open(path, "wx", 384);
|
|
1928
|
+
while (true) {
|
|
1929
|
+
const { value, done } = await reader.read();
|
|
1930
|
+
if (done) break;
|
|
1931
|
+
size += value.byteLength;
|
|
1932
|
+
if (size > max) throw new Error("Native outbound source exceeds configured size");
|
|
1933
|
+
await file.writeFile(value);
|
|
1934
|
+
}
|
|
1935
|
+
if (!size || length && size !== Number(length)) throw new Error("Native outbound source length mismatch");
|
|
1936
|
+
} finally {
|
|
1937
|
+
await reader.cancel().catch(() => {
|
|
1938
|
+
});
|
|
1939
|
+
reader.releaseLock();
|
|
1940
|
+
await file?.close();
|
|
1941
|
+
}
|
|
1942
|
+
blob = await openAsBlob(path, { type: mime });
|
|
1943
|
+
} else {
|
|
1944
|
+
const path = mediaUrl.startsWith("file:") ? new URL(mediaUrl) : mediaUrl;
|
|
1945
|
+
filename = basename(path instanceof URL ? path.pathname : path);
|
|
1946
|
+
const extension = extname(filename).toLowerCase();
|
|
1947
|
+
mime = [".png", ".jpg", ".jpeg", ".gif", ".webp"].includes(extension) ? `image/${extension === ".jpg" ? "jpeg" : extension.slice(1)}` : extension === ".mp4" ? "video/mp4" : [".wav", ".mp3", ".ogg", ".m4a", ".flac", ".aac"].includes(extension) ? `audio/${extension.slice(1)}` : "application/octet-stream";
|
|
1948
|
+
selectedKind = outboundMediaKind(mime, audioAsVoice, options);
|
|
1949
|
+
const max = outboundSourceLimit(account, selectedKind), file = await open(path, "r");
|
|
1950
|
+
let size;
|
|
1951
|
+
try {
|
|
1952
|
+
const stat = await file.stat();
|
|
1953
|
+
if (!stat.isFile() || !stat.size || stat.size > max) throw new Error("Native outbound source must be a bounded regular file");
|
|
1954
|
+
size = stat.size;
|
|
1955
|
+
} finally {
|
|
1956
|
+
await file.close();
|
|
1957
|
+
}
|
|
1958
|
+
blob = await openAsBlob(path, { type: mime });
|
|
1959
|
+
if (blob.size !== size) throw new Error("Native outbound source changed before staging");
|
|
1631
1960
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1961
|
+
sourceBytes = blob.size;
|
|
1962
|
+
if (sourceBytes > outboundSourceLimit(account, selectedKind)) throw new Error("Native outbound source exceeds selected media kind size");
|
|
1963
|
+
await beforeSend?.();
|
|
1964
|
+
source = await client.upload(accountId, blob, filename, mime);
|
|
1965
|
+
} finally {
|
|
1966
|
+
if (temporary) await rm(temporary, { recursive: true, force: true });
|
|
1635
1967
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1968
|
+
const input = {
|
|
1969
|
+
threadId: threadId2,
|
|
1970
|
+
kind: selectedKind,
|
|
1971
|
+
sourceId: source.id,
|
|
1972
|
+
standardSourceId: null,
|
|
1973
|
+
thumbnailSourceId: null,
|
|
1974
|
+
playSeconds: null,
|
|
1975
|
+
durationMs: null,
|
|
1976
|
+
...selectedKind === "video" ? { videoQuality: options.videoQuality ?? "normal" } : {}
|
|
1977
|
+
};
|
|
1641
1978
|
await beforeSend?.();
|
|
1642
|
-
|
|
1979
|
+
const waitMs = audioAsVoice ? 25 * 6e4 : selectedKind === "video" || selectedKind === "file" && sourceBytes > NATIVE_MAX_ORDINARY_SOURCE_BYTES ? 60 * 6e4 : 65e3;
|
|
1980
|
+
const submitted = await client.sendMedia(accountId, input, key);
|
|
1981
|
+
let job;
|
|
1982
|
+
try {
|
|
1983
|
+
job = await waitNativeMediaJob(client, accountId, submitted, waitMs);
|
|
1984
|
+
} catch {
|
|
1985
|
+
throw new NativeMediaJobInspectionError(submitted, accountId, threadId2, key, "status_unavailable");
|
|
1986
|
+
}
|
|
1643
1987
|
if (successfulSendJobs.has(job.state)) job = await waitNativeMediaSendReceipts(client, accountId, threadId2, job, audioAsVoice);
|
|
1644
|
-
if (!successfulSendJobs.has(job.state)) throw new
|
|
1988
|
+
if (!successfulSendJobs.has(job.state)) throw new NativeMediaJobInspectionError(job, accountId, threadId2, key, "job_not_completed");
|
|
1645
1989
|
const result = mediaSendResult(job);
|
|
1646
1990
|
const chunks = Array.isArray(result.chunks) ? result.chunks.filter((chunk) => chunk && typeof chunk === "object") : [];
|
|
1647
1991
|
const serverIds = audioAsVoice ? chunks.filter((chunk) => chunk.state === "accepted" && typeof chunk.serverMessageId === "string").map((chunk) => chunk.serverMessageId) : job.serverMessageId ? [job.serverMessageId] : void 0;
|
|
@@ -1649,13 +1993,16 @@ async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioA
|
|
|
1649
1993
|
}
|
|
1650
1994
|
|
|
1651
1995
|
// src/outbound-media.ts
|
|
1652
|
-
async function sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice = false, idempotencyKey) {
|
|
1996
|
+
async function sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice = false, idempotencyKey, options = {}, beforeSend) {
|
|
1997
|
+
validateNativeMediaSendOptions(options, audioAsVoice);
|
|
1998
|
+
if (!mediaUrl && (options.forceDocument || options.videoQuality != null)) throw new Error("WeChat media options require an attachment");
|
|
1653
1999
|
const account = resolveWeChatAccount(cfg);
|
|
1654
2000
|
if (!account?.native) throw new Error("No native daemon configured");
|
|
1655
2001
|
const { client } = await ensureNativeHost(account.native);
|
|
1656
2002
|
const key = idempotencyKey ?? randomUUID2();
|
|
1657
2003
|
if (!mediaUrl) {
|
|
1658
2004
|
if (audioAsVoice) throw new Error("Voice sending requires an audio attachment");
|
|
2005
|
+
await beforeSend?.();
|
|
1659
2006
|
const send = await submitNativeText(client, account.native.accountId, to, text3, key);
|
|
1660
2007
|
return nativeOutboundDelivery(
|
|
1661
2008
|
client,
|
|
@@ -1665,26 +2012,116 @@ async function sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice = false, i
|
|
|
1665
2012
|
{ sendId: send.id, state: send.state, serverIds: send.serverId ? [send.serverId] : void 0 }
|
|
1666
2013
|
);
|
|
1667
2014
|
}
|
|
1668
|
-
const result = await sendNativeAttachment(client, account, to, mediaUrl, audioAsVoice, key);
|
|
1669
|
-
if (text3)
|
|
2015
|
+
const result = await sendNativeAttachment(client, account, to, mediaUrl, audioAsVoice, key, beforeSend, options);
|
|
2016
|
+
if (text3) {
|
|
2017
|
+
await beforeSend?.();
|
|
2018
|
+
await submitNativeText(client, account.native.accountId, to, text3, createHash5("sha256").update(`${key}\0caption`).digest("hex"));
|
|
2019
|
+
}
|
|
1670
2020
|
return result;
|
|
1671
2021
|
}
|
|
1672
2022
|
|
|
2023
|
+
// src/outbound-options.ts
|
|
2024
|
+
function nativePayloadMediaOptions(payload) {
|
|
2025
|
+
const value = payload.channelData?.["agent-wechat"];
|
|
2026
|
+
if (value == null) return {};
|
|
2027
|
+
if (typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => key !== "videoQuality") || !["normal", "original"].includes(value.videoQuality)) throw Error("Invalid WeChat media options");
|
|
2028
|
+
return { videoQuality: value.videoQuality };
|
|
2029
|
+
}
|
|
2030
|
+
var wechatMediaActions = {
|
|
2031
|
+
describeMessageTool: () => ({ actions: ["send"], schema: { actions: ["send"], visibility: "all-configured", properties: {
|
|
2032
|
+
channelData: { type: "object", description: "WeChat per-send media options. Use agent-wechat.videoQuality for a single video attachment.", additionalProperties: true, properties: {
|
|
2033
|
+
"agent-wechat": { type: "object", additionalProperties: false, required: ["videoQuality"], properties: {
|
|
2034
|
+
videoQuality: { type: "string", enum: ["normal", "original"], description: "WeChat video mode: normal prepares playback; original also retains the unchanged source video. Cannot combine with forceDocument." }
|
|
2035
|
+
} }
|
|
2036
|
+
} }
|
|
2037
|
+
} } }),
|
|
2038
|
+
prepareSendPayload: ({ ctx, payload }) => {
|
|
2039
|
+
const options = nativePayloadMediaOptions(payload);
|
|
2040
|
+
if (options.videoQuality == null) return void 0;
|
|
2041
|
+
validateNativeMediaSendOptions({ ...options, forceDocument: ctx.params.forceDocument ?? ctx.params.asDocument }, payload.audioAsVoice);
|
|
2042
|
+
return payload;
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
2045
|
+
var NativeMediaPayloadInspectionError = class extends Error {
|
|
2046
|
+
retryable = false;
|
|
2047
|
+
code = "native_media_payload_inspection_required";
|
|
2048
|
+
deliveries;
|
|
2049
|
+
pendingJob;
|
|
2050
|
+
constructor(deliveries, pendingJob) {
|
|
2051
|
+
super("WeChat attachment batch requires inspection; retain completed receipts and do not automatically resend");
|
|
2052
|
+
this.name = "NativeMediaPayloadInspectionError";
|
|
2053
|
+
this.deliveries = deliveries;
|
|
2054
|
+
this.pendingJob = pendingJob;
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
function createWeChatMediaHandlers(send = sendWeChatMedia) {
|
|
2058
|
+
const key = (ctx, index = 0) => ctx.deliveryQueueId ? createHash6("sha256").update(JSON.stringify(["wechat-media", ctx.deliveryQueueId, ctx.deliveryPartIndex ?? 0, index])).digest("hex") : void 0;
|
|
2059
|
+
const guard = (ctx) => async () => {
|
|
2060
|
+
ctx.signal?.throwIfAborted();
|
|
2061
|
+
await ctx.onPlatformSendDispatch?.();
|
|
2062
|
+
ctx.signal?.throwIfAborted();
|
|
2063
|
+
ctx.assertDirectAdapterHandoff?.();
|
|
2064
|
+
};
|
|
2065
|
+
return {
|
|
2066
|
+
sendMedia: async (ctx) => ({
|
|
2067
|
+
channel: "agent-wechat",
|
|
2068
|
+
...await send(ctx.cfg, ctx.to, ctx.text, ctx.mediaUrl, ctx.audioAsVoice, key(ctx), { forceDocument: ctx.forceDocument }, guard(ctx))
|
|
2069
|
+
}),
|
|
2070
|
+
sendPayload: async (ctx) => {
|
|
2071
|
+
const options = { ...nativePayloadMediaOptions(ctx.payload), forceDocument: ctx.forceDocument };
|
|
2072
|
+
const audioAsVoice = ctx.payload.audioAsVoice ?? ctx.audioAsVoice;
|
|
2073
|
+
validateNativeMediaSendOptions(options, audioAsVoice);
|
|
2074
|
+
const urls = [...new Set([...ctx.payload.mediaUrls ?? [], ctx.payload.mediaUrl ?? ctx.mediaUrl].filter((url2) => typeof url2 === "string" && !!url2))];
|
|
2075
|
+
if (urls.length > 16 || options.videoQuality != null && urls.length !== 1) throw Error("WeChat video quality requires one attachment per send");
|
|
2076
|
+
const deliveries = [];
|
|
2077
|
+
for (const [index, mediaUrl] of (urls.length ? urls : [void 0]).entries()) {
|
|
2078
|
+
let delivered;
|
|
2079
|
+
try {
|
|
2080
|
+
delivered = await send(ctx.cfg, ctx.to, index === 0 ? ctx.payload.text ?? ctx.text : "", mediaUrl, audioAsVoice, key(ctx, index), options, guard(ctx));
|
|
2081
|
+
} catch (error) {
|
|
2082
|
+
if (!deliveries.length) throw error;
|
|
2083
|
+
throw new NativeMediaPayloadInspectionError(deliveries, error instanceof NativeMediaJobInspectionError ? { jobId: error.jobId, accountId: error.accountId, threadId: error.threadId, idempotencyKey: error.idempotencyKey } : void 0);
|
|
2084
|
+
}
|
|
2085
|
+
deliveries.push(delivered);
|
|
2086
|
+
if (urls.length > 1) {
|
|
2087
|
+
try {
|
|
2088
|
+
await ctx.onDeliveryResult?.({ channel: "agent-wechat", ...delivered });
|
|
2089
|
+
} catch {
|
|
2090
|
+
throw new NativeMediaPayloadInspectionError(deliveries);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
if (delivered.meta.outcome === "unknown") break;
|
|
2094
|
+
}
|
|
2095
|
+
const first = deliveries[0];
|
|
2096
|
+
return {
|
|
2097
|
+
channel: "agent-wechat",
|
|
2098
|
+
...first,
|
|
2099
|
+
...deliveries.length > 1 ? { meta: {
|
|
2100
|
+
...first.meta,
|
|
2101
|
+
parts: deliveries,
|
|
2102
|
+
retryable: false,
|
|
2103
|
+
...deliveries.some((part) => part.meta.outcome === "unknown") ? { outcome: "unknown", receiptState: "needs_review" } : {}
|
|
2104
|
+
} } : {}
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
|
|
1673
2110
|
// src/native-monitor.ts
|
|
1674
|
-
import { createHash as
|
|
2111
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
1675
2112
|
import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-outbound";
|
|
1676
2113
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
1677
2114
|
|
|
1678
2115
|
// src/native-consumer-state.ts
|
|
1679
|
-
import { createHash as
|
|
2116
|
+
import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
|
|
1680
2117
|
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1681
|
-
import { dirname as dirname2, isAbsolute as isAbsolute2, join as
|
|
2118
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
1682
2119
|
var NativeConsumerState = class {
|
|
1683
2120
|
file;
|
|
1684
2121
|
value;
|
|
1685
2122
|
constructor(directory, serverId, accountId) {
|
|
1686
2123
|
if (!isAbsolute2(directory) || !serverId || !accountId) throw new Error("Native consumer state requires absolute storage and bound server/account identities");
|
|
1687
|
-
this.file =
|
|
2124
|
+
this.file = join3(directory, `${createHash7("sha256").update(`${serverId}\0${accountId}`).digest("hex")}.json`);
|
|
1688
2125
|
this.value = { schemaVersion: 1, serverId, accountId, cursor: "0", items: {} };
|
|
1689
2126
|
if (existsSync(this.file)) {
|
|
1690
2127
|
const stat = lstatSync(this.file);
|
|
@@ -1693,6 +2130,7 @@ var NativeConsumerState = class {
|
|
|
1693
2130
|
if (parsed.schemaVersion !== 1 || parsed.serverId !== serverId || parsed.accountId !== accountId || !/^\d+$/.test(parsed.cursor) || !parsed.items || typeof parsed.items !== "object") throw new Error("Native consumer identity or schema mismatch; state was preserved");
|
|
1694
2131
|
for (const [id2, item] of Object.entries(parsed.items)) {
|
|
1695
2132
|
if (item.message?.id !== id2 || item.message.accountId !== accountId || typeof item.message.threadId !== "string" || !["received", "dispatching", "handed_off", "completed", "ignored", "needs_review"].includes(item.state)) throw new Error("Invalid native custody record; state was preserved");
|
|
2133
|
+
nativeRecallInfo(item.message);
|
|
1696
2134
|
if (item.cursor != null && !/^\d+$/.test(item.cursor) || item.groupContext != null && typeof item.groupContext !== "boolean") throw new Error("Invalid native group context record; state was preserved");
|
|
1697
2135
|
if (item.revision != null && (typeof item.revision !== "string" || !item.revision || item.revision.length > 64)) throw Error("Invalid native custody revision; state was preserved");
|
|
1698
2136
|
item.revision ??= randomUUID3();
|
|
@@ -1822,13 +2260,8 @@ var NativeConsumerState = class {
|
|
|
1822
2260
|
}
|
|
1823
2261
|
const key = nativeRecallIdentity(recall.threadId, recall.serverId);
|
|
1824
2262
|
this.value.recalls ??= {};
|
|
1825
|
-
this.value.recalls[key]
|
|
1826
|
-
|
|
1827
|
-
serverId: recall.serverId,
|
|
1828
|
-
observedAt: recall.observedAt,
|
|
1829
|
-
...recall.messageId ? { messageId: recall.messageId } : {},
|
|
1830
|
-
...recall.recalledAt ? { recalledAt: recall.recalledAt } : {}
|
|
1831
|
-
};
|
|
2263
|
+
const remembered = mergeNativeRecall(this.value.recalls[key] ?? recall, recall);
|
|
2264
|
+
this.value.recalls[key] = remembered;
|
|
1832
2265
|
const affected = /* @__PURE__ */ new Set();
|
|
1833
2266
|
for (const item of this.items()) {
|
|
1834
2267
|
if (item.message.threadId !== recall.threadId) continue;
|
|
@@ -1836,7 +2269,7 @@ var NativeConsumerState = class {
|
|
|
1836
2269
|
const quote = item.message.quote?.serverId === recall.serverId;
|
|
1837
2270
|
if (!target && !quote) continue;
|
|
1838
2271
|
affected.add(item.message.id);
|
|
1839
|
-
item.message = target ? recalledNativeMessage(item.message,
|
|
2272
|
+
item.message = target ? recalledNativeMessage(item.message, remembered) : { ...item.message, quote: void 0 };
|
|
1840
2273
|
if (target) item.groupContext = false;
|
|
1841
2274
|
item.readPending = false;
|
|
1842
2275
|
if (["dispatching", "handed_off"].includes(item.state)) item.state = "needs_review";
|
|
@@ -1907,7 +2340,7 @@ var NativeConsumerState = class {
|
|
|
1907
2340
|
});
|
|
1908
2341
|
const revisions = items.map((item) => item.revision);
|
|
1909
2342
|
if (new Set(items.map((item) => item.message.threadId)).size !== 1) throw new Error("Prepared input manifest crosses threads");
|
|
1910
|
-
const manifestHash =
|
|
2343
|
+
const manifestHash = createHash7("sha256").update(JSON.stringify({
|
|
1911
2344
|
accountId: this.value.accountId,
|
|
1912
2345
|
messageIds: exact,
|
|
1913
2346
|
dispatchManifests: items.map((item) => item.dispatchManifest ?? null)
|
|
@@ -1916,7 +2349,7 @@ var NativeConsumerState = class {
|
|
|
1916
2349
|
const runId = typeof args[0] === "string" ? args[0] : args[0]?.runId;
|
|
1917
2350
|
if (typeof runId !== "string" || !runId || runId.length > 256) throw new Error("Prepared run callback has no valid runId");
|
|
1918
2351
|
this.mutate(() => {
|
|
1919
|
-
const currentHash =
|
|
2352
|
+
const currentHash = createHash7("sha256").update(JSON.stringify({
|
|
1920
2353
|
accountId: this.value.accountId,
|
|
1921
2354
|
messageIds: exact,
|
|
1922
2355
|
dispatchManifests: exact.map((id2) => this.value.items[id2]?.dispatchManifest ?? null)
|
|
@@ -1935,11 +2368,11 @@ var NativeConsumerState = class {
|
|
|
1935
2368
|
};
|
|
1936
2369
|
|
|
1937
2370
|
// src/native-prepared-boundary.ts
|
|
1938
|
-
import { createHash as
|
|
2371
|
+
import { createHash as createHash8, randomUUID as randomUUID4 } from "node:crypto";
|
|
1939
2372
|
import { basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
|
|
1940
2373
|
var pending = /* @__PURE__ */ new Map();
|
|
1941
2374
|
var enabled = false;
|
|
1942
|
-
var digest = (value) =>
|
|
2375
|
+
var digest = (value) => createHash8("sha256").update(value).digest("hex");
|
|
1943
2376
|
function prune() {
|
|
1944
2377
|
for (const [id2, item] of pending) if (item.expires < Date.now()) pending.delete(id2);
|
|
1945
2378
|
}
|
|
@@ -1950,7 +2383,7 @@ function nativeDispatchManifest(params) {
|
|
|
1950
2383
|
const history = params.history ?? [];
|
|
1951
2384
|
if (history.length > 200 || history.some((message) => message.accountId !== params.message.accountId || message.threadId !== params.message.threadId || message.id === params.message.id)) throw Error("Invalid native history manifest");
|
|
1952
2385
|
const assets = Object.freeze((params.assets ?? []).map((asset) => {
|
|
1953
|
-
if (typeof asset.id !== "string" || !asset.id || asset.id.length > 2048 || !/^[a-f0-9]{64}$/.test(asset.sha256) || !Number.isSafeInteger(asset.byteLength) || asset.byteLength < 1 || asset.byteLength > 128 * 1024 * 1024 || typeof asset.path !== "string" || !isAbsolute3(asset.path) || asset.path.length > 4096 || typeof asset.mime !== "string" || asset.mime.length > 128 || !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(asset.mime) || !["image", "audio", "video", "file"].includes(asset.kind) || !["model_image", "file_reference"].includes(asset.role) || asset.role === "model_image" && (asset.kind !== "image" || !asset.mime.startsWith("image/") || !asset.runtimeMedia) || !["trigger", "history"].includes(asset.contextRole) || asset.sourceMessageId != null && (typeof asset.sourceMessageId !== "string" || !asset.sourceMessageId || asset.sourceMessageId.length > 8192 || ![params.message, ...history].some((message) => message.id === asset.messageId && message.type === 49 && message.appMessageType === 57 && message.quote?.type === 3)) || asset.contextRole === "history" && !history.some((message) => message.id === asset.messageId)) throw Error("Invalid native supplied asset manifest");
|
|
2386
|
+
if (typeof asset.id !== "string" || !asset.id || asset.id.length > 2048 || !/^[a-f0-9]{64}$/.test(asset.sha256) || !Number.isSafeInteger(asset.byteLength) || asset.byteLength < 1 || asset.byteLength > (asset.kind === "file" ? NATIVE_MAX_SOURCE_BYTES : 128 * 1024 * 1024) || typeof asset.path !== "string" || !isAbsolute3(asset.path) || asset.path.length > 4096 || typeof asset.mime !== "string" || asset.mime.length > 128 || !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(asset.mime) || !["image", "audio", "video", "file"].includes(asset.kind) || !["model_image", "file_reference"].includes(asset.role) || asset.role === "model_image" && (asset.kind !== "image" || !asset.mime.startsWith("image/") || !asset.runtimeMedia) || !["trigger", "history"].includes(asset.contextRole) || asset.sourceMessageId != null && (typeof asset.sourceMessageId !== "string" || !asset.sourceMessageId || asset.sourceMessageId.length > 8192 || ![params.message, ...history].some((message) => message.id === asset.messageId && message.type === 49 && message.appMessageType === 57 && message.quote?.type === 3)) || asset.contextRole === "history" && !history.some((message) => message.id === asset.messageId)) throw Error("Invalid native supplied asset manifest");
|
|
1954
2387
|
return Object.freeze({ ...asset });
|
|
1955
2388
|
}));
|
|
1956
2389
|
const messageSid = nativeSourceMessageSid(params.message);
|
|
@@ -2463,12 +2896,12 @@ async function dispatchNativeMessageInput(params, revision) {
|
|
|
2463
2896
|
if (urls.length > 16) throw new Error("Native reply media count exceeds its bound");
|
|
2464
2897
|
if (payload.audioAsVoice && !urls.length) throw new Error("Native voice reply requires an audio attachment");
|
|
2465
2898
|
for (const url2 of urls) {
|
|
2466
|
-
const key =
|
|
2899
|
+
const key = createHash9("sha256").update(`${dispatchManifest.messageSid}\0${delivery++}`).digest("hex");
|
|
2467
2900
|
await sendNativeAttachment(client, account, message.threadId, url2, payload.audioAsVoice === true, key, beforeSend);
|
|
2468
2901
|
}
|
|
2469
2902
|
if (payload.text) {
|
|
2470
2903
|
await beforeSend();
|
|
2471
|
-
const key =
|
|
2904
|
+
const key = createHash9("sha256").update(`${dispatchManifest.messageSid}\0${delivery++}`).digest("hex");
|
|
2472
2905
|
await submitNativeText(client, account.native.accountId, message.threadId, payload.text, key);
|
|
2473
2906
|
}
|
|
2474
2907
|
if (urls.length || payload.text) params.onDelivered?.();
|
|
@@ -2724,6 +3157,7 @@ async function sendWeChatText(cfg, to, text3) {
|
|
|
2724
3157
|
if (!account?.serverUrl) throw new Error("No native daemon configured");
|
|
2725
3158
|
return sendNativeText(account, to, text3);
|
|
2726
3159
|
}
|
|
3160
|
+
var mediaHandlers = createWeChatMediaHandlers();
|
|
2727
3161
|
var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
2728
3162
|
id: "agent-wechat",
|
|
2729
3163
|
capabilities: {
|
|
@@ -2736,10 +3170,7 @@ var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
|
2736
3170
|
channel: "agent-wechat",
|
|
2737
3171
|
...await sendWeChatText(cfg, to, text3)
|
|
2738
3172
|
}),
|
|
2739
|
-
|
|
2740
|
-
channel: "agent-wechat",
|
|
2741
|
-
...await sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice)
|
|
2742
|
-
})
|
|
3173
|
+
...mediaHandlers
|
|
2743
3174
|
}
|
|
2744
3175
|
});
|
|
2745
3176
|
var meta = {
|
|
@@ -2763,6 +3194,7 @@ var wechatPlugin = {
|
|
|
2763
3194
|
tts: { voice: { synthesisTarget: "voice-note", transcodesAudio: true } }
|
|
2764
3195
|
},
|
|
2765
3196
|
reload: { configPrefixes: ["channels.agent-wechat"] },
|
|
3197
|
+
actions: wechatMediaActions,
|
|
2766
3198
|
configSchema: {
|
|
2767
3199
|
uiHints: { "native.token": { sensitive: true } },
|
|
2768
3200
|
schema: {
|
|
@@ -2884,10 +3316,7 @@ var wechatPlugin = {
|
|
|
2884
3316
|
channel: "agent-wechat",
|
|
2885
3317
|
...await sendWeChatText(cfg, to, text3)
|
|
2886
3318
|
}),
|
|
2887
|
-
|
|
2888
|
-
channel: "agent-wechat",
|
|
2889
|
-
...await sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice)
|
|
2890
|
-
})
|
|
3319
|
+
...mediaHandlers
|
|
2891
3320
|
},
|
|
2892
3321
|
message: wechatMessageAdapter,
|
|
2893
3322
|
gateway: {
|
|
@@ -3054,7 +3483,7 @@ function createNativeFacade(config, dependencies = {}) {
|
|
|
3054
3483
|
fail(res, 403, "facade_account_forbidden");
|
|
3055
3484
|
return true;
|
|
3056
3485
|
}
|
|
3057
|
-
if (!["GET", "HEAD", "POST", "PATCH", "DELETE"].includes(req.method ?? "")) {
|
|
3486
|
+
if (!["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"].includes(req.method ?? "")) {
|
|
3058
3487
|
fail(res, 405, "method_not_allowed");
|
|
3059
3488
|
return true;
|
|
3060
3489
|
}
|
|
@@ -3067,19 +3496,23 @@ function createNativeFacade(config, dependencies = {}) {
|
|
|
3067
3496
|
try {
|
|
3068
3497
|
const host2 = dependencies.endpoint ? void 0 : await ensureNativeHost(config);
|
|
3069
3498
|
const headers = new Headers();
|
|
3070
|
-
for (const
|
|
3071
|
-
const value = req.headers[
|
|
3072
|
-
if (typeof value === "string") headers.set(
|
|
3499
|
+
for (const name3 of ["content-type", "accept", "range", "if-none-match", "if-range", "last-event-id", "idempotency-key"]) {
|
|
3500
|
+
const value = req.headers[name3];
|
|
3501
|
+
if (typeof value === "string") headers.set(name3, value);
|
|
3073
3502
|
}
|
|
3503
|
+
const upload = pieces.length === 4 && pieces[3] === "uploads" && req.method === "POST";
|
|
3504
|
+
const mediaContent = pieces.length === 6 && pieces[3] === "media" && pieces[5] === "content" && parsed.searchParams.get("representation") !== "preview";
|
|
3505
|
+
const requestLimit = upload ? NATIVE_MAX_SOURCE_BYTES + 64 * 1024 : MAX_BODY;
|
|
3506
|
+
const responseLimit = mediaContent ? Math.max(MAX_BODY, NATIVE_MAX_SOURCE_BYTES) : MAX_BODY;
|
|
3074
3507
|
const length = req.headers["content-length"];
|
|
3075
|
-
if (length && (!/^\d+$/.test(length) || Number(length) >
|
|
3508
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > requestLimit)) {
|
|
3076
3509
|
fail(res, 413, "payload_too_large");
|
|
3077
3510
|
return true;
|
|
3078
3511
|
}
|
|
3079
3512
|
let size = 0;
|
|
3080
3513
|
const body = ["GET", "HEAD"].includes(req.method ?? "") ? void 0 : Readable.toWeb(req).pipeThrough(new NodeTransformStream({ transform(chunk, controller) {
|
|
3081
3514
|
size += chunk.byteLength;
|
|
3082
|
-
if (size >
|
|
3515
|
+
if (size > requestLimit) {
|
|
3083
3516
|
tooLarge = true;
|
|
3084
3517
|
throw new Error("payload_too_large");
|
|
3085
3518
|
}
|
|
@@ -3092,10 +3525,16 @@ function createNativeFacade(config, dependencies = {}) {
|
|
|
3092
3525
|
headers.set("Authorization", `Bearer ${dependencies.token}`);
|
|
3093
3526
|
response = await (dependencies.fetch ?? fetch)(new URL(relative, dependencies.endpoint), init);
|
|
3094
3527
|
}
|
|
3528
|
+
const responseLength = response.headers.get("content-length");
|
|
3529
|
+
if (responseLength && (!/^\d+$/.test(responseLength) || Number(responseLength) > responseLimit)) {
|
|
3530
|
+
await response.body?.cancel();
|
|
3531
|
+
fail(res, 502, "native_facade_unavailable");
|
|
3532
|
+
return true;
|
|
3533
|
+
}
|
|
3095
3534
|
res.statusCode = response.status;
|
|
3096
|
-
for (const
|
|
3097
|
-
const value = response.headers.get(
|
|
3098
|
-
if (value) res.setHeader(
|
|
3535
|
+
for (const name3 of ["content-type", "content-length", "content-range", "accept-ranges", "etag", "cache-control", "content-disposition", "x-content-type-options"]) {
|
|
3536
|
+
const value = response.headers.get(name3);
|
|
3537
|
+
if (value) res.setHeader(name3, value);
|
|
3099
3538
|
}
|
|
3100
3539
|
res.setHeader("x-content-type-options", "nosniff");
|
|
3101
3540
|
const reader = response.body?.getReader();
|
|
@@ -3110,7 +3549,7 @@ function createNativeFacade(config, dependencies = {}) {
|
|
|
3110
3549
|
const { value, done } = await reader.read();
|
|
3111
3550
|
if (done) break;
|
|
3112
3551
|
emitted += value.length;
|
|
3113
|
-
if (!sse && emitted >
|
|
3552
|
+
if (!sse && emitted > responseLimit) throw new Error("upstream_response_too_large");
|
|
3114
3553
|
if (!res.write(value)) await drain(res, abort.signal);
|
|
3115
3554
|
}
|
|
3116
3555
|
} finally {
|
|
@@ -3154,8 +3593,8 @@ function object(value) {
|
|
|
3154
3593
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw Error("Expected tool arguments");
|
|
3155
3594
|
return value;
|
|
3156
3595
|
}
|
|
3157
|
-
function string(value,
|
|
3158
|
-
if (typeof value !== "string" || !value.trim() || value.length > max || /[\u0000-\u001f\u007f]/.test(value)) throw Error(`Invalid ${
|
|
3596
|
+
function string(value, name3, max = 4096) {
|
|
3597
|
+
if (typeof value !== "string" || !value.trim() || value.length > max || /[\u0000-\u001f\u007f]/.test(value)) throw Error(`Invalid ${name3}`);
|
|
3159
3598
|
return value.trim();
|
|
3160
3599
|
}
|
|
3161
3600
|
function threadId(value) {
|
|
@@ -3227,9 +3666,9 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3227
3666
|
if (!currentThread) return [];
|
|
3228
3667
|
}
|
|
3229
3668
|
const selectedId = selected.native.accountId;
|
|
3230
|
-
const tool = (
|
|
3231
|
-
name:
|
|
3232
|
-
label:
|
|
3669
|
+
const tool = (name3, action, description, properties, required = []) => ({
|
|
3670
|
+
name: name3,
|
|
3671
|
+
label: name3.replaceAll("_", " "),
|
|
3233
3672
|
description,
|
|
3234
3673
|
// The pinned host's tool_call wrapper JSON-serializes image blocks. Its
|
|
3235
3674
|
// public direct-only surface preserves actual image content for vision.
|
|
@@ -3530,7 +3969,7 @@ function registerNativeShareTools(api) {
|
|
|
3530
3969
|
}
|
|
3531
3970
|
|
|
3532
3971
|
// src/native-recall-tools.ts
|
|
3533
|
-
import { createHash as
|
|
3972
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
3534
3973
|
import { resolveStableChannelMessageIngress as resolveStableChannelMessageIngress4 } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
3535
3974
|
var names2 = ["wechat_recall"];
|
|
3536
3975
|
function text2(value, label) {
|
|
@@ -3541,7 +3980,7 @@ function thread2(value) {
|
|
|
3541
3980
|
return text2(text2(value, "conversation").replace(/^(?:agent-)?wechat:(?:group:)?/i, ""), "conversation");
|
|
3542
3981
|
}
|
|
3543
3982
|
function nativeRecallKey(accountId, threadId2, messageId, serverId) {
|
|
3544
|
-
return `openclaw-recall-${
|
|
3983
|
+
return `openclaw-recall-${createHash10("sha256").update(JSON.stringify([accountId, threadId2, messageId, serverId])).digest("hex")}`;
|
|
3545
3984
|
}
|
|
3546
3985
|
function createNativeRecallTools(context, dependencies = {}) {
|
|
3547
3986
|
const currentConfig = () => context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
@@ -3664,6 +4103,55 @@ function registerNativeRecallTools(api) {
|
|
|
3664
4103
|
api.registerTool({ contextVersion: 2, create: (context) => createNativeRecallTools(context) }, { names: names2 });
|
|
3665
4104
|
}
|
|
3666
4105
|
|
|
4106
|
+
// src/native-group-tool.ts
|
|
4107
|
+
var name2 = "wechat_group_info";
|
|
4108
|
+
function createNativeGroupTools(context, dependencies = {}) {
|
|
4109
|
+
if (context.senderIsOwner !== true || typeof context.assertInvocationCurrent !== "function") return [];
|
|
4110
|
+
const currentConfig = () => context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
4111
|
+
const selected = resolveWeChatAccount(currentConfig());
|
|
4112
|
+
if (!selected?.enabled || !selected.native || context.agentAccountId && context.agentAccountId !== selected.accountId || context.deliveryContext?.channel === "agent-wechat" && context.deliveryContext.accountId && context.deliveryContext.accountId !== selected.accountId) return [];
|
|
4113
|
+
const accountId = selected.native.accountId;
|
|
4114
|
+
const configurationKey = nativeConfigurationKey(selected.native);
|
|
4115
|
+
return [{
|
|
4116
|
+
name: name2,
|
|
4117
|
+
label: "WeChat Group Info",
|
|
4118
|
+
description: "Read cached group information and one page of members for the configured WeChat account. Available only to the trusted OpenClaw owner. Does not create groups, invite people or change settings. Cached member absence does not prove non-membership.",
|
|
4119
|
+
parameters: { type: "object", additionalProperties: false, properties: {
|
|
4120
|
+
groupId: { type: "string", minLength: 10, maxLength: 256, description: "Exact ordinary group username ending in @chatroom." },
|
|
4121
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
4122
|
+
cursor: { type: "string", maxLength: 8192 },
|
|
4123
|
+
username: { type: "string", minLength: 1, maxLength: 256, description: "Optionally inspect one exact member instead of listing a page." }
|
|
4124
|
+
}, required: ["groupId"] },
|
|
4125
|
+
execute: async (_callId, input, signal) => {
|
|
4126
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).some((k) => !["groupId", "limit", "cursor", "username"].includes(k))) throw Error("Invalid group info request");
|
|
4127
|
+
const args = input;
|
|
4128
|
+
const identity = (value) => typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\s\u0000-\u001f\u007f]/u.test(value);
|
|
4129
|
+
if (!identity(args.groupId) || !args.groupId.endsWith("@chatroom") || args.groupId.length <= 9 || args.limit != null && (!Number.isInteger(args.limit) || Number(args.limit) < 1 || Number(args.limit) > 100) || args.cursor != null && (typeof args.cursor !== "string" || args.cursor.length > 8192) || args.username != null && !identity(args.username) || args.username != null && (args.limit != null || args.cursor != null)) throw Error("Invalid group info selectors");
|
|
4130
|
+
const guard = () => {
|
|
4131
|
+
signal?.throwIfAborted();
|
|
4132
|
+
if (context.senderIsOwner !== true) throw Error("OpenClaw owner authorization is required");
|
|
4133
|
+
context.assertInvocationCurrent();
|
|
4134
|
+
const account = resolveWeChatAccount(currentConfig());
|
|
4135
|
+
if (!account?.enabled || !account.native || account.accountId !== selected.accountId || account.native.accountId !== accountId || nativeConfigurationKey(account.native) !== configurationKey) throw Error("WeChat account configuration changed; start a new turn");
|
|
4136
|
+
return account.native;
|
|
4137
|
+
};
|
|
4138
|
+
const native = guard();
|
|
4139
|
+
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
4140
|
+
guard();
|
|
4141
|
+
const group = await client.group(accountId, args.groupId, signal);
|
|
4142
|
+
guard();
|
|
4143
|
+
if (group.id !== args.groupId || group.roomId !== args.groupId) throw Error("Group identity is invalid");
|
|
4144
|
+
const members = typeof args.username === "string" ? await client.groupMember(accountId, args.groupId, args.username, signal) : await client.groupMembers(accountId, args.groupId, { limit: args.limit == null ? 50 : Number(args.limit), cursor: args.cursor }, signal);
|
|
4145
|
+
guard();
|
|
4146
|
+
const details = { accountId, group, members, source: "cached", membershipAbsence: "unconfirmed" };
|
|
4147
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
4148
|
+
}
|
|
4149
|
+
}];
|
|
4150
|
+
}
|
|
4151
|
+
function registerNativeGroupTools(api) {
|
|
4152
|
+
api.registerTool({ contextVersion: 2, create: (context) => createNativeGroupTools(context) }, { names: [name2] });
|
|
4153
|
+
}
|
|
4154
|
+
|
|
3667
4155
|
// index.ts
|
|
3668
4156
|
var plugin = {
|
|
3669
4157
|
id: "agent-wechat",
|
|
@@ -3677,6 +4165,7 @@ var plugin = {
|
|
|
3677
4165
|
registerNativeHistoryTools(api);
|
|
3678
4166
|
registerNativeShareTools(api);
|
|
3679
4167
|
registerNativeRecallTools(api);
|
|
4168
|
+
registerNativeGroupTools(api);
|
|
3680
4169
|
const stopFacade = selected?.native ? registerNativeFacade(api, selected.native) : () => {
|
|
3681
4170
|
};
|
|
3682
4171
|
const stopPreparedBoundary = selected?.native?.readPolicy && selected.native.readPolicy !== "on_inbound" ? registerNativePreparedBoundary(api) : () => {
|