@agent-wechat/agent-wechat 1.0.0-beta.11 → 1.0.0-beta.13
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 +38 -0
- package/dist/index.js +848 -322
- 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);
|
|
349
388
|
}
|
|
350
|
-
|
|
351
|
-
return this.
|
|
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);
|
|
403
|
+
}
|
|
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,15 +449,9 @@ 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
|
-
/** Resolve only the stored original in this message's own conversation. */
|
|
397
|
-
async quotedMessage(accountId, messageId) {
|
|
398
|
-
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))
|
|
400
|
-
throw new Error("Invalid native quote identity");
|
|
401
|
-
return quote;
|
|
402
|
-
}
|
|
403
455
|
/** Account-wide local search, with the daemon's query-bound opaque cursor. */
|
|
404
456
|
async searchMessages(accountId, options) {
|
|
405
457
|
const result = await this.request(`v1/accounts/${id(accountId)}/messages${listQuery(options)}`);
|
|
@@ -408,13 +460,20 @@ var NativeWeChatClient = class {
|
|
|
408
460
|
continue;
|
|
409
461
|
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
462
|
throw new Error("Invalid native archive message identity");
|
|
463
|
+
validateMessageRecall(message);
|
|
411
464
|
}
|
|
412
465
|
return result;
|
|
413
466
|
}
|
|
414
|
-
|
|
467
|
+
/** Quote a locally stored text, image, voice, video, link, file or quoted reply in the same chat.
|
|
468
|
+
* Live acceptance and recipient rendering remain unverified; other source kinds are rejected.
|
|
469
|
+
*/
|
|
470
|
+
sendText(accountId, threadId2, text3, idempotencyKey, quote) {
|
|
415
471
|
if (!idempotencyKey || /[\r\n]/.test(idempotencyKey))
|
|
416
472
|
throw new Error("An idempotency key is required");
|
|
417
|
-
|
|
473
|
+
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))
|
|
474
|
+
throw new Error("Invalid quote message identity");
|
|
475
|
+
const input = quote === void 0 ? { threadId: threadId2, text: text3 } : { kind: "text", threadId: threadId2, text: text3, quote };
|
|
476
|
+
return this.request(`v1/accounts/${id(accountId)}/sends`, { method: "POST", headers: { "Idempotency-Key": idempotencyKey }, body: JSON.stringify(input) });
|
|
418
477
|
}
|
|
419
478
|
send(accountId, sendId, signal) {
|
|
420
479
|
return this.request(`v1/accounts/${id(accountId)}/sends/${id(sendId)}`, { signal });
|
|
@@ -432,12 +491,16 @@ var NativeWeChatClient = class {
|
|
|
432
491
|
cancelSend(accountId, sendId) {
|
|
433
492
|
return this.request(`v1/accounts/${id(accountId)}/sends/${id(sendId)}/cancel`, { method: "POST", body: "{}" });
|
|
434
493
|
}
|
|
435
|
-
|
|
436
|
-
|
|
494
|
+
/** A disk-backed Blob avoids holding the source in memory before staging. */
|
|
495
|
+
async upload(accountId, bytes, name3, contentType = "application/octet-stream", signal) {
|
|
496
|
+
const size = bytes instanceof Blob ? bytes.size : bytes.byteLength;
|
|
497
|
+
if (!size || size > NATIVE_MAX_SOURCE_BYTES || !name3 || name3.length > 255 || /[\r\n/\\]/.test(name3))
|
|
437
498
|
throw new Error("Invalid bounded media upload");
|
|
499
|
+
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
500
|
const form = new FormData();
|
|
439
|
-
form.set("file",
|
|
440
|
-
|
|
501
|
+
form.set("file", part, name3);
|
|
502
|
+
const response = await this.response(`v1/accounts/${id(accountId)}/uploads`, { method: "POST", body: form, signal }, false, this.mediaTimeoutMs);
|
|
503
|
+
return JSON.parse(new TextDecoder().decode(await this.readBounded(response)));
|
|
441
504
|
}
|
|
442
505
|
sendMedia(accountId, input, key) {
|
|
443
506
|
if (!key || /[\r\n]/.test(key))
|
|
@@ -459,26 +522,94 @@ var NativeWeChatClient = class {
|
|
|
459
522
|
cancelJob(accountId, jobId) {
|
|
460
523
|
return this.request(`v1/accounts/${id(accountId)}/jobs/${id(jobId)}/cancel`, { method: "POST", body: "{}" });
|
|
461
524
|
}
|
|
462
|
-
retryJob(accountId, jobId) {
|
|
463
|
-
|
|
525
|
+
retryJob(accountId, jobId, options = {}) {
|
|
526
|
+
if (!options || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some((key) => key !== "reuseCompletedUpload"))
|
|
527
|
+
throw new Error("Invalid media retry options");
|
|
528
|
+
const reuse = options.reuseCompletedUpload;
|
|
529
|
+
if (reuse !== void 0 && typeof reuse !== "boolean")
|
|
530
|
+
throw new Error("Invalid media retry options");
|
|
531
|
+
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
532
|
}
|
|
465
533
|
repairMediaWriteback(accountId, limit = 25) {
|
|
466
534
|
if (!Number.isInteger(limit) || limit < 1 || limit > 100)
|
|
467
535
|
throw new Error("Invalid media writeback limit");
|
|
468
536
|
return this.request(`v1/accounts/${id(accountId)}/media-writeback/repair`, { method: "POST", body: JSON.stringify({ limit }) });
|
|
469
537
|
}
|
|
470
|
-
|
|
538
|
+
/** Consume the bounded stream once, or cancel it; this never starts a CDN transfer. */
|
|
539
|
+
async mediaContentStream(accountId, assetId, options = {}) {
|
|
471
540
|
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))
|
|
541
|
+
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
542
|
throw new Error("Invalid bounded content request");
|
|
474
543
|
const query = options.representation === "preview" ? "?representation=preview" : "";
|
|
475
|
-
const
|
|
544
|
+
const cancellation = new AbortController();
|
|
545
|
+
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
546
|
const length = response.headers.get("content-length");
|
|
477
547
|
if (length && (!/^\d+$/.test(length) || Number(length) > max)) {
|
|
478
548
|
await response.body?.cancel();
|
|
479
549
|
throw new Error("Native media response exceeds its bound");
|
|
480
550
|
}
|
|
481
|
-
|
|
551
|
+
if (!response.body && length && Number(length) > 0)
|
|
552
|
+
throw new Error("Native media response length mismatch");
|
|
553
|
+
let activeReader;
|
|
554
|
+
const iterate = async function* () {
|
|
555
|
+
cancellation.signal.throwIfAborted();
|
|
556
|
+
options.signal?.throwIfAborted();
|
|
557
|
+
const reader = response.body?.getReader();
|
|
558
|
+
if (!reader)
|
|
559
|
+
return;
|
|
560
|
+
activeReader = reader;
|
|
561
|
+
let size = 0;
|
|
562
|
+
try {
|
|
563
|
+
while (true) {
|
|
564
|
+
cancellation.signal.throwIfAborted();
|
|
565
|
+
options.signal?.throwIfAborted();
|
|
566
|
+
const { value, done } = await reader.read();
|
|
567
|
+
cancellation.signal.throwIfAborted();
|
|
568
|
+
options.signal?.throwIfAborted();
|
|
569
|
+
if (done)
|
|
570
|
+
break;
|
|
571
|
+
size += value.byteLength;
|
|
572
|
+
if (size > max)
|
|
573
|
+
throw new Error("Native media response exceeds its bound");
|
|
574
|
+
yield value;
|
|
575
|
+
}
|
|
576
|
+
if (length && size !== Number(length))
|
|
577
|
+
throw new Error("Native media response length mismatch");
|
|
578
|
+
} finally {
|
|
579
|
+
await reader.cancel().catch(() => {
|
|
580
|
+
});
|
|
581
|
+
reader.releaseLock();
|
|
582
|
+
activeReader = void 0;
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
let claimed = false;
|
|
586
|
+
const stream = { [Symbol.asyncIterator]() {
|
|
587
|
+
if (claimed)
|
|
588
|
+
throw new Error("Native media stream was already consumed");
|
|
589
|
+
claimed = true;
|
|
590
|
+
return iterate();
|
|
591
|
+
} };
|
|
592
|
+
return { stream, cancel: async () => {
|
|
593
|
+
cancellation.abort();
|
|
594
|
+
await (activeReader ? activeReader.cancel() : response.body?.cancel())?.catch(() => {
|
|
595
|
+
});
|
|
596
|
+
}, 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 };
|
|
597
|
+
}
|
|
598
|
+
async mediaContent(accountId, assetId, options = {}) {
|
|
599
|
+
const { stream, cancel: _cancel, ...metadata } = await this.mediaContentStream(accountId, assetId, options);
|
|
600
|
+
const chunks = [];
|
|
601
|
+
let size = 0;
|
|
602
|
+
for await (const chunk of stream) {
|
|
603
|
+
chunks.push(chunk);
|
|
604
|
+
size += chunk.byteLength;
|
|
605
|
+
}
|
|
606
|
+
const bytes = new Uint8Array(size);
|
|
607
|
+
let offset = 0;
|
|
608
|
+
for (const chunk of chunks) {
|
|
609
|
+
bytes.set(chunk, offset);
|
|
610
|
+
offset += chunk.byteLength;
|
|
611
|
+
}
|
|
612
|
+
return { ...metadata, bytes };
|
|
482
613
|
}
|
|
483
614
|
markRead(accountId, threadId2, expectedVersion) {
|
|
484
615
|
if (typeof expectedVersion !== "string" || !expectedVersion || expectedVersion.length > 128)
|
|
@@ -488,7 +619,12 @@ var NativeWeChatClient = class {
|
|
|
488
619
|
events(accountId, after = "0", limit = 100) {
|
|
489
620
|
if (!Number.isInteger(limit) || limit < 1 || limit > 200)
|
|
490
621
|
throw new Error("Invalid event page limit");
|
|
491
|
-
return this.request(`v1/accounts/${id(accountId)}/events?after=${decimal(after)}&limit=${limit}`)
|
|
622
|
+
return this.request(`v1/accounts/${id(accountId)}/events?after=${decimal(after)}&limit=${limit}`).then((page) => {
|
|
623
|
+
for (const event of page.items)
|
|
624
|
+
if (event.kind === "message.recalled")
|
|
625
|
+
validateNativeRecallMetadata(event.data);
|
|
626
|
+
return page;
|
|
627
|
+
});
|
|
492
628
|
}
|
|
493
629
|
ack(accountId, consumerId, cursor) {
|
|
494
630
|
return this.request(`v1/accounts/${id(accountId)}/consumers/${id(consumerId)}/ack`, { method: "POST", body: JSON.stringify({ cursor: decimal(cursor) }) });
|
|
@@ -552,6 +688,8 @@ var NativeWeChatClient = class {
|
|
|
552
688
|
const event = payload;
|
|
553
689
|
if (!event || typeof event.kind !== "string" || !event.data || typeof event.data !== "object")
|
|
554
690
|
throw new Error("Invalid native event");
|
|
691
|
+
if (event.kind === "message.recalled")
|
|
692
|
+
validateNativeRecallMetadata(event.data);
|
|
555
693
|
event.cursor = decimal(event.cursor ?? eventCursor);
|
|
556
694
|
yield event;
|
|
557
695
|
}
|
|
@@ -598,7 +736,7 @@ function requireNativeIdentity(value) {
|
|
|
598
736
|
}
|
|
599
737
|
}
|
|
600
738
|
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((
|
|
739
|
+
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
740
|
}
|
|
603
741
|
function nativeHostTarget(platform = process.platform, arch = process.arch) {
|
|
604
742
|
const cpu = arch === "arm64" ? "aarch64" : "x86_64";
|
|
@@ -1034,15 +1172,21 @@ function resolveWeChatIngress(params) {
|
|
|
1034
1172
|
});
|
|
1035
1173
|
}
|
|
1036
1174
|
|
|
1175
|
+
// src/outbound-options.ts
|
|
1176
|
+
import { createHash as createHash6 } from "node:crypto";
|
|
1177
|
+
|
|
1037
1178
|
// src/outbound-media.ts
|
|
1038
1179
|
import { createHash as createHash5 } from "node:crypto";
|
|
1039
1180
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1040
1181
|
|
|
1041
1182
|
// src/native-media.ts
|
|
1042
1183
|
import { createHash as createHash4, randomUUID } from "node:crypto";
|
|
1043
|
-
import {
|
|
1044
|
-
import {
|
|
1184
|
+
import { openAsBlob } from "node:fs";
|
|
1185
|
+
import { mkdtemp, open, rm } from "node:fs/promises";
|
|
1186
|
+
import { tmpdir } from "node:os";
|
|
1187
|
+
import { basename, extname, join as join2 } from "node:path";
|
|
1045
1188
|
import { getImageMetadata, resizeToJpeg, IMAGE_REDUCE_QUALITY_STEPS } from "openclaw/plugin-sdk/media-runtime";
|
|
1189
|
+
import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
|
|
1046
1190
|
|
|
1047
1191
|
// src/attachment-store.ts
|
|
1048
1192
|
import { createHash as createHash3 } from "node:crypto";
|
|
@@ -1062,6 +1206,12 @@ async function saveManagedAttachment(params) {
|
|
|
1062
1206
|
}
|
|
1063
1207
|
const buffer = Buffer.from(params.data, "base64");
|
|
1064
1208
|
if (buffer.byteLength !== size) throw new Error("Attachment decoding length mismatch");
|
|
1209
|
+
return saveManagedAttachmentBytes({ ...params, bytes: buffer });
|
|
1210
|
+
}
|
|
1211
|
+
async function saveManagedAttachmentBytes(params) {
|
|
1212
|
+
const size = params.bytes.byteLength;
|
|
1213
|
+
if (size > params.maxBytes) throw new AttachmentTooLargeError(`Attachment exceeds configured ${params.maxBytes} byte limit`);
|
|
1214
|
+
const buffer = Buffer.from(params.bytes.buffer, params.bytes.byteOffset, size);
|
|
1065
1215
|
const sha256 = createHash3("sha256").update(buffer).digest("hex");
|
|
1066
1216
|
const saved = await params.saveMediaBuffer(
|
|
1067
1217
|
buffer,
|
|
@@ -1072,31 +1222,59 @@ async function saveManagedAttachment(params) {
|
|
|
1072
1222
|
);
|
|
1073
1223
|
return { path: saved.path, size, sha256, filename: params.filename };
|
|
1074
1224
|
}
|
|
1225
|
+
async function saveVerifiedManagedAttachmentStream(params) {
|
|
1226
|
+
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");
|
|
1227
|
+
let size = 0, finished = false;
|
|
1228
|
+
const digest2 = createHash3("sha256");
|
|
1229
|
+
const stream = (async function* () {
|
|
1230
|
+
for await (const chunk of params.stream) {
|
|
1231
|
+
params.assertCurrent();
|
|
1232
|
+
size += chunk.byteLength;
|
|
1233
|
+
if (size > params.maxBytes || size > params.expectedBytes) throw new AttachmentTooLargeError("Native attachment stream exceeds its bound");
|
|
1234
|
+
digest2.update(chunk);
|
|
1235
|
+
yield chunk;
|
|
1236
|
+
}
|
|
1237
|
+
params.assertCurrent();
|
|
1238
|
+
if (size !== params.expectedBytes || digest2.digest("hex") !== params.expectedSha256) throw new Error("Native attachment integrity did not verify");
|
|
1239
|
+
finished = true;
|
|
1240
|
+
})();
|
|
1241
|
+
const saved = await params.saveMediaStream(
|
|
1242
|
+
stream,
|
|
1243
|
+
params.mime,
|
|
1244
|
+
"inbound",
|
|
1245
|
+
params.maxBytes,
|
|
1246
|
+
params.filename,
|
|
1247
|
+
void 0,
|
|
1248
|
+
{ assertCommitAllowed: params.assertCurrent, durable: true }
|
|
1249
|
+
);
|
|
1250
|
+
if (!finished || saved.size != null && saved.size !== size) throw new Error("Native attachment stream was not fully saved");
|
|
1251
|
+
return { path: saved.path, size, sha256: params.expectedSha256, filename: params.filename };
|
|
1252
|
+
}
|
|
1075
1253
|
|
|
1076
1254
|
// src/media-delivery.ts
|
|
1077
1255
|
function appendReference(body, reference) {
|
|
1078
1256
|
return body ? `${body}
|
|
1079
1257
|
${reference}` : reference;
|
|
1080
1258
|
}
|
|
1081
|
-
function safeDisplayName(
|
|
1082
|
-
return
|
|
1259
|
+
function safeDisplayName(name3) {
|
|
1260
|
+
return name3.replace(/[\r\n\0]/g, " ").trim() || "attachment";
|
|
1083
1261
|
}
|
|
1084
1262
|
function renderAttachmentBody(originalBody, attachment, exposeNonVisualPath = true) {
|
|
1085
1263
|
if (!attachment) return originalBody;
|
|
1086
|
-
const
|
|
1264
|
+
const name3 = safeDisplayName(attachment.filename);
|
|
1087
1265
|
if (attachment.status !== "ready" || !attachment.path) {
|
|
1088
1266
|
const label = attachment.kind === "file" ? "File" : attachment.kind === "image" ? "Image" : attachment.kind === "audio" ? "Voice message" : "Video";
|
|
1089
1267
|
if (attachment.status === "pending") {
|
|
1090
|
-
return appendReference(originalBody, `[${label} pending: ${
|
|
1268
|
+
return appendReference(originalBody, `[${label} pending: ${name3}]`);
|
|
1091
1269
|
}
|
|
1092
1270
|
if (attachment.status === "expired") {
|
|
1093
|
-
return appendReference(originalBody, `[${label} expired: ${
|
|
1271
|
+
return appendReference(originalBody, `[${label} expired: ${name3}; no cached copy available. Ask the sender to resend.]`);
|
|
1094
1272
|
}
|
|
1095
|
-
return appendReference(originalBody, `[${label} unavailable: ${
|
|
1273
|
+
return appendReference(originalBody, `[${label} unavailable: ${name3} (${attachment.status})]`);
|
|
1096
1274
|
}
|
|
1097
1275
|
if (attachment.kind === "file") {
|
|
1098
|
-
const prefix = originalBody.trim() ===
|
|
1099
|
-
return appendReference(prefix, `[File: ${
|
|
1276
|
+
const prefix = originalBody.trim() === name3 ? "" : originalBody;
|
|
1277
|
+
return appendReference(prefix, `[File: ${name3}]
|
|
1100
1278
|
[Local file: <${attachment.path}>]`);
|
|
1101
1279
|
}
|
|
1102
1280
|
if (exposeNonVisualPath) {
|
|
@@ -1150,13 +1328,13 @@ async function nativeOutboundDelivery(client, accountId, threadId2, messageIds,
|
|
|
1150
1328
|
async function submitNativeText(client, accountId, threadId2, text3, key, options = {}) {
|
|
1151
1329
|
let intent = await client.sendText(accountId, threadId2, text3, key);
|
|
1152
1330
|
const deadline = Date.now() + Math.max(1, Math.min(options.timeoutMs ?? 65e3, 65e3));
|
|
1153
|
-
let
|
|
1331
|
+
let delay2 = Math.max(1, Math.min(options.initialDelayMs ?? 250, 1e3));
|
|
1154
1332
|
while (["queued", "submitting"].includes(intent.state) && Date.now() < deadline) {
|
|
1155
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(
|
|
1333
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay2, deadline - Date.now())));
|
|
1156
1334
|
const remaining = deadline - Date.now();
|
|
1157
1335
|
if (remaining <= 0) break;
|
|
1158
1336
|
intent = await client.send(accountId, intent.id, AbortSignal.timeout(remaining));
|
|
1159
|
-
|
|
1337
|
+
delay2 = Math.min(delay2 * 2, 1e3);
|
|
1160
1338
|
}
|
|
1161
1339
|
if (!["accepted", "sent", "server_accepted"].includes(intent.state)) throw new Error(`Native send ${intent.id} requires inspection (${intent.state}); do not automatically resend`);
|
|
1162
1340
|
return intent;
|
|
@@ -1164,15 +1342,33 @@ async function submitNativeText(client, accountId, threadId2, text3, key, option
|
|
|
1164
1342
|
|
|
1165
1343
|
// src/native-recall-state.ts
|
|
1166
1344
|
function nativeRecallInfo(message) {
|
|
1345
|
+
if (message.recall) validateNativeRecallMetadata(message.recall);
|
|
1167
1346
|
return message.recall;
|
|
1168
1347
|
}
|
|
1169
1348
|
function nativeRecallIdentity(threadId2, serverId) {
|
|
1170
1349
|
return JSON.stringify([threadId2, serverId]);
|
|
1171
1350
|
}
|
|
1172
1351
|
function validateNativeRecall(value) {
|
|
1352
|
+
validateNativeRecallMetadata(value);
|
|
1173
1353
|
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
1354
|
}
|
|
1355
|
+
function mergeNativeRecall(prior, next) {
|
|
1356
|
+
validateNativeRecall(prior);
|
|
1357
|
+
validateNativeRecall(next);
|
|
1358
|
+
if (prior.threadId !== next.threadId || prior.serverId !== next.serverId) throw Error("Conflicting native recall identity; cursor retained");
|
|
1359
|
+
return {
|
|
1360
|
+
threadId: prior.threadId,
|
|
1361
|
+
serverId: prior.serverId,
|
|
1362
|
+
observedAt: prior.observedAt,
|
|
1363
|
+
...prior.messageId ?? next.messageId ? { messageId: prior.messageId ?? next.messageId } : {},
|
|
1364
|
+
...prior.recalledAt ?? next.recalledAt ? { recalledAt: prior.recalledAt ?? next.recalledAt } : {},
|
|
1365
|
+
...(prior.replacementText ?? next.replacementText) != null ? { replacementText: prior.replacementText ?? next.replacementText } : {}
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1175
1368
|
function recalledNativeMessage(message, recall) {
|
|
1369
|
+
validateNativeRecall(recall);
|
|
1370
|
+
const previous = nativeRecallInfo(message);
|
|
1371
|
+
const replacementText = previous?.replacementText ?? recall.replacementText;
|
|
1176
1372
|
return {
|
|
1177
1373
|
...message,
|
|
1178
1374
|
type: 1e4,
|
|
@@ -1180,8 +1376,42 @@ function recalledNativeMessage(message, recall) {
|
|
|
1180
1376
|
quote: void 0,
|
|
1181
1377
|
link: void 0,
|
|
1182
1378
|
media: [],
|
|
1379
|
+
senderId: message.senderId || null,
|
|
1380
|
+
direction: message.senderId ? message.direction : null,
|
|
1183
1381
|
mentions: { state: "unknown", userIds: [], self: null, all: null, source: null },
|
|
1184
|
-
recall: {
|
|
1382
|
+
recall: {
|
|
1383
|
+
observedAt: previous?.observedAt ?? recall.observedAt,
|
|
1384
|
+
...previous?.recalledAt ?? recall.recalledAt ? { recalledAt: previous?.recalledAt ?? recall.recalledAt } : {},
|
|
1385
|
+
...replacementText != null ? { replacementText } : {}
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/native-quote.ts
|
|
1391
|
+
function renderEmbeddedQuote(message, permitted = true, imageBody) {
|
|
1392
|
+
const comment = (message.body ?? "").slice(0, 16384);
|
|
1393
|
+
const source = message.quote;
|
|
1394
|
+
const unavailable = (state2) => ({
|
|
1395
|
+
body: `${comment}${comment ? "\n\n" : ""}[Quoted message ${state2}]`,
|
|
1396
|
+
image: false,
|
|
1397
|
+
quote: { state: state2, type: source?.type, senderId: source?.senderId ?? void 0 }
|
|
1398
|
+
});
|
|
1399
|
+
if (!source) return unavailable("unavailable");
|
|
1400
|
+
if (!permitted) return unavailable("not permitted");
|
|
1401
|
+
const state = source.state ?? "missing";
|
|
1402
|
+
if (state === "recalled") return unavailable("recalled");
|
|
1403
|
+
if (!["embedded", "missing", "unsupported"].includes(state) || source.embeddedText != null && (typeof source.embeddedText !== "string" || source.embeddedText.length > 16384) || source.embeddedImage != null && typeof source.embeddedImage !== "boolean") return unavailable("invalid");
|
|
1404
|
+
const label = source.type === 1 ? "text" : source.type === 3 ? "image" : source.type === 34 ? "voice" : source.type === 43 ? "video" : source.type === 49 && source.appMessageType === 5 ? "link" : source.type === 49 && source.appMessageType === 6 ? "file" : "message";
|
|
1405
|
+
const image = state === "embedded" && source.type === 3 && source.embeddedImage === true;
|
|
1406
|
+
if (state === "embedded" && !image && !source.embeddedText) return unavailable("invalid");
|
|
1407
|
+
const content = state === "missing" ? "" : image ? imageBody ?? "[Image metadata embedded in quote; image not retrieved]" : source.embeddedText ?? "";
|
|
1408
|
+
const status = state === "unsupported" ? `[${label} attachment unavailable from embedded quote]` : state === "missing" ? "[Embedded quote content unavailable]" : "";
|
|
1409
|
+
return {
|
|
1410
|
+
body: `${comment}${comment ? "\n\n" : ""}[Quoted ${label} from ${source.senderId ?? "unknown sender"}; embedded in reply]
|
|
1411
|
+
${[content, status].filter(Boolean).join("\n")}
|
|
1412
|
+
[/Quoted message]`,
|
|
1413
|
+
image,
|
|
1414
|
+
quote: { state, type: source.type, senderId: source.senderId ?? void 0 }
|
|
1185
1415
|
};
|
|
1186
1416
|
}
|
|
1187
1417
|
|
|
@@ -1192,6 +1422,26 @@ function kind(type) {
|
|
|
1192
1422
|
var terminalJobs = /* @__PURE__ */ new Set(["accepted", "completed", "succeeded", "available", "failed", "partial", "cancelled", "expired", "needs_review"]);
|
|
1193
1423
|
var successfulSendJobs = /* @__PURE__ */ new Set(["accepted", "completed", "succeeded"]);
|
|
1194
1424
|
var terminalIdentityErrors = /* @__PURE__ */ new Set(["media_identity_unavailable", "media_source_changed"]);
|
|
1425
|
+
var NativeMediaJobInspectionError = class extends Error {
|
|
1426
|
+
code = "native_media_job_inspection_required";
|
|
1427
|
+
retryable = false;
|
|
1428
|
+
jobId;
|
|
1429
|
+
accountId;
|
|
1430
|
+
threadId;
|
|
1431
|
+
idempotencyKey;
|
|
1432
|
+
state;
|
|
1433
|
+
reason;
|
|
1434
|
+
constructor(job, accountId, threadId2, key, reason) {
|
|
1435
|
+
super(`Native media job ${job.id} requires inspection (${job.state}); retain the original job and idempotency key and do not automatically resend`);
|
|
1436
|
+
this.name = "NativeMediaJobInspectionError";
|
|
1437
|
+
this.jobId = job.id;
|
|
1438
|
+
this.accountId = accountId;
|
|
1439
|
+
this.threadId = threadId2;
|
|
1440
|
+
this.idempotencyKey = key;
|
|
1441
|
+
this.state = job.state;
|
|
1442
|
+
this.reason = reason;
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1195
1445
|
function mediaSendResult(job) {
|
|
1196
1446
|
return job.result && typeof job.result === "object" && !Array.isArray(job.result) ? job.result : {};
|
|
1197
1447
|
}
|
|
@@ -1207,11 +1457,11 @@ function mediaSendReceiptIds(job, audioAsVoice) {
|
|
|
1207
1457
|
async function waitNativeMediaSendReceipts(client, accountId, threadId2, initial, audioAsVoice, timeoutMs = 1e4) {
|
|
1208
1458
|
if (initial.accountId !== accountId || initial.threadId !== threadId2 || initial.kind !== "send") throw Error("Native media receipt job identity mismatch; inspect the submitted job and do not resend");
|
|
1209
1459
|
const deadline = Date.now() + Math.max(0, Math.min(timeoutMs, 1e4));
|
|
1210
|
-
let job = initial,
|
|
1460
|
+
let job = initial, delay2 = 100;
|
|
1211
1461
|
while (successfulSendJobs.has(job.state) && !mediaSendReceiptIds(job, audioAsVoice) && Date.now() < deadline) {
|
|
1212
1462
|
const result = mediaSendResult(job);
|
|
1213
1463
|
if (["unknown", "not_applicable"].includes(String(result.profileProjection)) || audioAsVoice && Array.isArray(result.chunks) && result.chunks.some((chunk) => !chunk || chunk.state !== "accepted")) break;
|
|
1214
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(
|
|
1464
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay2, Math.max(0, deadline - Date.now()))));
|
|
1215
1465
|
const remaining = deadline - Date.now();
|
|
1216
1466
|
if (remaining <= 0) break;
|
|
1217
1467
|
let next;
|
|
@@ -1222,27 +1472,46 @@ async function waitNativeMediaSendReceipts(client, accountId, threadId2, initial
|
|
|
1222
1472
|
}
|
|
1223
1473
|
if (next.id !== initial.id || next.accountId !== accountId || next.threadId !== threadId2 || next.kind !== "send" || next.sourceId !== initial.sourceId || initial.serverMessageId != null && next.serverMessageId !== initial.serverMessageId) break;
|
|
1224
1474
|
job = next;
|
|
1225
|
-
|
|
1475
|
+
delay2 = Math.min(delay2 * 2, 1e3);
|
|
1226
1476
|
}
|
|
1227
1477
|
return job;
|
|
1228
1478
|
}
|
|
1229
1479
|
async function waitNativeMediaJob(client, accountId, initial, timeoutMs = 65e3, signal, assertCurrent) {
|
|
1480
|
+
return waitNativeMediaStatus(initial, timeoutMs, signal, assertCurrent, (job, pollSignal) => client.job(accountId, job.id, pollSignal));
|
|
1481
|
+
}
|
|
1482
|
+
async function waitNativeMediaStatus(initial, timeoutMs, signal, assertCurrent, readStatus) {
|
|
1230
1483
|
const deadline = Date.now() + timeoutMs;
|
|
1231
1484
|
let job = initial;
|
|
1232
|
-
let
|
|
1485
|
+
let delay2 = 250;
|
|
1233
1486
|
while (!terminalJobs.has(job.state) && Date.now() < deadline) {
|
|
1234
1487
|
signal?.throwIfAborted();
|
|
1235
1488
|
assertCurrent?.();
|
|
1236
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(
|
|
1489
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(delay2, Math.max(0, deadline - Date.now()))));
|
|
1237
1490
|
const remaining = deadline - Date.now();
|
|
1238
1491
|
if (remaining <= 0) break;
|
|
1239
1492
|
signal?.throwIfAborted();
|
|
1240
1493
|
assertCurrent?.();
|
|
1241
|
-
|
|
1242
|
-
|
|
1494
|
+
const next = await readStatus(job, signal ? AbortSignal.any([signal, AbortSignal.timeout(remaining)]) : AbortSignal.timeout(remaining));
|
|
1495
|
+
if (!next) break;
|
|
1496
|
+
job = next;
|
|
1497
|
+
delay2 = Math.min(delay2 * 2, 1e3);
|
|
1243
1498
|
}
|
|
1244
1499
|
return job;
|
|
1245
1500
|
}
|
|
1501
|
+
async function incomingMediaJobStatus(client, accountId, jobId, assetId, signal, assertCurrent) {
|
|
1502
|
+
let job;
|
|
1503
|
+
assertCurrent();
|
|
1504
|
+
try {
|
|
1505
|
+
job = await client.job(accountId, jobId, signal);
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
assertCurrent();
|
|
1508
|
+
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;
|
|
1509
|
+
throw error;
|
|
1510
|
+
}
|
|
1511
|
+
assertCurrent();
|
|
1512
|
+
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");
|
|
1513
|
+
return job;
|
|
1514
|
+
}
|
|
1246
1515
|
function select(assets, type, quality = "best") {
|
|
1247
1516
|
const preferred = type === 34 ? ["normal"] : type === 43 ? ["normal", "original"] : ["original", "standard", "thumbnail", "encoded"];
|
|
1248
1517
|
const rank = (asset) => {
|
|
@@ -1301,83 +1570,37 @@ ${prepared.body}`,
|
|
|
1301
1570
|
};
|
|
1302
1571
|
}
|
|
1303
1572
|
async function prepareNativeQuote(params) {
|
|
1304
|
-
const {
|
|
1305
|
-
const
|
|
1573
|
+
const { message } = params;
|
|
1574
|
+
const guard2 = () => {
|
|
1306
1575
|
params.signal?.throwIfAborted();
|
|
1307
1576
|
params.assertCurrent?.();
|
|
1308
1577
|
};
|
|
1309
|
-
|
|
1310
|
-
const
|
|
1311
|
-
const
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
return terminalIdentityErrors.has(error.code) ? { ...unavailable("unavailable"), retryable: false, reason: error.code } : unavailable("unavailable");
|
|
1322
|
-
}
|
|
1323
|
-
guard();
|
|
1324
|
-
if (resolution.accountId !== message.accountId || resolution.messageId !== message.id || resolution.threadId !== message.threadId) throw Error("Native quote identity does not match its message");
|
|
1325
|
-
if (resolution.state === "recalled" || resolution.message && nativeRecallInfo(resolution.message)) return { ...unavailable("recalled"), retryable: false, reason: "message_recalled" };
|
|
1326
|
-
if (!message.quote) return unavailable("invalid");
|
|
1327
|
-
if (resolution.state === "embedded") {
|
|
1328
|
-
if (resolution.message != null || (message.quote.type === 1 ? typeof resolution.embeddedText !== "string" || resolution.embeddedText.length > 16384 : message.quote.type !== 3 || resolution.embeddedImage !== true)) throw Error("Invalid embedded quote resolution");
|
|
1329
|
-
const author = { senderId: resolution.senderId ?? null, direction: null };
|
|
1330
|
-
if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) return unavailable("not permitted");
|
|
1331
|
-
guard();
|
|
1332
|
-
const quote2 = { state: "embedded", senderId: resolution.senderId ?? void 0, type: message.quote.type };
|
|
1333
|
-
const prefix2 = `${comment}${comment ? "\n\n" : ""}[Quoted ${message.quote.type === 3 ? "image" : "text"} from ${resolution.senderId ?? "unknown sender"}; embedded in quote, original not stored]
|
|
1334
|
-
`;
|
|
1335
|
-
if (message.quote.type === 1) return { body: `${prefix2}${resolution.embeddedText}
|
|
1336
|
-
[/Quoted message]`, pending: false, quote: quote2 };
|
|
1337
|
-
const beforeMediaAccess2 = async () => {
|
|
1338
|
-
await params.beforeMediaAccess?.();
|
|
1339
|
-
guard();
|
|
1340
|
-
if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) throw Error("Quoted message is no longer permitted");
|
|
1341
|
-
guard();
|
|
1342
|
-
};
|
|
1343
|
-
const prepared2 = await prepareNativeAttachment({ ...params, message: { ...message, type: 3, appMessageType: void 0, quote: void 0 }, beforeMediaAccess: beforeMediaAccess2 });
|
|
1344
|
-
return {
|
|
1345
|
-
...prepared2,
|
|
1346
|
-
body: `${prefix2}${prepared2.body}
|
|
1347
|
-
[/Quoted message]`,
|
|
1348
|
-
pending: false,
|
|
1349
|
-
quote: quote2,
|
|
1350
|
-
...prepared2.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {}
|
|
1351
|
-
};
|
|
1352
|
-
}
|
|
1353
|
-
if (resolution.state !== "resolved") {
|
|
1354
|
-
if (!["missing", "invalid", "unsupported", "ambiguous"].includes(resolution.state) || resolution.message != null) throw Error("Invalid native quote resolution");
|
|
1355
|
-
return unavailable(resolution.state);
|
|
1356
|
-
}
|
|
1357
|
-
const original = resolution.message;
|
|
1358
|
-
if (!original || original.accountId !== message.accountId || original.threadId !== message.threadId || original.id === message.id || original.serverId !== message.quote.serverId || original.type !== message.quote.type || ![1, 3].includes(original.type)) throw Error("Native quoted original crosses its message boundary");
|
|
1359
|
-
if (params.allowQuotedMessage && !await params.allowQuotedMessage(original)) return unavailable("not permitted");
|
|
1360
|
-
guard();
|
|
1361
|
-
const quote = { state: "resolved", messageId: original.id, senderId: original.senderId ?? void 0, type: original.type };
|
|
1362
|
-
const prefix = `${comment}${comment ? "\n\n" : ""}[Quoted ${original.type === 3 ? "image" : "text"} from ${original.senderId ?? "unknown sender"}; message ${original.id}]
|
|
1363
|
-
`;
|
|
1364
|
-
if (original.type === 1) return { body: `${prefix}${(original.body ?? "").slice(0, 16384)}
|
|
1365
|
-
[/Quoted message]`, pending: false, quote };
|
|
1578
|
+
guard2();
|
|
1579
|
+
const author = { senderId: message.quote?.senderId ?? null, direction: message.quote?.direction ?? null };
|
|
1580
|
+
const permitted = !message.quote || !params.allowQuotedMessage || await params.allowQuotedMessage(author);
|
|
1581
|
+
guard2();
|
|
1582
|
+
const rendered = renderEmbeddedQuote(message, permitted);
|
|
1583
|
+
if (!rendered.image) return {
|
|
1584
|
+
body: rendered.body,
|
|
1585
|
+
quote: rendered.quote,
|
|
1586
|
+
pending: false,
|
|
1587
|
+
retryable: false,
|
|
1588
|
+
...rendered.quote.state === "recalled" ? { reason: "message_recalled" } : {}
|
|
1589
|
+
};
|
|
1366
1590
|
const beforeMediaAccess = async () => {
|
|
1367
1591
|
await params.beforeMediaAccess?.();
|
|
1368
|
-
|
|
1369
|
-
if (params.allowQuotedMessage && !await params.allowQuotedMessage(
|
|
1370
|
-
|
|
1592
|
+
guard2();
|
|
1593
|
+
if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) throw Error("Quoted message is no longer permitted");
|
|
1594
|
+
guard2();
|
|
1371
1595
|
};
|
|
1372
|
-
const prepared = await prepareNativeAttachment({ ...params, message:
|
|
1596
|
+
const prepared = await prepareNativeAttachment({ ...params, message: { ...message, type: 3, appMessageType: void 0, quote: void 0, link: void 0 }, beforeMediaAccess });
|
|
1597
|
+
if (prepared.reason === "message_recalled" || terminalIdentityErrors.has(prepared.reason ?? "")) return prepared;
|
|
1373
1598
|
return {
|
|
1374
1599
|
...prepared,
|
|
1375
|
-
body:
|
|
1376
|
-
|
|
1600
|
+
body: renderEmbeddedQuote(message, true, prepared.body).body,
|
|
1601
|
+
quote: rendered.quote,
|
|
1377
1602
|
pending: false,
|
|
1378
|
-
|
|
1379
|
-
...prepared.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {},
|
|
1380
|
-
suppliedAssets: prepared.suppliedAssets?.map((asset) => ({ ...asset, sourceMessageId: original.id }))
|
|
1603
|
+
...prepared.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {}
|
|
1381
1604
|
};
|
|
1382
1605
|
}
|
|
1383
1606
|
async function prepareNativeAttachment(params) {
|
|
@@ -1402,16 +1625,16 @@ async function prepareNativeAttachment(params) {
|
|
|
1402
1625
|
async function prepareNativeAttachmentInput(params) {
|
|
1403
1626
|
const { client, account, message, core } = params;
|
|
1404
1627
|
const attachmentKind = kind(message.type);
|
|
1405
|
-
const
|
|
1628
|
+
const guard2 = () => {
|
|
1406
1629
|
params.signal?.throwIfAborted();
|
|
1407
1630
|
params.assertCurrent?.();
|
|
1408
1631
|
};
|
|
1409
|
-
|
|
1632
|
+
guard2();
|
|
1410
1633
|
if (nativeRecallInfo(message)) return { body: "[Message recalled]", pending: false, retryable: false, reason: "message_recalled" };
|
|
1411
1634
|
const authorize = async () => {
|
|
1412
|
-
|
|
1635
|
+
guard2();
|
|
1413
1636
|
await params.beforeMediaAccess?.();
|
|
1414
|
-
|
|
1637
|
+
guard2();
|
|
1415
1638
|
};
|
|
1416
1639
|
if (message.type === 49 && message.appMessageType === 57) return prepareNativeQuote(params);
|
|
1417
1640
|
if (message.type === 49 && message.appMessageType === 19) return { body: message.body?.slice(0, 65536) || "[Forwarded chat history unavailable]", pending: false };
|
|
@@ -1419,11 +1642,14 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1419
1642
|
if (message.type === 49 && message.appMessageType != null && message.appMessageType !== 6) return { body: `[Unsupported WeChat app message subtype ${message.appMessageType}]`, pending: false, retryable: false };
|
|
1420
1643
|
if (!attachmentKind) return { body: message.type === 1 ? message.body ?? "" : `[Unsupported WeChat message type ${message.type}]`, pending: false };
|
|
1421
1644
|
const accountId = account.native.accountId;
|
|
1422
|
-
const maxBytes = Math.min(
|
|
1645
|
+
const maxBytes = Math.min(
|
|
1646
|
+
account.mediaMaxMb * 1024 * 1024,
|
|
1647
|
+
attachmentKind === "file" ? Math.max(NATIVE_MAX_SOURCE_BYTES, 128 * 1024 * 1024) : 128 * 1024 * 1024
|
|
1648
|
+
);
|
|
1423
1649
|
const scopedAssets = async () => {
|
|
1424
1650
|
await authorize();
|
|
1425
1651
|
const items = (await client.mediaAssets(accountId, message.id)).items;
|
|
1426
|
-
|
|
1652
|
+
guard2();
|
|
1427
1653
|
if (items.some((asset2) => asset2.accountId !== accountId || asset2.messageId !== message.id)) throw Error("Native attachment identity does not match its message");
|
|
1428
1654
|
return items;
|
|
1429
1655
|
};
|
|
@@ -1445,9 +1671,9 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1445
1671
|
let transferError;
|
|
1446
1672
|
if (!asset && initialCandidate?.jobId && initialCandidate.state !== "recalled") {
|
|
1447
1673
|
await authorize();
|
|
1448
|
-
job = await client
|
|
1449
|
-
|
|
1450
|
-
if (job
|
|
1674
|
+
job = await incomingMediaJobStatus(client, accountId, initialCandidate.jobId, initialCandidate.id, params.signal, guard2);
|
|
1675
|
+
guard2();
|
|
1676
|
+
if (!job) return { body: `[${attachmentKind} pending: retrieval is unavailable]`, pending: true, jobId: initialCandidate.jobId, retryable: true };
|
|
1451
1677
|
assets = await scopedAssets();
|
|
1452
1678
|
asset = select(assets, message.type, quality);
|
|
1453
1679
|
if (!asset && transferCandidate()?.state === "recalled") return { body: "[Message recalled]", pending: false, retryable: false, reason: "message_recalled" };
|
|
@@ -1457,11 +1683,17 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1457
1683
|
await authorize();
|
|
1458
1684
|
try {
|
|
1459
1685
|
job = await client.transferMedia(accountId, { messageId: message.id, assetId: null, quality: transferQuality });
|
|
1460
|
-
|
|
1461
|
-
job = await
|
|
1462
|
-
|
|
1686
|
+
guard2();
|
|
1687
|
+
job = await waitNativeMediaStatus(
|
|
1688
|
+
job,
|
|
1689
|
+
params.waitMs ?? 2e4,
|
|
1690
|
+
params.signal,
|
|
1691
|
+
guard2,
|
|
1692
|
+
(known, pollSignal) => incomingMediaJobStatus(client, accountId, known.id, initialCandidate?.id, pollSignal, guard2)
|
|
1693
|
+
);
|
|
1694
|
+
guard2();
|
|
1463
1695
|
} catch (error) {
|
|
1464
|
-
|
|
1696
|
+
guard2();
|
|
1465
1697
|
if (!(error instanceof NativeApiError) || terminalIdentityErrors.has(error.code)) throw error;
|
|
1466
1698
|
if (error.retryable) return { body: `[${attachmentKind} pending: retrieval is unavailable]`, pending: true, jobId: job?.id, retryable: true };
|
|
1467
1699
|
transferError = error;
|
|
@@ -1491,14 +1723,63 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1491
1723
|
return { body: renderAttachmentBody("", attachment2), attachment: attachment2, pending: false };
|
|
1492
1724
|
}
|
|
1493
1725
|
await authorize();
|
|
1726
|
+
if (attachmentKind === "file" && asset.byteLength != null && asset.byteLength > 128 * 1024 * 1024) {
|
|
1727
|
+
const content2 = await client.mediaContentStream(accountId, asset.id, { maxBytes, signal: params.signal });
|
|
1728
|
+
const mime2 = content2.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
1729
|
+
if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mime2)) {
|
|
1730
|
+
await content2.cancel();
|
|
1731
|
+
throw Error("Native attachment MIME type is unsupported");
|
|
1732
|
+
}
|
|
1733
|
+
let saved2;
|
|
1734
|
+
try {
|
|
1735
|
+
saved2 = await saveVerifiedManagedAttachmentStream({
|
|
1736
|
+
stream: content2.stream,
|
|
1737
|
+
mime: mime2,
|
|
1738
|
+
filename,
|
|
1739
|
+
maxBytes,
|
|
1740
|
+
expectedBytes: asset.byteLength,
|
|
1741
|
+
expectedSha256: asset.sha256 ?? "",
|
|
1742
|
+
assertCurrent: guard2,
|
|
1743
|
+
saveMediaStream: params.saveMediaStream ?? saveMediaStream
|
|
1744
|
+
});
|
|
1745
|
+
} finally {
|
|
1746
|
+
await content2.cancel();
|
|
1747
|
+
}
|
|
1748
|
+
await authorize();
|
|
1749
|
+
const attachment2 = { kind: "file", filename, status: "ready", path: saved2.path, mime: mime2 };
|
|
1750
|
+
return {
|
|
1751
|
+
body: renderAttachmentBody("", attachment2),
|
|
1752
|
+
attachment: attachment2,
|
|
1753
|
+
mediaMime: mime2,
|
|
1754
|
+
pending: false,
|
|
1755
|
+
jobId: job?.id,
|
|
1756
|
+
retryable: false,
|
|
1757
|
+
variant: asset.actualVariant ?? asset.variant,
|
|
1758
|
+
suppliedAssets: [{
|
|
1759
|
+
id: asset.id,
|
|
1760
|
+
contextRole: "trigger",
|
|
1761
|
+
role: "file_reference",
|
|
1762
|
+
kind: "file",
|
|
1763
|
+
path: saved2.path,
|
|
1764
|
+
mime: mime2,
|
|
1765
|
+
sha256: saved2.sha256,
|
|
1766
|
+
byteLength: saved2.size,
|
|
1767
|
+
runtimeMedia: false,
|
|
1768
|
+
sourceSha256: saved2.sha256,
|
|
1769
|
+
sourceByteLength: saved2.size,
|
|
1770
|
+
sourceMime: mime2,
|
|
1771
|
+
sourcePath: saved2.path
|
|
1772
|
+
}]
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1494
1775
|
const content = await client.mediaContent(accountId, asset.id, { maxBytes, signal: params.signal });
|
|
1495
|
-
|
|
1776
|
+
guard2();
|
|
1496
1777
|
const mime = content.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
1497
1778
|
if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mime)) throw Error("Native attachment MIME type is unsupported");
|
|
1498
1779
|
const hash = createHash4("sha256").update(content.bytes).digest("hex");
|
|
1499
1780
|
if (!asset.sha256 || hash !== asset.sha256 || asset.byteLength != null && asset.byteLength !== content.bytes.length) throw new Error("Native attachment integrity did not verify");
|
|
1500
1781
|
await authorize();
|
|
1501
|
-
const saved = await
|
|
1782
|
+
const saved = await saveManagedAttachmentBytes({ bytes: content.bytes, mime, filename, maxBytes, saveMediaBuffer: core.channel.media.saveMediaBuffer });
|
|
1502
1783
|
const actual = asset.actualVariant ?? asset.variant;
|
|
1503
1784
|
const attachment = {
|
|
1504
1785
|
kind: attachmentKind,
|
|
@@ -1518,7 +1799,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1518
1799
|
if (!preview || preview.sourceSha256 !== hash || !/^[a-f0-9]{64}$/.test(preview.sha256) || !Number.isInteger(preview.byteLength) || preview.byteLength <= 0 || preview.byteLength > 16 * 1024 * 1024 || !["image/jpeg", "image/png"].includes(preview.contentType)) throw Error("Native WXGF preview metadata did not verify");
|
|
1519
1800
|
await authorize();
|
|
1520
1801
|
const derived = await client.mediaContent(accountId, asset.id, { maxBytes: preview.byteLength, representation: "preview", signal: params.signal });
|
|
1521
|
-
|
|
1802
|
+
guard2();
|
|
1522
1803
|
const derivedMime = derived.contentType.split(";", 1)[0].trim().toLowerCase();
|
|
1523
1804
|
if (derived.sourceSha256 !== hash || derivedMime !== preview.contentType || derived.bytes.length !== preview.byteLength || createHash4("sha256").update(derived.bytes).digest("hex") !== preview.sha256) throw Error("Native WXGF preview integrity did not verify");
|
|
1524
1805
|
imageBytes = Buffer.from(derived.bytes);
|
|
@@ -1543,7 +1824,7 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1543
1824
|
[Model image: prepared still preview; original retained separately]`;
|
|
1544
1825
|
}
|
|
1545
1826
|
}
|
|
1546
|
-
|
|
1827
|
+
guard2();
|
|
1547
1828
|
return {
|
|
1548
1829
|
body,
|
|
1549
1830
|
attachment,
|
|
@@ -1570,78 +1851,114 @@ async function prepareNativeAttachmentInput(params) {
|
|
|
1570
1851
|
pending: false
|
|
1571
1852
|
};
|
|
1572
1853
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1854
|
+
function validateNativeMediaSendOptions(options, audioAsVoice = false) {
|
|
1855
|
+
if (options.forceDocument != null && typeof options.forceDocument !== "boolean") throw new Error("Invalid WeChat document option");
|
|
1856
|
+
if (options.videoQuality != null && !["normal", "original"].includes(options.videoQuality)) throw new Error("Invalid WeChat video quality");
|
|
1857
|
+
if (options.forceDocument && (audioAsVoice || options.videoQuality != null)) throw new Error("Document sending cannot also request voice or video quality");
|
|
1858
|
+
}
|
|
1859
|
+
function outboundMediaKind(mime, audioAsVoice, options) {
|
|
1860
|
+
if (options.forceDocument) return "file";
|
|
1861
|
+
if (audioAsVoice && !mime.startsWith("audio/")) throw new Error("Voice sending requires an explicit audio attachment");
|
|
1862
|
+
const selected = audioAsVoice ? "voice" : mime.startsWith("image/") ? "image" : mime.startsWith("video/") ? "video" : "file";
|
|
1863
|
+
if (options.videoQuality != null && selected !== "video") throw new Error("Video quality requires a video attachment");
|
|
1864
|
+
return selected;
|
|
1865
|
+
}
|
|
1866
|
+
function outboundSourceLimit(account, selectedKind) {
|
|
1867
|
+
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);
|
|
1868
|
+
}
|
|
1869
|
+
async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioAsVoice = false, key = randomUUID(), beforeSend, options = {}) {
|
|
1870
|
+
validateNativeMediaSendOptions(options, audioAsVoice);
|
|
1871
|
+
await beforeSend?.();
|
|
1872
|
+
let filename, mime, selectedKind, sourceBytes, source;
|
|
1873
|
+
let temporary;
|
|
1874
|
+
const accountId = account.native.accountId;
|
|
1875
|
+
try {
|
|
1876
|
+
let blob;
|
|
1877
|
+
if (/^https?:/.test(mediaUrl)) {
|
|
1878
|
+
const response = await fetch(mediaUrl, { redirect: "error", signal: AbortSignal.timeout(60 * 6e4) });
|
|
1879
|
+
if (!response.ok) throw new Error(`Native outbound source unavailable (${response.status})`);
|
|
1880
|
+
filename = basename(new URL(mediaUrl).pathname) || "attachment";
|
|
1881
|
+
mime = response.headers.get("content-type")?.split(";")[0].trim().toLowerCase() ?? "application/octet-stream";
|
|
1882
|
+
try {
|
|
1883
|
+
selectedKind = outboundMediaKind(mime, audioAsVoice, options);
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
await response.body?.cancel();
|
|
1886
|
+
throw error;
|
|
1597
1887
|
}
|
|
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));
|
|
1888
|
+
const max = outboundSourceLimit(account, selectedKind);
|
|
1889
|
+
const length = response.headers.get("content-length");
|
|
1890
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > max)) {
|
|
1891
|
+
await response.body?.cancel();
|
|
1892
|
+
throw new Error("Native outbound source exceeds configured size");
|
|
1627
1893
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1894
|
+
const reader = response.body?.getReader();
|
|
1895
|
+
if (!reader) throw new Error("Native outbound source has no bytes");
|
|
1896
|
+
let path, file, size = 0;
|
|
1897
|
+
try {
|
|
1898
|
+
temporary = await mkdtemp(join2(tmpdir(), "wechat-outbound-"));
|
|
1899
|
+
path = join2(temporary, "source");
|
|
1900
|
+
file = await open(path, "wx", 384);
|
|
1901
|
+
while (true) {
|
|
1902
|
+
const { value, done } = await reader.read();
|
|
1903
|
+
if (done) break;
|
|
1904
|
+
size += value.byteLength;
|
|
1905
|
+
if (size > max) throw new Error("Native outbound source exceeds configured size");
|
|
1906
|
+
await file.writeFile(value);
|
|
1907
|
+
}
|
|
1908
|
+
if (!size || length && size !== Number(length)) throw new Error("Native outbound source length mismatch");
|
|
1909
|
+
} finally {
|
|
1910
|
+
await reader.cancel().catch(() => {
|
|
1911
|
+
});
|
|
1912
|
+
reader.releaseLock();
|
|
1913
|
+
await file?.close();
|
|
1914
|
+
}
|
|
1915
|
+
blob = await openAsBlob(path, { type: mime });
|
|
1916
|
+
} else {
|
|
1917
|
+
const path = mediaUrl.startsWith("file:") ? new URL(mediaUrl) : mediaUrl;
|
|
1918
|
+
filename = basename(path instanceof URL ? path.pathname : path);
|
|
1919
|
+
const extension = extname(filename).toLowerCase();
|
|
1920
|
+
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";
|
|
1921
|
+
selectedKind = outboundMediaKind(mime, audioAsVoice, options);
|
|
1922
|
+
const max = outboundSourceLimit(account, selectedKind), file = await open(path, "r");
|
|
1923
|
+
let size;
|
|
1924
|
+
try {
|
|
1925
|
+
const stat = await file.stat();
|
|
1926
|
+
if (!stat.isFile() || !stat.size || stat.size > max) throw new Error("Native outbound source must be a bounded regular file");
|
|
1927
|
+
size = stat.size;
|
|
1928
|
+
} finally {
|
|
1929
|
+
await file.close();
|
|
1930
|
+
}
|
|
1931
|
+
blob = await openAsBlob(path, { type: mime });
|
|
1932
|
+
if (blob.size !== size) throw new Error("Native outbound source changed before staging");
|
|
1631
1933
|
}
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1934
|
+
sourceBytes = blob.size;
|
|
1935
|
+
if (sourceBytes > outboundSourceLimit(account, selectedKind)) throw new Error("Native outbound source exceeds selected media kind size");
|
|
1936
|
+
await beforeSend?.();
|
|
1937
|
+
source = await client.upload(accountId, blob, filename, mime);
|
|
1938
|
+
} finally {
|
|
1939
|
+
if (temporary) await rm(temporary, { recursive: true, force: true });
|
|
1635
1940
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1941
|
+
const input = {
|
|
1942
|
+
threadId: threadId2,
|
|
1943
|
+
kind: selectedKind,
|
|
1944
|
+
sourceId: source.id,
|
|
1945
|
+
standardSourceId: null,
|
|
1946
|
+
thumbnailSourceId: null,
|
|
1947
|
+
playSeconds: null,
|
|
1948
|
+
durationMs: null,
|
|
1949
|
+
...selectedKind === "video" ? { videoQuality: options.videoQuality ?? "normal" } : {}
|
|
1950
|
+
};
|
|
1641
1951
|
await beforeSend?.();
|
|
1642
|
-
|
|
1952
|
+
const waitMs = audioAsVoice ? 25 * 6e4 : selectedKind === "video" || selectedKind === "file" && sourceBytes > NATIVE_MAX_ORDINARY_SOURCE_BYTES ? 60 * 6e4 : 65e3;
|
|
1953
|
+
const submitted = await client.sendMedia(accountId, input, key);
|
|
1954
|
+
let job;
|
|
1955
|
+
try {
|
|
1956
|
+
job = await waitNativeMediaJob(client, accountId, submitted, waitMs);
|
|
1957
|
+
} catch {
|
|
1958
|
+
throw new NativeMediaJobInspectionError(submitted, accountId, threadId2, key, "status_unavailable");
|
|
1959
|
+
}
|
|
1643
1960
|
if (successfulSendJobs.has(job.state)) job = await waitNativeMediaSendReceipts(client, accountId, threadId2, job, audioAsVoice);
|
|
1644
|
-
if (!successfulSendJobs.has(job.state)) throw new
|
|
1961
|
+
if (!successfulSendJobs.has(job.state)) throw new NativeMediaJobInspectionError(job, accountId, threadId2, key, "job_not_completed");
|
|
1645
1962
|
const result = mediaSendResult(job);
|
|
1646
1963
|
const chunks = Array.isArray(result.chunks) ? result.chunks.filter((chunk) => chunk && typeof chunk === "object") : [];
|
|
1647
1964
|
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 +1966,16 @@ async function sendNativeAttachment(client, account, threadId2, mediaUrl, audioA
|
|
|
1649
1966
|
}
|
|
1650
1967
|
|
|
1651
1968
|
// src/outbound-media.ts
|
|
1652
|
-
async function sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice = false, idempotencyKey) {
|
|
1969
|
+
async function sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice = false, idempotencyKey, options = {}, beforeSend) {
|
|
1970
|
+
validateNativeMediaSendOptions(options, audioAsVoice);
|
|
1971
|
+
if (!mediaUrl && (options.forceDocument || options.videoQuality != null)) throw new Error("WeChat media options require an attachment");
|
|
1653
1972
|
const account = resolveWeChatAccount(cfg);
|
|
1654
1973
|
if (!account?.native) throw new Error("No native daemon configured");
|
|
1655
1974
|
const { client } = await ensureNativeHost(account.native);
|
|
1656
1975
|
const key = idempotencyKey ?? randomUUID2();
|
|
1657
1976
|
if (!mediaUrl) {
|
|
1658
1977
|
if (audioAsVoice) throw new Error("Voice sending requires an audio attachment");
|
|
1978
|
+
await beforeSend?.();
|
|
1659
1979
|
const send = await submitNativeText(client, account.native.accountId, to, text3, key);
|
|
1660
1980
|
return nativeOutboundDelivery(
|
|
1661
1981
|
client,
|
|
@@ -1665,26 +1985,116 @@ async function sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice = false, i
|
|
|
1665
1985
|
{ sendId: send.id, state: send.state, serverIds: send.serverId ? [send.serverId] : void 0 }
|
|
1666
1986
|
);
|
|
1667
1987
|
}
|
|
1668
|
-
const result = await sendNativeAttachment(client, account, to, mediaUrl, audioAsVoice, key);
|
|
1669
|
-
if (text3)
|
|
1988
|
+
const result = await sendNativeAttachment(client, account, to, mediaUrl, audioAsVoice, key, beforeSend, options);
|
|
1989
|
+
if (text3) {
|
|
1990
|
+
await beforeSend?.();
|
|
1991
|
+
await submitNativeText(client, account.native.accountId, to, text3, createHash5("sha256").update(`${key}\0caption`).digest("hex"));
|
|
1992
|
+
}
|
|
1670
1993
|
return result;
|
|
1671
1994
|
}
|
|
1672
1995
|
|
|
1996
|
+
// src/outbound-options.ts
|
|
1997
|
+
function nativePayloadMediaOptions(payload) {
|
|
1998
|
+
const value = payload.channelData?.["agent-wechat"];
|
|
1999
|
+
if (value == null) return {};
|
|
2000
|
+
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");
|
|
2001
|
+
return { videoQuality: value.videoQuality };
|
|
2002
|
+
}
|
|
2003
|
+
var wechatMediaActions = {
|
|
2004
|
+
describeMessageTool: () => ({ actions: ["send"], schema: { actions: ["send"], visibility: "all-configured", properties: {
|
|
2005
|
+
channelData: { type: "object", description: "WeChat per-send media options. Use agent-wechat.videoQuality for a single video attachment.", additionalProperties: true, properties: {
|
|
2006
|
+
"agent-wechat": { type: "object", additionalProperties: false, required: ["videoQuality"], properties: {
|
|
2007
|
+
videoQuality: { type: "string", enum: ["normal", "original"], description: "WeChat video mode: normal prepares playback; original also retains the unchanged source video. Cannot combine with forceDocument." }
|
|
2008
|
+
} }
|
|
2009
|
+
} }
|
|
2010
|
+
} } }),
|
|
2011
|
+
prepareSendPayload: ({ ctx, payload }) => {
|
|
2012
|
+
const options = nativePayloadMediaOptions(payload);
|
|
2013
|
+
if (options.videoQuality == null) return void 0;
|
|
2014
|
+
validateNativeMediaSendOptions({ ...options, forceDocument: ctx.params.forceDocument ?? ctx.params.asDocument }, payload.audioAsVoice);
|
|
2015
|
+
return payload;
|
|
2016
|
+
}
|
|
2017
|
+
};
|
|
2018
|
+
var NativeMediaPayloadInspectionError = class extends Error {
|
|
2019
|
+
retryable = false;
|
|
2020
|
+
code = "native_media_payload_inspection_required";
|
|
2021
|
+
deliveries;
|
|
2022
|
+
pendingJob;
|
|
2023
|
+
constructor(deliveries, pendingJob) {
|
|
2024
|
+
super("WeChat attachment batch requires inspection; retain completed receipts and do not automatically resend");
|
|
2025
|
+
this.name = "NativeMediaPayloadInspectionError";
|
|
2026
|
+
this.deliveries = deliveries;
|
|
2027
|
+
this.pendingJob = pendingJob;
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
function createWeChatMediaHandlers(send = sendWeChatMedia) {
|
|
2031
|
+
const key = (ctx, index = 0) => ctx.deliveryQueueId ? createHash6("sha256").update(JSON.stringify(["wechat-media", ctx.deliveryQueueId, ctx.deliveryPartIndex ?? 0, index])).digest("hex") : void 0;
|
|
2032
|
+
const guard2 = (ctx) => async () => {
|
|
2033
|
+
ctx.signal?.throwIfAborted();
|
|
2034
|
+
await ctx.onPlatformSendDispatch?.();
|
|
2035
|
+
ctx.signal?.throwIfAborted();
|
|
2036
|
+
ctx.assertDirectAdapterHandoff?.();
|
|
2037
|
+
};
|
|
2038
|
+
return {
|
|
2039
|
+
sendMedia: async (ctx) => ({
|
|
2040
|
+
channel: "agent-wechat",
|
|
2041
|
+
...await send(ctx.cfg, ctx.to, ctx.text, ctx.mediaUrl, ctx.audioAsVoice, key(ctx), { forceDocument: ctx.forceDocument }, guard2(ctx))
|
|
2042
|
+
}),
|
|
2043
|
+
sendPayload: async (ctx) => {
|
|
2044
|
+
const options = { ...nativePayloadMediaOptions(ctx.payload), forceDocument: ctx.forceDocument };
|
|
2045
|
+
const audioAsVoice = ctx.payload.audioAsVoice ?? ctx.audioAsVoice;
|
|
2046
|
+
validateNativeMediaSendOptions(options, audioAsVoice);
|
|
2047
|
+
const urls = [...new Set([...ctx.payload.mediaUrls ?? [], ctx.payload.mediaUrl ?? ctx.mediaUrl].filter((url2) => typeof url2 === "string" && !!url2))];
|
|
2048
|
+
if (urls.length > 16 || options.videoQuality != null && urls.length !== 1) throw Error("WeChat video quality requires one attachment per send");
|
|
2049
|
+
const deliveries = [];
|
|
2050
|
+
for (const [index, mediaUrl] of (urls.length ? urls : [void 0]).entries()) {
|
|
2051
|
+
let delivered;
|
|
2052
|
+
try {
|
|
2053
|
+
delivered = await send(ctx.cfg, ctx.to, index === 0 ? ctx.payload.text ?? ctx.text : "", mediaUrl, audioAsVoice, key(ctx, index), options, guard2(ctx));
|
|
2054
|
+
} catch (error) {
|
|
2055
|
+
if (!deliveries.length) throw error;
|
|
2056
|
+
throw new NativeMediaPayloadInspectionError(deliveries, error instanceof NativeMediaJobInspectionError ? { jobId: error.jobId, accountId: error.accountId, threadId: error.threadId, idempotencyKey: error.idempotencyKey } : void 0);
|
|
2057
|
+
}
|
|
2058
|
+
deliveries.push(delivered);
|
|
2059
|
+
if (urls.length > 1) {
|
|
2060
|
+
try {
|
|
2061
|
+
await ctx.onDeliveryResult?.({ channel: "agent-wechat", ...delivered });
|
|
2062
|
+
} catch {
|
|
2063
|
+
throw new NativeMediaPayloadInspectionError(deliveries);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
if (delivered.meta.outcome === "unknown") break;
|
|
2067
|
+
}
|
|
2068
|
+
const first = deliveries[0];
|
|
2069
|
+
return {
|
|
2070
|
+
channel: "agent-wechat",
|
|
2071
|
+
...first,
|
|
2072
|
+
...deliveries.length > 1 ? { meta: {
|
|
2073
|
+
...first.meta,
|
|
2074
|
+
parts: deliveries,
|
|
2075
|
+
retryable: false,
|
|
2076
|
+
...deliveries.some((part) => part.meta.outcome === "unknown") ? { outcome: "unknown", receiptState: "needs_review" } : {}
|
|
2077
|
+
} } : {}
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1673
2083
|
// src/native-monitor.ts
|
|
1674
|
-
import { createHash as
|
|
2084
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
1675
2085
|
import { createChannelMessageReplyPipeline } from "openclaw/plugin-sdk/channel-outbound";
|
|
1676
2086
|
import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
1677
2087
|
|
|
1678
2088
|
// src/native-consumer-state.ts
|
|
1679
|
-
import { createHash as
|
|
2089
|
+
import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
|
|
1680
2090
|
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1681
|
-
import { dirname as dirname2, isAbsolute as isAbsolute2, join as
|
|
2091
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3 } from "node:path";
|
|
1682
2092
|
var NativeConsumerState = class {
|
|
1683
2093
|
file;
|
|
1684
2094
|
value;
|
|
1685
2095
|
constructor(directory, serverId, accountId) {
|
|
1686
2096
|
if (!isAbsolute2(directory) || !serverId || !accountId) throw new Error("Native consumer state requires absolute storage and bound server/account identities");
|
|
1687
|
-
this.file =
|
|
2097
|
+
this.file = join3(directory, `${createHash7("sha256").update(`${serverId}\0${accountId}`).digest("hex")}.json`);
|
|
1688
2098
|
this.value = { schemaVersion: 1, serverId, accountId, cursor: "0", items: {} };
|
|
1689
2099
|
if (existsSync(this.file)) {
|
|
1690
2100
|
const stat = lstatSync(this.file);
|
|
@@ -1693,6 +2103,7 @@ var NativeConsumerState = class {
|
|
|
1693
2103
|
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
2104
|
for (const [id2, item] of Object.entries(parsed.items)) {
|
|
1695
2105
|
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");
|
|
2106
|
+
nativeRecallInfo(item.message);
|
|
1696
2107
|
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
2108
|
if (item.revision != null && (typeof item.revision !== "string" || !item.revision || item.revision.length > 64)) throw Error("Invalid native custody revision; state was preserved");
|
|
1698
2109
|
item.revision ??= randomUUID3();
|
|
@@ -1822,13 +2233,8 @@ var NativeConsumerState = class {
|
|
|
1822
2233
|
}
|
|
1823
2234
|
const key = nativeRecallIdentity(recall.threadId, recall.serverId);
|
|
1824
2235
|
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
|
-
};
|
|
2236
|
+
const remembered = mergeNativeRecall(this.value.recalls[key] ?? recall, recall);
|
|
2237
|
+
this.value.recalls[key] = remembered;
|
|
1832
2238
|
const affected = /* @__PURE__ */ new Set();
|
|
1833
2239
|
for (const item of this.items()) {
|
|
1834
2240
|
if (item.message.threadId !== recall.threadId) continue;
|
|
@@ -1836,7 +2242,7 @@ var NativeConsumerState = class {
|
|
|
1836
2242
|
const quote = item.message.quote?.serverId === recall.serverId;
|
|
1837
2243
|
if (!target && !quote) continue;
|
|
1838
2244
|
affected.add(item.message.id);
|
|
1839
|
-
item.message = target ? recalledNativeMessage(item.message,
|
|
2245
|
+
item.message = target ? recalledNativeMessage(item.message, remembered) : { ...item.message, quote: void 0 };
|
|
1840
2246
|
if (target) item.groupContext = false;
|
|
1841
2247
|
item.readPending = false;
|
|
1842
2248
|
if (["dispatching", "handed_off"].includes(item.state)) item.state = "needs_review";
|
|
@@ -1907,7 +2313,7 @@ var NativeConsumerState = class {
|
|
|
1907
2313
|
});
|
|
1908
2314
|
const revisions = items.map((item) => item.revision);
|
|
1909
2315
|
if (new Set(items.map((item) => item.message.threadId)).size !== 1) throw new Error("Prepared input manifest crosses threads");
|
|
1910
|
-
const manifestHash =
|
|
2316
|
+
const manifestHash = createHash7("sha256").update(JSON.stringify({
|
|
1911
2317
|
accountId: this.value.accountId,
|
|
1912
2318
|
messageIds: exact,
|
|
1913
2319
|
dispatchManifests: items.map((item) => item.dispatchManifest ?? null)
|
|
@@ -1916,7 +2322,7 @@ var NativeConsumerState = class {
|
|
|
1916
2322
|
const runId = typeof args[0] === "string" ? args[0] : args[0]?.runId;
|
|
1917
2323
|
if (typeof runId !== "string" || !runId || runId.length > 256) throw new Error("Prepared run callback has no valid runId");
|
|
1918
2324
|
this.mutate(() => {
|
|
1919
|
-
const currentHash =
|
|
2325
|
+
const currentHash = createHash7("sha256").update(JSON.stringify({
|
|
1920
2326
|
accountId: this.value.accountId,
|
|
1921
2327
|
messageIds: exact,
|
|
1922
2328
|
dispatchManifests: exact.map((id2) => this.value.items[id2]?.dispatchManifest ?? null)
|
|
@@ -1935,11 +2341,11 @@ var NativeConsumerState = class {
|
|
|
1935
2341
|
};
|
|
1936
2342
|
|
|
1937
2343
|
// src/native-prepared-boundary.ts
|
|
1938
|
-
import { createHash as
|
|
2344
|
+
import { createHash as createHash8, randomUUID as randomUUID4 } from "node:crypto";
|
|
1939
2345
|
import { basename as basename2, isAbsolute as isAbsolute3 } from "node:path";
|
|
1940
2346
|
var pending = /* @__PURE__ */ new Map();
|
|
1941
2347
|
var enabled = false;
|
|
1942
|
-
var digest = (value) =>
|
|
2348
|
+
var digest = (value) => createHash8("sha256").update(value).digest("hex");
|
|
1943
2349
|
function prune() {
|
|
1944
2350
|
for (const [id2, item] of pending) if (item.expires < Date.now()) pending.delete(id2);
|
|
1945
2351
|
}
|
|
@@ -1950,7 +2356,7 @@ function nativeDispatchManifest(params) {
|
|
|
1950
2356
|
const history = params.history ?? [];
|
|
1951
2357
|
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
2358
|
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");
|
|
2359
|
+
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
2360
|
return Object.freeze({ ...asset });
|
|
1955
2361
|
}));
|
|
1956
2362
|
const messageSid = nativeSourceMessageSid(params.message);
|
|
@@ -2277,17 +2683,17 @@ async function recheckNativeInputs(client, state, messages) {
|
|
|
2277
2683
|
throw new NativeInputInvalidated();
|
|
2278
2684
|
}
|
|
2279
2685
|
if (message.quote) {
|
|
2280
|
-
const quote =
|
|
2281
|
-
if (quote
|
|
2282
|
-
if (quote.state === "recalled" || quote.message && nativeRecallInfo(quote.message)) {
|
|
2686
|
+
const quote = current.quote;
|
|
2687
|
+
if (quote?.state === "recalled") {
|
|
2283
2688
|
const affected = state.acceptRecall(state.cursor, {
|
|
2284
2689
|
threadId: message.threadId,
|
|
2285
2690
|
serverId: message.quote.serverId,
|
|
2286
|
-
observedAt:
|
|
2691
|
+
observedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2287
2692
|
});
|
|
2288
2693
|
invalidateNativePreparedInputs(message.accountId, affected);
|
|
2289
2694
|
throw new NativeInputInvalidated();
|
|
2290
2695
|
}
|
|
2696
|
+
if (!quote || JSON.stringify(quote) !== JSON.stringify(message.quote)) throw Error("Native embedded quote changed; delivery held for review");
|
|
2291
2697
|
}
|
|
2292
2698
|
if (!state.isCurrent(message)) throw new NativeInputInvalidated();
|
|
2293
2699
|
}
|
|
@@ -2444,7 +2850,7 @@ async function dispatchNativeMessageInput(params, revision) {
|
|
|
2444
2850
|
});
|
|
2445
2851
|
assertCurrent();
|
|
2446
2852
|
state.beginDispatch(message.id, dispatchManifest, buffered.map((item) => item.message.id));
|
|
2447
|
-
const callback = readPolicy !== "on_inbound" && (message.type === 1 || message.type === 49 && message.appMessageType === 19 && !!message.body || message.type === 49 && [3, 4, 5].includes(message.appMessageType ?? 0) && !!message.link || preparedMedia.quote
|
|
2853
|
+
const callback = readPolicy !== "on_inbound" && (message.type === 1 || message.type === 49 && message.appMessageType === 19 && !!message.body || message.type === 49 && [3, 4, 5].includes(message.appMessageType ?? 0) && !!message.link || preparedMedia.quote && [1, 49].includes(preparedMedia.quote.type ?? 0) && ["resolved", "embedded"].includes(preparedMedia.quote.state) || preparedMedia.attachment?.status === "ready" && preparedMedia.suppliedAssets?.length) ? state.preparedCallback([message.id], readPolicy) : void 0;
|
|
2448
2854
|
if (callback) trackNativePreparedInput(dispatchManifest, callback);
|
|
2449
2855
|
const beforeSend = () => recheck([message, ...admitted]);
|
|
2450
2856
|
let failed = false;
|
|
@@ -2463,12 +2869,12 @@ async function dispatchNativeMessageInput(params, revision) {
|
|
|
2463
2869
|
if (urls.length > 16) throw new Error("Native reply media count exceeds its bound");
|
|
2464
2870
|
if (payload.audioAsVoice && !urls.length) throw new Error("Native voice reply requires an audio attachment");
|
|
2465
2871
|
for (const url2 of urls) {
|
|
2466
|
-
const key =
|
|
2872
|
+
const key = createHash9("sha256").update(`${dispatchManifest.messageSid}\0${delivery++}`).digest("hex");
|
|
2467
2873
|
await sendNativeAttachment(client, account, message.threadId, url2, payload.audioAsVoice === true, key, beforeSend);
|
|
2468
2874
|
}
|
|
2469
2875
|
if (payload.text) {
|
|
2470
2876
|
await beforeSend();
|
|
2471
|
-
const key =
|
|
2877
|
+
const key = createHash9("sha256").update(`${dispatchManifest.messageSid}\0${delivery++}`).digest("hex");
|
|
2472
2878
|
await submitNativeText(client, account.native.accountId, message.threadId, payload.text, key);
|
|
2473
2879
|
}
|
|
2474
2880
|
if (urls.length || payload.text) params.onDelivered?.();
|
|
@@ -2644,43 +3050,69 @@ async function startNativeMonitor(opts) {
|
|
|
2644
3050
|
|
|
2645
3051
|
// src/native-login.ts
|
|
2646
3052
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
3053
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2647
3054
|
var operations = /* @__PURE__ */ new Map();
|
|
2648
3055
|
var terminal = /* @__PURE__ */ new Set(["succeeded", "failed", "cancelled", "expired", "rejected", "needs_login"]);
|
|
2649
|
-
|
|
3056
|
+
function guard(access2) {
|
|
3057
|
+
access2.signal?.throwIfAborted();
|
|
3058
|
+
access2.assertCurrent?.();
|
|
3059
|
+
}
|
|
3060
|
+
function validateOperation(operation, accountId, operationId) {
|
|
3061
|
+
if (operation.accountId !== accountId || operation.kind !== "login" || operationId && operation.id !== operationId) throw Error("Native login operation identity does not match its account");
|
|
3062
|
+
}
|
|
3063
|
+
async function nativeLoginStart(account, options = {}, access2 = {}) {
|
|
3064
|
+
guard(access2);
|
|
2650
3065
|
const native = account.native;
|
|
2651
|
-
const { client } = await ensureNativeHost(native);
|
|
3066
|
+
const { client } = await (access2.host ?? ensureNativeHost)(native);
|
|
3067
|
+
guard(access2);
|
|
3068
|
+
const configurationKey = nativeConfigurationKey(native);
|
|
2652
3069
|
const existing = operations.get(account.accountId);
|
|
2653
|
-
if (existing && existing.
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
3070
|
+
if (existing && existing.configurationKey === configurationKey) {
|
|
3071
|
+
const refreshed = await client.operation(native.accountId, existing.operation.id);
|
|
3072
|
+
guard(access2);
|
|
3073
|
+
validateOperation(refreshed, native.accountId, existing.operation.id);
|
|
3074
|
+
existing.operation = refreshed;
|
|
3075
|
+
existing.client = client;
|
|
3076
|
+
if (!terminal.has(existing.operation.state)) return nativeLoginWait(account, Math.min(options.timeoutMs ?? 3e4, 3e4), true, access2);
|
|
3077
|
+
}
|
|
3078
|
+
guard(access2);
|
|
2657
3079
|
const operation = await client.login(native.accountId, {
|
|
2658
3080
|
allowQr: true,
|
|
2659
3081
|
forceQr: options.force === true,
|
|
2660
3082
|
timeoutSeconds: Math.max(10, Math.min(900, Math.ceil((options.timeoutMs ?? 3e5) / 1e3))),
|
|
2661
3083
|
computerName: "agent-wechat"
|
|
2662
|
-
});
|
|
2663
|
-
|
|
2664
|
-
|
|
3084
|
+
}, randomUUID5());
|
|
3085
|
+
validateOperation(operation, native.accountId);
|
|
3086
|
+
operations.set(account.accountId, { client, accountId: native.accountId, configurationKey, operation });
|
|
3087
|
+
guard(access2);
|
|
3088
|
+
return nativeLoginWait(account, Math.min(options.timeoutMs ?? 3e4, 3e4), true, access2);
|
|
2665
3089
|
}
|
|
2666
|
-
async function nativeLoginWait(
|
|
2667
|
-
|
|
2668
|
-
|
|
3090
|
+
async function nativeLoginWait(account, timeoutMs = 3e4, stopAtQr = false, access2 = {}) {
|
|
3091
|
+
guard(access2);
|
|
3092
|
+
const active2 = operations.get(account.accountId);
|
|
3093
|
+
if (!active2 || active2.configurationKey !== nativeConfigurationKey(account.native)) return { connected: false, state: "idle", message: "No native login operation; start login first" };
|
|
2669
3094
|
const deadline = Date.now() + Math.max(1e3, Math.min(timeoutMs, 3e5));
|
|
2670
3095
|
while (Date.now() < deadline) {
|
|
2671
|
-
|
|
3096
|
+
guard(access2);
|
|
3097
|
+
const refreshed = await active2.client.operation(active2.accountId, active2.operation.id);
|
|
3098
|
+
guard(access2);
|
|
3099
|
+
validateOperation(refreshed, active2.accountId, active2.operation.id);
|
|
3100
|
+
active2.operation = refreshed;
|
|
2672
3101
|
const state = active2.operation.state;
|
|
2673
|
-
|
|
2674
|
-
if (
|
|
3102
|
+
const result = { connected: state === "succeeded", state, operationId: active2.operation.id };
|
|
3103
|
+
if (state === "succeeded") return { ...result, message: "Native login succeeded" };
|
|
3104
|
+
if (terminal.has(state)) return { ...result, message: `Native login ${state}` };
|
|
3105
|
+
if (stopAtQr && state === "awaiting_approval") return { ...result, message: "Approve native login on your phone" };
|
|
2675
3106
|
if (stopAtQr && active2.operation.result?.qrAvailable === true) {
|
|
2676
3107
|
const generation = active2.operation.result?.qrGeneration;
|
|
2677
3108
|
const png = await active2.client.qr(active2.accountId, active2.operation.id, typeof generation === "number" ? generation : void 0);
|
|
2678
|
-
|
|
3109
|
+
guard(access2);
|
|
3110
|
+
return { ...result, qrDataUrl: `data:image/png;base64,${Buffer.from(png).toString("base64")}`, message: "Scan the white-background native QR and approve login" };
|
|
2679
3111
|
}
|
|
2680
|
-
|
|
2681
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
3112
|
+
await delay(Math.min(500, Math.max(0, deadline - Date.now())), void 0, { signal: access2.signal });
|
|
2682
3113
|
}
|
|
2683
|
-
|
|
3114
|
+
guard(access2);
|
|
3115
|
+
return { connected: false, state: active2.operation.state, operationId: active2.operation.id, message: "Native login remains pending; wait on the same operation rather than starting a replacement" };
|
|
2684
3116
|
}
|
|
2685
3117
|
async function sendNativeText(account, threadId2, text3, key = randomUUID5()) {
|
|
2686
3118
|
const native = account.native;
|
|
@@ -2694,29 +3126,59 @@ async function sendNativeText(account, threadId2, text3, key = randomUUID5()) {
|
|
|
2694
3126
|
{ sendId: send.id, state: send.state, serverIds: send.serverId ? [send.serverId] : void 0 }
|
|
2695
3127
|
);
|
|
2696
3128
|
}
|
|
2697
|
-
function createNativeLoginTool(account) {
|
|
3129
|
+
function createNativeLoginTool(account, dependencies = {}) {
|
|
3130
|
+
const actions = ["status", "start", "wait", "disconnect", "logout", "forget"];
|
|
3131
|
+
const configurationKey = nativeConfigurationKey(account.native);
|
|
2698
3132
|
return {
|
|
2699
3133
|
label: "WeChat Login",
|
|
2700
3134
|
name: "wechat_login",
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3135
|
+
catalogMode: "direct-only",
|
|
3136
|
+
description: "Check WeChat status, start or resume native login, or wait for the same login operation. Show the returned QR image to the user to scan and approve on their phone; do not generate a replacement QR. Repeated start reuses an active operation. For a new operation, force uses QR instead of saved trust. timeoutMs bounds the wait. Disconnect retains saved trust. Logout reports its durable native outcome; it does not promise saved-device revocation. Forget requires confirm=true and removes local authentication while retaining history.",
|
|
3137
|
+
parameters: { type: "object", additionalProperties: false, properties: {
|
|
3138
|
+
action: { type: "string", enum: actions },
|
|
3139
|
+
force: { type: "boolean", description: "With start, use QR instead of saved trust for a new login. An active login is still reused." },
|
|
3140
|
+
timeoutMs: { type: "integer", minimum: 1e3, maximum: 3e5, description: "Wait bound in milliseconds; defaults to 30000. Start returns as soon as a QR is available." },
|
|
3141
|
+
confirm: { type: "boolean", description: "Required true for forget." }
|
|
3142
|
+
}, required: ["action"] },
|
|
3143
|
+
execute: async (_id, params, signal) => {
|
|
3144
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) throw Error("Expected login arguments");
|
|
2704
3145
|
const args = params;
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
3146
|
+
if (Object.keys(args).some((key) => !["action", "force", "timeoutMs", "confirm"].includes(key)) || !actions.includes(args.action) || args.force !== void 0 && typeof args.force !== "boolean" || args.confirm !== void 0 && typeof args.confirm !== "boolean" || args.timeoutMs !== void 0 && (!Number.isSafeInteger(args.timeoutMs) || args.timeoutMs < 1e3 || args.timeoutMs > 3e5)) throw Error("Invalid native login arguments");
|
|
3147
|
+
if (args.action === "forget" && args.confirm !== true) throw Error("Forgetting native credentials requires confirm=true");
|
|
3148
|
+
const access2 = { host: dependencies.host, signal, assertCurrent: () => {
|
|
3149
|
+
dependencies.context?.assertInvocationCurrent?.();
|
|
3150
|
+
if (!dependencies.context) return;
|
|
3151
|
+
const context = dependencies.context;
|
|
3152
|
+
const config = context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
3153
|
+
const current = config && resolveWeChatAccount(config);
|
|
3154
|
+
if (!current?.enabled || !current.native || nativeConfigurationKey(current.native) !== configurationKey) throw Error("Native login configuration changed; invoke the tool again");
|
|
3155
|
+
} };
|
|
3156
|
+
guard(access2);
|
|
3157
|
+
if (args.action === "start" || args.action === "wait") {
|
|
3158
|
+
const result2 = args.action === "start" ? await nativeLoginStart(account, { force: args.force, timeoutMs: args.timeoutMs }, access2) : await nativeLoginWait(account, args.timeoutMs, false, access2);
|
|
2709
3159
|
const content = [{ type: "text", text: result2.message }];
|
|
2710
3160
|
if (result2.qrDataUrl) content.push({ type: "image", data: result2.qrDataUrl.split(",")[1], mimeType: "image/png" });
|
|
2711
|
-
return { content, details: { connected: result2.connected } };
|
|
3161
|
+
return { content, details: { connected: result2.connected, state: result2.state, operationId: result2.operationId } };
|
|
2712
3162
|
}
|
|
3163
|
+
const native = account.native;
|
|
3164
|
+
const { client } = await (access2.host ?? ensureNativeHost)(native);
|
|
3165
|
+
guard(access2);
|
|
2713
3166
|
const result = args.action === "status" ? await client.account(native.accountId) : args.action === "disconnect" ? await client.disconnect(native.accountId) : args.action === "logout" ? await client.logout(native.accountId, randomUUID5()) : args.action === "forget" && args.confirm === true ? await client.forgetCredentials(native.accountId, randomUUID5(), true) : (() => {
|
|
2714
3167
|
throw new Error("Unknown native login action");
|
|
2715
3168
|
})();
|
|
3169
|
+
if (args.action !== "status") operations.delete(account.accountId);
|
|
3170
|
+
guard(access2);
|
|
2716
3171
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
2717
3172
|
}
|
|
2718
3173
|
};
|
|
2719
3174
|
}
|
|
3175
|
+
function registerNativeLoginTool(api) {
|
|
3176
|
+
api.registerTool({ contextVersion: 2, create: (context) => {
|
|
3177
|
+
const config = context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
3178
|
+
const account = config && resolveWeChatAccount(config);
|
|
3179
|
+
return account?.enabled && account.native ? [createNativeLoginTool(account, { context })] : [];
|
|
3180
|
+
} }, { names: ["wechat_login"] });
|
|
3181
|
+
}
|
|
2720
3182
|
|
|
2721
3183
|
// src/channel.ts
|
|
2722
3184
|
async function sendWeChatText(cfg, to, text3) {
|
|
@@ -2724,6 +3186,7 @@ async function sendWeChatText(cfg, to, text3) {
|
|
|
2724
3186
|
if (!account?.serverUrl) throw new Error("No native daemon configured");
|
|
2725
3187
|
return sendNativeText(account, to, text3);
|
|
2726
3188
|
}
|
|
3189
|
+
var mediaHandlers = createWeChatMediaHandlers();
|
|
2727
3190
|
var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
2728
3191
|
id: "agent-wechat",
|
|
2729
3192
|
capabilities: {
|
|
@@ -2736,10 +3199,7 @@ var wechatMessageAdapter = createChannelMessageAdapterFromOutbound({
|
|
|
2736
3199
|
channel: "agent-wechat",
|
|
2737
3200
|
...await sendWeChatText(cfg, to, text3)
|
|
2738
3201
|
}),
|
|
2739
|
-
|
|
2740
|
-
channel: "agent-wechat",
|
|
2741
|
-
...await sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice)
|
|
2742
|
-
})
|
|
3202
|
+
...mediaHandlers
|
|
2743
3203
|
}
|
|
2744
3204
|
});
|
|
2745
3205
|
var meta = {
|
|
@@ -2763,6 +3223,7 @@ var wechatPlugin = {
|
|
|
2763
3223
|
tts: { voice: { synthesisTarget: "voice-note", transcodesAudio: true } }
|
|
2764
3224
|
},
|
|
2765
3225
|
reload: { configPrefixes: ["channels.agent-wechat"] },
|
|
3226
|
+
actions: wechatMediaActions,
|
|
2766
3227
|
configSchema: {
|
|
2767
3228
|
uiHints: { "native.token": { sensitive: true } },
|
|
2768
3229
|
schema: {
|
|
@@ -2884,10 +3345,7 @@ var wechatPlugin = {
|
|
|
2884
3345
|
channel: "agent-wechat",
|
|
2885
3346
|
...await sendWeChatText(cfg, to, text3)
|
|
2886
3347
|
}),
|
|
2887
|
-
|
|
2888
|
-
channel: "agent-wechat",
|
|
2889
|
-
...await sendWeChatMedia(cfg, to, text3, mediaUrl, audioAsVoice)
|
|
2890
|
-
})
|
|
3348
|
+
...mediaHandlers
|
|
2891
3349
|
},
|
|
2892
3350
|
message: wechatMessageAdapter,
|
|
2893
3351
|
gateway: {
|
|
@@ -2917,7 +3375,7 @@ var wechatPlugin = {
|
|
|
2917
3375
|
loginWithQrWait: async ({ accountId, timeoutMs }) => {
|
|
2918
3376
|
const account = resolveWeChatAccount(getWeChatRuntime().config.current(), accountId ?? void 0);
|
|
2919
3377
|
if (!account?.native) return { connected: false, message: "No native daemon configured" };
|
|
2920
|
-
return nativeLoginWait(account
|
|
3378
|
+
return nativeLoginWait(account, timeoutMs);
|
|
2921
3379
|
}
|
|
2922
3380
|
},
|
|
2923
3381
|
auth: {
|
|
@@ -2943,10 +3401,6 @@ var wechatPlugin = {
|
|
|
2943
3401
|
linked: runtime2?.linked ?? false
|
|
2944
3402
|
})
|
|
2945
3403
|
},
|
|
2946
|
-
agentTools: (({ cfg }) => {
|
|
2947
|
-
const account = resolveWeChatAccount(cfg);
|
|
2948
|
-
return account?.native ? [createNativeLoginTool(account)] : [];
|
|
2949
|
-
}),
|
|
2950
3404
|
directory: {
|
|
2951
3405
|
self: async ({ cfg }) => {
|
|
2952
3406
|
const account = resolveWeChatAccount(cfg);
|
|
@@ -3054,7 +3508,7 @@ function createNativeFacade(config, dependencies = {}) {
|
|
|
3054
3508
|
fail(res, 403, "facade_account_forbidden");
|
|
3055
3509
|
return true;
|
|
3056
3510
|
}
|
|
3057
|
-
if (!["GET", "HEAD", "POST", "PATCH", "DELETE"].includes(req.method ?? "")) {
|
|
3511
|
+
if (!["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"].includes(req.method ?? "")) {
|
|
3058
3512
|
fail(res, 405, "method_not_allowed");
|
|
3059
3513
|
return true;
|
|
3060
3514
|
}
|
|
@@ -3067,19 +3521,23 @@ function createNativeFacade(config, dependencies = {}) {
|
|
|
3067
3521
|
try {
|
|
3068
3522
|
const host2 = dependencies.endpoint ? void 0 : await ensureNativeHost(config);
|
|
3069
3523
|
const headers = new Headers();
|
|
3070
|
-
for (const
|
|
3071
|
-
const value = req.headers[
|
|
3072
|
-
if (typeof value === "string") headers.set(
|
|
3524
|
+
for (const name3 of ["content-type", "accept", "range", "if-none-match", "if-range", "last-event-id", "idempotency-key"]) {
|
|
3525
|
+
const value = req.headers[name3];
|
|
3526
|
+
if (typeof value === "string") headers.set(name3, value);
|
|
3073
3527
|
}
|
|
3528
|
+
const upload = pieces.length === 4 && pieces[3] === "uploads" && req.method === "POST";
|
|
3529
|
+
const mediaContent = pieces.length === 6 && pieces[3] === "media" && pieces[5] === "content" && parsed.searchParams.get("representation") !== "preview";
|
|
3530
|
+
const requestLimit = upload ? NATIVE_MAX_SOURCE_BYTES + 64 * 1024 : MAX_BODY;
|
|
3531
|
+
const responseLimit = mediaContent ? Math.max(MAX_BODY, NATIVE_MAX_SOURCE_BYTES) : MAX_BODY;
|
|
3074
3532
|
const length = req.headers["content-length"];
|
|
3075
|
-
if (length && (!/^\d+$/.test(length) || Number(length) >
|
|
3533
|
+
if (length && (!/^\d+$/.test(length) || Number(length) > requestLimit)) {
|
|
3076
3534
|
fail(res, 413, "payload_too_large");
|
|
3077
3535
|
return true;
|
|
3078
3536
|
}
|
|
3079
3537
|
let size = 0;
|
|
3080
3538
|
const body = ["GET", "HEAD"].includes(req.method ?? "") ? void 0 : Readable.toWeb(req).pipeThrough(new NodeTransformStream({ transform(chunk, controller) {
|
|
3081
3539
|
size += chunk.byteLength;
|
|
3082
|
-
if (size >
|
|
3540
|
+
if (size > requestLimit) {
|
|
3083
3541
|
tooLarge = true;
|
|
3084
3542
|
throw new Error("payload_too_large");
|
|
3085
3543
|
}
|
|
@@ -3092,10 +3550,16 @@ function createNativeFacade(config, dependencies = {}) {
|
|
|
3092
3550
|
headers.set("Authorization", `Bearer ${dependencies.token}`);
|
|
3093
3551
|
response = await (dependencies.fetch ?? fetch)(new URL(relative, dependencies.endpoint), init);
|
|
3094
3552
|
}
|
|
3553
|
+
const responseLength = response.headers.get("content-length");
|
|
3554
|
+
if (responseLength && (!/^\d+$/.test(responseLength) || Number(responseLength) > responseLimit)) {
|
|
3555
|
+
await response.body?.cancel();
|
|
3556
|
+
fail(res, 502, "native_facade_unavailable");
|
|
3557
|
+
return true;
|
|
3558
|
+
}
|
|
3095
3559
|
res.statusCode = response.status;
|
|
3096
|
-
for (const
|
|
3097
|
-
const value = response.headers.get(
|
|
3098
|
-
if (value) res.setHeader(
|
|
3560
|
+
for (const name3 of ["content-type", "content-length", "content-range", "accept-ranges", "etag", "cache-control", "content-disposition", "x-content-type-options"]) {
|
|
3561
|
+
const value = response.headers.get(name3);
|
|
3562
|
+
if (value) res.setHeader(name3, value);
|
|
3099
3563
|
}
|
|
3100
3564
|
res.setHeader("x-content-type-options", "nosniff");
|
|
3101
3565
|
const reader = response.body?.getReader();
|
|
@@ -3110,7 +3574,7 @@ function createNativeFacade(config, dependencies = {}) {
|
|
|
3110
3574
|
const { value, done } = await reader.read();
|
|
3111
3575
|
if (done) break;
|
|
3112
3576
|
emitted += value.length;
|
|
3113
|
-
if (!sse && emitted >
|
|
3577
|
+
if (!sse && emitted > responseLimit) throw new Error("upstream_response_too_large");
|
|
3114
3578
|
if (!res.write(value)) await drain(res, abort.signal);
|
|
3115
3579
|
}
|
|
3116
3580
|
} finally {
|
|
@@ -3154,14 +3618,14 @@ function object(value) {
|
|
|
3154
3618
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw Error("Expected tool arguments");
|
|
3155
3619
|
return value;
|
|
3156
3620
|
}
|
|
3157
|
-
function string(value,
|
|
3158
|
-
if (typeof value !== "string" || !value.trim() || value.length > max || /[\u0000-\u001f\u007f]/.test(value)) throw Error(`Invalid ${
|
|
3621
|
+
function string(value, name3, max = 4096) {
|
|
3622
|
+
if (typeof value !== "string" || !value.trim() || value.length > max || /[\u0000-\u001f\u007f]/.test(value)) throw Error(`Invalid ${name3}`);
|
|
3159
3623
|
return value.trim();
|
|
3160
3624
|
}
|
|
3161
3625
|
function threadId(value) {
|
|
3162
3626
|
return string(string(value, "thread ID").replace(/^(?:agent-)?wechat:(?:group:)?/i, ""), "thread ID");
|
|
3163
3627
|
}
|
|
3164
|
-
function messageResult(message, retrievalTool = "wechat_media") {
|
|
3628
|
+
function messageResult(message, retrievalTool = "wechat_media", quotePermitted = false) {
|
|
3165
3629
|
if (nativeRecallInfo(message)) return {
|
|
3166
3630
|
id: message.id,
|
|
3167
3631
|
threadId: message.threadId,
|
|
@@ -3176,8 +3640,15 @@ function messageResult(message, retrievalTool = "wechat_media") {
|
|
|
3176
3640
|
const isForward = message.type === 49 && message.appMessageType === 19;
|
|
3177
3641
|
const isLink = message.type === 49 && [3, 4, 5].includes(message.appMessageType ?? 0) && !!message.link;
|
|
3178
3642
|
const kind2 = isQuote ? "quote" : isForward ? "forwarded_history" : isLink ? "link_preview" : message.type === 3 ? "image" : message.type === 47 ? "sticker" : message.type === 34 ? "voice" : message.type === 43 ? "video" : message.type === 49 && (message.appMessageType == null || message.appMessageType === 6) ? "file" : "unsupported";
|
|
3179
|
-
const body = message.type === 1 || isForward || isLink ? message.body ?? "" : isQuote ?
|
|
3180
|
-
|
|
3643
|
+
const body = message.type === 1 || isForward || isLink ? message.body ?? "" : isQuote ? renderEmbeddedQuote(message, quotePermitted).body : `[WeChat ${kind2} message; attachment content has not been retrieved by this tool]`;
|
|
3644
|
+
const quote = message.quote && {
|
|
3645
|
+
serverId: message.quote.serverId,
|
|
3646
|
+
type: message.quote.type,
|
|
3647
|
+
state: quotePermitted ? message.quote.state : "not permitted",
|
|
3648
|
+
senderId: message.quote.senderId,
|
|
3649
|
+
appMessageType: message.quote.appMessageType,
|
|
3650
|
+
embeddedImage: quotePermitted ? message.quote.embeddedImage : void 0
|
|
3651
|
+
};
|
|
3181
3652
|
return {
|
|
3182
3653
|
id: message.id,
|
|
3183
3654
|
threadId: message.threadId,
|
|
@@ -3186,7 +3657,7 @@ function messageResult(message, retrievalTool = "wechat_media") {
|
|
|
3186
3657
|
direction: message.direction,
|
|
3187
3658
|
type: message.type,
|
|
3188
3659
|
appMessageType: message.appMessageType,
|
|
3189
|
-
quote
|
|
3660
|
+
quote,
|
|
3190
3661
|
link: isLink ? message.link : void 0,
|
|
3191
3662
|
createdAt: message.createdAt,
|
|
3192
3663
|
body: body.slice(0, 4e3),
|
|
@@ -3227,9 +3698,9 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3227
3698
|
if (!currentThread) return [];
|
|
3228
3699
|
}
|
|
3229
3700
|
const selectedId = selected.native.accountId;
|
|
3230
|
-
const tool = (
|
|
3231
|
-
name:
|
|
3232
|
-
label:
|
|
3701
|
+
const tool = (name3, action, description, properties, required = []) => ({
|
|
3702
|
+
name: name3,
|
|
3703
|
+
label: name3.replaceAll("_", " "),
|
|
3233
3704
|
description,
|
|
3234
3705
|
// The pinned host's tool_call wrapper JSON-serializes image blocks. Its
|
|
3235
3706
|
// public direct-only surface preserves actual image content for vision.
|
|
@@ -3237,7 +3708,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3237
3708
|
parameters: { type: "object", additionalProperties: false, properties, required },
|
|
3238
3709
|
execute: async (_callId, input, signal) => {
|
|
3239
3710
|
const args = object(input);
|
|
3240
|
-
const
|
|
3711
|
+
const guard2 = () => {
|
|
3241
3712
|
signal?.throwIfAborted();
|
|
3242
3713
|
context.assertInvocationCurrent?.();
|
|
3243
3714
|
const cfg2 = currentConfig();
|
|
@@ -3245,7 +3716,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3245
3716
|
if (!account2?.enabled || !account2.native || account2.native.accountId !== selectedId || account2.accountId !== selected.accountId || nativeConfigurationKey(account2.native) !== configurationKey) throw Error("WeChat account configuration changed; start a new turn");
|
|
3246
3717
|
return { cfg: cfg2, account: account2, native: account2.native };
|
|
3247
3718
|
};
|
|
3248
|
-
const { cfg, account, native } =
|
|
3719
|
+
const { cfg, account, native } = guard2();
|
|
3249
3720
|
if (Object.keys(args).some((key) => !Object.hasOwn(properties, key))) throw Error(archive ? "Unknown archive lookup argument" : "Unknown lookup argument; WeChat history is restricted to the current conversation");
|
|
3250
3721
|
const limit = args.limit ?? 20;
|
|
3251
3722
|
if (!Number.isInteger(limit) || limit < 1 || limit > 50) throw Error("limit must be 1\u201350");
|
|
@@ -3262,7 +3733,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3262
3733
|
const core = dependencies.core ?? getWeChatRuntime();
|
|
3263
3734
|
const allowed = async (sender) => {
|
|
3264
3735
|
if (archive) return true;
|
|
3265
|
-
const { cfg: cfg2, account: account2 } =
|
|
3736
|
+
const { cfg: cfg2, account: account2 } = guard2();
|
|
3266
3737
|
const result2 = await resolveWeChatIngress({
|
|
3267
3738
|
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress2,
|
|
3268
3739
|
cfg: cfg2,
|
|
@@ -3276,10 +3747,15 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3276
3747
|
return result2.ingress.admission === "dispatch";
|
|
3277
3748
|
};
|
|
3278
3749
|
if (!await allowed(requesterSenderId)) throw Error("WeChat conversation access is no longer allowed");
|
|
3279
|
-
|
|
3750
|
+
guard2();
|
|
3280
3751
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
3281
|
-
|
|
3752
|
+
guard2();
|
|
3282
3753
|
const accountId = native.accountId;
|
|
3754
|
+
const visibleMessage = async (message) => messageResult(
|
|
3755
|
+
message,
|
|
3756
|
+
archive ? "wechat_archive_media" : "wechat_media",
|
|
3757
|
+
!message.quote || archive || message.quote.direction === "outgoing" || !!message.quote.senderId && await allowed(message.quote.senderId)
|
|
3758
|
+
);
|
|
3283
3759
|
let result;
|
|
3284
3760
|
if (action === "threads") {
|
|
3285
3761
|
const page = await client.threads(accountId, { limit, cursor, ...query ? { query } : {} });
|
|
@@ -3292,21 +3768,20 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3292
3768
|
result = { ...page, items, scope: "local_profile", note: "Locally stored threads; coverage may be partial. This lookup does not mark threads read." };
|
|
3293
3769
|
} else if (action === "message" || action === "media") {
|
|
3294
3770
|
const message = await client.message(accountId, messageId);
|
|
3295
|
-
|
|
3771
|
+
guard2();
|
|
3296
3772
|
if (!await allowed(requesterSenderId)) throw Error("WeChat conversation access is no longer allowed");
|
|
3297
3773
|
if (message.id !== messageId || message.accountId !== accountId || !archive && message.threadId !== conversation || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("Message is outside the permitted conversation or sender policy");
|
|
3298
|
-
|
|
3774
|
+
if (action === "message") result = await visibleMessage(message);
|
|
3299
3775
|
if (action === "media") {
|
|
3300
3776
|
const checkRecall = async () => {
|
|
3301
|
-
|
|
3777
|
+
guard2();
|
|
3302
3778
|
const latest = await client.message(accountId, messageId);
|
|
3303
|
-
|
|
3779
|
+
guard2();
|
|
3304
3780
|
if (latest.id !== message.id || latest.accountId !== accountId || latest.threadId !== message.threadId || latest.serverId !== message.serverId) throw Error("Native media message identity changed");
|
|
3305
3781
|
if (nativeRecallInfo(latest)) throw new NativeApiError(410, "message_recalled", false);
|
|
3306
3782
|
if (message.quote) {
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
if (quote.state === "recalled" || quote.message && nativeRecallInfo(quote.message)) throw new NativeApiError(410, "message_recalled", false);
|
|
3783
|
+
if (latest.quote?.state === "recalled") throw new NativeApiError(410, "message_recalled", false);
|
|
3784
|
+
if (!latest.quote || JSON.stringify(latest.quote) !== JSON.stringify(message.quote)) throw new NativeApiError(409, "media_source_changed", false);
|
|
3310
3785
|
}
|
|
3311
3786
|
};
|
|
3312
3787
|
let prepared;
|
|
@@ -3321,7 +3796,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3321
3796
|
download: args.download !== false,
|
|
3322
3797
|
waitMs: 2e3,
|
|
3323
3798
|
signal,
|
|
3324
|
-
assertCurrent:
|
|
3799
|
+
assertCurrent: guard2,
|
|
3325
3800
|
allowQuotedMessage: async (original) => original.direction === "outgoing" || await allowed(original.senderId ?? void 0),
|
|
3326
3801
|
beforeMediaAccess: async () => {
|
|
3327
3802
|
if (!await allowed(requesterSenderId) || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("WeChat conversation access is no longer allowed");
|
|
@@ -3329,7 +3804,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3329
3804
|
}
|
|
3330
3805
|
});
|
|
3331
3806
|
} catch (error) {
|
|
3332
|
-
|
|
3807
|
+
guard2();
|
|
3333
3808
|
if (!(error instanceof NativeApiError)) throw error;
|
|
3334
3809
|
const details2 = {
|
|
3335
3810
|
messageId: message.id,
|
|
@@ -3340,7 +3815,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3340
3815
|
};
|
|
3341
3816
|
return { content: [{ type: "text", text: JSON.stringify(details2) }], details: details2 };
|
|
3342
3817
|
}
|
|
3343
|
-
|
|
3818
|
+
guard2();
|
|
3344
3819
|
if (!await allowed(requesterSenderId) || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("WeChat conversation access is no longer allowed");
|
|
3345
3820
|
const details = {
|
|
3346
3821
|
messageId: message.id,
|
|
@@ -3357,7 +3832,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3357
3832
|
};
|
|
3358
3833
|
const image = prepared.suppliedAssets?.find((asset) => asset.role === "model_image");
|
|
3359
3834
|
const output = image ? await imageResultFromFile({ label: "WeChat attachment", path: image.path, extraText: JSON.stringify(details), details }) : { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
3360
|
-
|
|
3835
|
+
guard2();
|
|
3361
3836
|
try {
|
|
3362
3837
|
await checkRecall();
|
|
3363
3838
|
} catch (error) {
|
|
@@ -3374,7 +3849,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3374
3849
|
for (const message of page.items) {
|
|
3375
3850
|
if (isNativeTombstone(message)) continue;
|
|
3376
3851
|
if (conversation && message.threadId !== conversation || message.accountId !== accountId) throw Error("WeChat history response crosses a conversation boundary");
|
|
3377
|
-
if (message.direction === "outgoing" || await allowed(message.senderId ?? void 0)) items.push(
|
|
3852
|
+
if (message.direction === "outgoing" || await allowed(message.senderId ?? void 0)) items.push(await visibleMessage(message));
|
|
3378
3853
|
}
|
|
3379
3854
|
result = {
|
|
3380
3855
|
...page,
|
|
@@ -3384,7 +3859,7 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3384
3859
|
note: archive ? "Account-wide local history may be partial. These reads do not mark threads read or download attachments." : "Local history may be partial. A filtered page can be empty with a nextCursor. These reads do not mark a conversation read or download attachments."
|
|
3385
3860
|
};
|
|
3386
3861
|
}
|
|
3387
|
-
|
|
3862
|
+
guard2();
|
|
3388
3863
|
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
3389
3864
|
}
|
|
3390
3865
|
});
|
|
@@ -3399,13 +3874,13 @@ function createLookupTools(context, dependencies, archive) {
|
|
|
3399
3874
|
tool(archiveNames[1], "search", "Search locally stored messages across every thread in the configured WeChat account, including other senders. Optional threadId narrows the search. Follow nextCursor with the same query and thread selection; no remote backfill or read marking.", { query: querySchema, threadId: messageIdSchema, ...pageSchema }, ["query"]),
|
|
3400
3875
|
tool(archiveNames[2], "history", "Read the locally stored history of any thread in the configured WeChat account, newest first, without sender filtering or read marking. Use wechat_archive_threads to find thread IDs.", { threadId: messageIdSchema, ...pageSchema }, ["threadId"]),
|
|
3401
3876
|
tool(archiveNames[3], "message", "Look up a stored message in any thread of the configured WeChat account by its exact opaque message ID. Use wechat_archive_media to retrieve its attachment.", { messageId: messageIdSchema }, ["messageId"]),
|
|
3402
|
-
tool(archiveNames[4], "media", "Retrieve an attachment or quoted text/image from any thread in the configured WeChat account by its message ID. Use the parent quote message ID for quotes. Returns verified managed file paths and an image preview for vision. Cached bytes first; missing media requests a bounded server-owned download unless download=false. Preserve pending/expired/unavailable results and never automatically retry retryable=false. No sends or read marking.", mediaSchema, ["messageId"])
|
|
3877
|
+
tool(archiveNames[4], "media", "Retrieve an attachment or quoted text/image/link from any thread in the configured WeChat account by its message ID. Use the parent quote message ID for quotes. Returns verified managed file paths and an image preview for vision. Cached bytes first; missing media requests a bounded server-owned download unless download=false. Preserve pending/expired/unavailable results and never automatically retry retryable=false. No sends or read marking.", mediaSchema, ["messageId"])
|
|
3403
3878
|
];
|
|
3404
3879
|
return [
|
|
3405
3880
|
tool(names[0], "history", "Read the current WeChat conversation's locally stored messages, newest first, beyond the recent inbound buffer. Follow nextCursor for older history; no remote history backfill or read marking.", { ...pageSchema }),
|
|
3406
3881
|
tool(names[1], "search", "Search the current WeChat conversation's locally stored message text by case-insensitive substring, beyond buffered messages. Follow nextCursor with the same query in this conversation.", { query: querySchema, ...pageSchema }, ["query"]),
|
|
3407
3882
|
tool(names[2], "message", "Look up a stored message by its exact opaque message ID in the current WeChat conversation. Media is described without downloading it; use wechat_media to retrieve its attachment. Long text is bounded.", { messageId: messageIdSchema }, ["messageId"]),
|
|
3408
|
-
tool(names[3], "media", "Retrieve a message attachment or quoted text/image from the current WeChat conversation. Use the parent quote message ID for quotes. Returns a verified local file path and an image preview for vision when applicable. Uses cached bytes first; download defaults to true and requests a bounded server-owned transfer if missing. Pending results may be checked again; never automatically retry confirmed expiry or retryable=false. Does not send messages or mark the thread read.", mediaSchema, ["messageId"])
|
|
3883
|
+
tool(names[3], "media", "Retrieve a message attachment or quoted text/image/link from the current WeChat conversation. Use the parent quote message ID for quotes. Returns a verified local file path and an image preview for vision when applicable. Uses cached bytes first; download defaults to true and requests a bounded server-owned transfer if missing. Pending results may be checked again; never automatically retry confirmed expiry or retryable=false. Does not send messages or mark the thread read.", mediaSchema, ["messageId"])
|
|
3409
3884
|
];
|
|
3410
3885
|
}
|
|
3411
3886
|
function registerNativeHistoryTools(api) {
|
|
@@ -3471,7 +3946,7 @@ function createNativeShareTools(context, dependencies = {}) {
|
|
|
3471
3946
|
...args.description !== void 0 ? { description: args.description === "" ? "" : text(args.description, "description", 16384, true) } : {},
|
|
3472
3947
|
...args.thumbnailUrl !== void 0 ? { thumbnailUrl: url(args.thumbnailUrl, "thumbnail URL") } : {}
|
|
3473
3948
|
};
|
|
3474
|
-
const
|
|
3949
|
+
const guard2 = () => {
|
|
3475
3950
|
signal?.throwIfAborted();
|
|
3476
3951
|
context.assertInvocationCurrent?.();
|
|
3477
3952
|
const cfg = currentConfig(), account = resolveWeChatAccount(cfg);
|
|
@@ -3480,7 +3955,7 @@ function createNativeShareTools(context, dependencies = {}) {
|
|
|
3480
3955
|
};
|
|
3481
3956
|
const core = dependencies.core ?? getWeChatRuntime();
|
|
3482
3957
|
const authorize = async () => {
|
|
3483
|
-
const { cfg, account } =
|
|
3958
|
+
const { cfg, account } = guard2();
|
|
3484
3959
|
const result = await resolveWeChatIngress({
|
|
3485
3960
|
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress3,
|
|
3486
3961
|
cfg,
|
|
@@ -3491,11 +3966,11 @@ function createNativeShareTools(context, dependencies = {}) {
|
|
|
3491
3966
|
senderId: requester,
|
|
3492
3967
|
readStoreAllowFrom: async () => core.channel.pairing.readAllowFromStore({ channel: "agent-wechat", accountId: account.accountId, env: process.env }).catch(() => [])
|
|
3493
3968
|
});
|
|
3494
|
-
|
|
3969
|
+
guard2();
|
|
3495
3970
|
if (result.ingress.admission !== "dispatch") throw Error("WeChat conversation access is no longer allowed");
|
|
3496
3971
|
};
|
|
3497
3972
|
await authorize();
|
|
3498
|
-
const { native } =
|
|
3973
|
+
const { native } = guard2();
|
|
3499
3974
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
3500
3975
|
await authorize();
|
|
3501
3976
|
let intent;
|
|
@@ -3530,7 +4005,7 @@ function registerNativeShareTools(api) {
|
|
|
3530
4005
|
}
|
|
3531
4006
|
|
|
3532
4007
|
// src/native-recall-tools.ts
|
|
3533
|
-
import { createHash as
|
|
4008
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
3534
4009
|
import { resolveStableChannelMessageIngress as resolveStableChannelMessageIngress4 } from "openclaw/plugin-sdk/channel-ingress-runtime";
|
|
3535
4010
|
var names2 = ["wechat_recall"];
|
|
3536
4011
|
function text2(value, label) {
|
|
@@ -3541,7 +4016,7 @@ function thread2(value) {
|
|
|
3541
4016
|
return text2(text2(value, "conversation").replace(/^(?:agent-)?wechat:(?:group:)?/i, ""), "conversation");
|
|
3542
4017
|
}
|
|
3543
4018
|
function nativeRecallKey(accountId, threadId2, messageId, serverId) {
|
|
3544
|
-
return `openclaw-recall-${
|
|
4019
|
+
return `openclaw-recall-${createHash10("sha256").update(JSON.stringify([accountId, threadId2, messageId, serverId])).digest("hex")}`;
|
|
3545
4020
|
}
|
|
3546
4021
|
function createNativeRecallTools(context, dependencies = {}) {
|
|
3547
4022
|
const currentConfig = () => context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
@@ -3573,7 +4048,7 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
3573
4048
|
const messageId = text2(input.messageId, "message ID");
|
|
3574
4049
|
const serverId = text2(input.serverId, "server ID");
|
|
3575
4050
|
if (!/^[1-9]\d{0,19}$/.test(serverId) || BigInt(serverId) > 18446744073709551615n) throw Error("Invalid exact server message ID");
|
|
3576
|
-
const
|
|
4051
|
+
const guard2 = () => {
|
|
3577
4052
|
signal?.throwIfAborted();
|
|
3578
4053
|
context.assertInvocationCurrent?.();
|
|
3579
4054
|
const cfg = currentConfig(), account = resolveWeChatAccount(cfg);
|
|
@@ -3581,7 +4056,7 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
3581
4056
|
return { cfg, account, native: account.native };
|
|
3582
4057
|
};
|
|
3583
4058
|
const allowed = async () => {
|
|
3584
|
-
const { cfg, account } =
|
|
4059
|
+
const { cfg, account } = guard2();
|
|
3585
4060
|
const core = dependencies.core ?? getWeChatRuntime();
|
|
3586
4061
|
const ingress = await resolveWeChatIngress({
|
|
3587
4062
|
resolve: core.channel.inbound?.ingress?.resolveStable ?? resolveStableChannelMessageIngress4,
|
|
@@ -3593,20 +4068,20 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
3593
4068
|
senderId: requester,
|
|
3594
4069
|
readStoreAllowFrom: async () => core.channel.pairing.readAllowFromStore({ channel: "agent-wechat", accountId: account.accountId, env: process.env }).catch(() => [])
|
|
3595
4070
|
});
|
|
3596
|
-
|
|
4071
|
+
guard2();
|
|
3597
4072
|
if (ingress.ingress.admission !== "dispatch") throw Error("WeChat conversation access is no longer allowed");
|
|
3598
4073
|
};
|
|
3599
4074
|
await allowed();
|
|
3600
|
-
const { native } =
|
|
4075
|
+
const { native } = guard2();
|
|
3601
4076
|
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
3602
|
-
|
|
4077
|
+
guard2();
|
|
3603
4078
|
const message = await client.message(accountId, messageId);
|
|
3604
|
-
|
|
4079
|
+
guard2();
|
|
3605
4080
|
const retainedRecall = !!nativeRecallInfo(message);
|
|
3606
4081
|
if (message.id !== messageId || message.accountId !== accountId || message.threadId !== conversation || message.direction !== "outgoing" && !retainedRecall) throw Error("Recall target is outside this conversation or was not sent by this account");
|
|
3607
4082
|
if (message.serverId !== serverId) throw Error("The exact server identity does not match this receipt; the local message identity may have been reused");
|
|
3608
4083
|
await allowed();
|
|
3609
|
-
|
|
4084
|
+
guard2();
|
|
3610
4085
|
const key = nativeRecallKey(accountId, conversation, messageId, serverId);
|
|
3611
4086
|
let operation;
|
|
3612
4087
|
let operationId;
|
|
@@ -3636,13 +4111,13 @@ function createNativeRecallTools(context, dependencies = {}) {
|
|
|
3636
4111
|
const deadline = Date.now() + Math.max(0, Math.min(dependencies.waitMs ?? 2e3, 1e4));
|
|
3637
4112
|
while (["queued", "running", "submitting"].includes(operation.state) && Date.now() < deadline) {
|
|
3638
4113
|
await new Promise((resolve) => setTimeout(resolve, Math.min(250, deadline - Date.now())));
|
|
3639
|
-
|
|
4114
|
+
guard2();
|
|
3640
4115
|
const remaining = Math.max(1, deadline - Date.now());
|
|
3641
4116
|
const next = await client.operation(accountId, operation.id, signal ? AbortSignal.any([signal, AbortSignal.timeout(remaining)]) : AbortSignal.timeout(remaining));
|
|
3642
4117
|
checkOperation(next);
|
|
3643
4118
|
operation = next;
|
|
3644
4119
|
}
|
|
3645
|
-
|
|
4120
|
+
guard2();
|
|
3646
4121
|
} catch (error) {
|
|
3647
4122
|
const reason = error instanceof NativeApiError ? error.code : "recall_result_unavailable";
|
|
3648
4123
|
return result("needs_review", { reason, note: "Inspect the existing operation or repeat only this same message with the same idempotency key. No automatic second submission was made." });
|
|
@@ -3664,6 +4139,55 @@ function registerNativeRecallTools(api) {
|
|
|
3664
4139
|
api.registerTool({ contextVersion: 2, create: (context) => createNativeRecallTools(context) }, { names: names2 });
|
|
3665
4140
|
}
|
|
3666
4141
|
|
|
4142
|
+
// src/native-group-tool.ts
|
|
4143
|
+
var name2 = "wechat_group_info";
|
|
4144
|
+
function createNativeGroupTools(context, dependencies = {}) {
|
|
4145
|
+
if (context.senderIsOwner !== true || typeof context.assertInvocationCurrent !== "function") return [];
|
|
4146
|
+
const currentConfig = () => context.getRuntimeConfig?.() ?? context.runtimeConfig ?? context.config;
|
|
4147
|
+
const selected = resolveWeChatAccount(currentConfig());
|
|
4148
|
+
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 [];
|
|
4149
|
+
const accountId = selected.native.accountId;
|
|
4150
|
+
const configurationKey = nativeConfigurationKey(selected.native);
|
|
4151
|
+
return [{
|
|
4152
|
+
name: name2,
|
|
4153
|
+
label: "WeChat Group Info",
|
|
4154
|
+
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.",
|
|
4155
|
+
parameters: { type: "object", additionalProperties: false, properties: {
|
|
4156
|
+
groupId: { type: "string", minLength: 10, maxLength: 256, description: "Exact ordinary group username ending in @chatroom." },
|
|
4157
|
+
limit: { type: "integer", minimum: 1, maximum: 100 },
|
|
4158
|
+
cursor: { type: "string", maxLength: 8192 },
|
|
4159
|
+
username: { type: "string", minLength: 1, maxLength: 256, description: "Optionally inspect one exact member instead of listing a page." }
|
|
4160
|
+
}, required: ["groupId"] },
|
|
4161
|
+
execute: async (_callId, input, signal) => {
|
|
4162
|
+
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");
|
|
4163
|
+
const args = input;
|
|
4164
|
+
const identity = (value) => typeof value === "string" && value.length > 0 && value.length <= 256 && !/[\s\u0000-\u001f\u007f]/u.test(value);
|
|
4165
|
+
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");
|
|
4166
|
+
const guard2 = () => {
|
|
4167
|
+
signal?.throwIfAborted();
|
|
4168
|
+
if (context.senderIsOwner !== true) throw Error("OpenClaw owner authorization is required");
|
|
4169
|
+
context.assertInvocationCurrent();
|
|
4170
|
+
const account = resolveWeChatAccount(currentConfig());
|
|
4171
|
+
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");
|
|
4172
|
+
return account.native;
|
|
4173
|
+
};
|
|
4174
|
+
const native = guard2();
|
|
4175
|
+
const { client } = await (dependencies.host ?? ensureNativeHost)(native);
|
|
4176
|
+
guard2();
|
|
4177
|
+
const group = await client.group(accountId, args.groupId, signal);
|
|
4178
|
+
guard2();
|
|
4179
|
+
if (group.id !== args.groupId || group.roomId !== args.groupId) throw Error("Group identity is invalid");
|
|
4180
|
+
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);
|
|
4181
|
+
guard2();
|
|
4182
|
+
const details = { accountId, group, members, source: "cached", membershipAbsence: "unconfirmed" };
|
|
4183
|
+
return { content: [{ type: "text", text: JSON.stringify(details) }], details };
|
|
4184
|
+
}
|
|
4185
|
+
}];
|
|
4186
|
+
}
|
|
4187
|
+
function registerNativeGroupTools(api) {
|
|
4188
|
+
api.registerTool({ contextVersion: 2, create: (context) => createNativeGroupTools(context) }, { names: [name2] });
|
|
4189
|
+
}
|
|
4190
|
+
|
|
3667
4191
|
// index.ts
|
|
3668
4192
|
var plugin = {
|
|
3669
4193
|
id: "agent-wechat",
|
|
@@ -3677,6 +4201,8 @@ var plugin = {
|
|
|
3677
4201
|
registerNativeHistoryTools(api);
|
|
3678
4202
|
registerNativeShareTools(api);
|
|
3679
4203
|
registerNativeRecallTools(api);
|
|
4204
|
+
registerNativeGroupTools(api);
|
|
4205
|
+
registerNativeLoginTool(api);
|
|
3680
4206
|
const stopFacade = selected?.native ? registerNativeFacade(api, selected.native) : () => {
|
|
3681
4207
|
};
|
|
3682
4208
|
const stopPreparedBoundary = selected?.native?.readPolicy && selected.native.readPolicy !== "on_inbound" ? registerNativePreparedBoundary(api) : () => {
|