@agent-wechat/agent-wechat 1.0.0-beta.6 → 1.0.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +134 -20
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -377,6 +377,13 @@ var NativeWeChatClient = class {
377
377
  throw new Error("Invalid native message identity");
378
378
  return message;
379
379
  }
380
+ /** Resolve only the stored original in this message's own conversation. */
381
+ async quotedMessage(accountId, messageId) {
382
+ const quote = await this.request(`v1/accounts/${id(accountId)}/messages/${id(messageId)}/quote`);
383
+ 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))
384
+ throw new Error("Invalid native quote identity");
385
+ return quote;
386
+ }
380
387
  /** Account-wide local search, with the daemon's query-bound opaque cursor. */
381
388
  async searchMessages(accountId, options) {
382
389
  const result = await this.request(`v1/accounts/${id(accountId)}/messages${listQuery(options)}`);
@@ -441,15 +448,16 @@ var NativeWeChatClient = class {
441
448
  }
442
449
  async mediaContent(accountId, assetId, options = {}) {
443
450
  const max = options.maxBytes ?? 128 * 1024 * 1024;
444
- if (!Number.isInteger(max) || max < 1 || max > 128 * 1024 * 1024 || options.range && !/^bytes=(?:\d+-\d*|-\d+)$/.test(options.range))
451
+ 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))
445
452
  throw new Error("Invalid bounded content request");
446
- const response = await this.response(`v1/accounts/${id(accountId)}/media/${id(assetId)}/content`, { signal: options.signal, headers: options.range ? { Range: options.range } : {} });
453
+ const query = options.representation === "preview" ? "?representation=preview" : "";
454
+ const response = await this.response(`v1/accounts/${id(accountId)}/media/${id(assetId)}/content${query}`, { signal: options.signal, headers: options.range ? { Range: options.range } : {} });
447
455
  const length = response.headers.get("content-length");
448
456
  if (length && (!/^\d+$/.test(length) || Number(length) > max)) {
449
457
  await response.body?.cancel();
450
458
  throw new Error("Native media response exceeds its bound");
451
459
  }
452
- return { bytes: await this.readBounded(response, max), contentType: response.headers.get("content-type") ?? "application/octet-stream", etag: response.headers.get("etag"), contentRange: response.headers.get("content-range"), status: response.status };
460
+ return { bytes: await this.readBounded(response, max), 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 };
453
461
  }
454
462
  markRead(accountId, threadId2, expectedVersion) {
455
463
  if (typeof expectedVersion !== "string" || !expectedVersion || expectedVersion.length > 128)
@@ -1095,6 +1103,84 @@ async function prepareImageRepresentation(bytes, mime, cfg) {
1095
1103
  }
1096
1104
  throw Error("Native model image representation exceeds its bound");
1097
1105
  }
1106
+ async function prepareNativeQuote(params) {
1107
+ const { client, account, message } = params;
1108
+ const guard = () => {
1109
+ params.signal?.throwIfAborted();
1110
+ params.assertCurrent?.();
1111
+ };
1112
+ guard();
1113
+ const comment = (message.body ?? "").slice(0, 16384);
1114
+ const unavailable = (state) => ({ body: `${comment}${comment ? "\n\n" : ""}[Quoted message ${state}; original lookup is confined to this chat]`, pending: false, retryable: state === "unavailable", quote: { state } });
1115
+ if (!message.quote) return unavailable("invalid");
1116
+ await params.beforeMediaAccess?.();
1117
+ guard();
1118
+ let resolution;
1119
+ try {
1120
+ resolution = await client.quotedMessage(account.native.accountId, message.id);
1121
+ } catch (error) {
1122
+ guard();
1123
+ if (!(error instanceof NativeApiError)) throw error;
1124
+ return unavailable("unavailable");
1125
+ }
1126
+ guard();
1127
+ if (resolution.accountId !== message.accountId || resolution.messageId !== message.id || resolution.threadId !== message.threadId) throw Error("Native quote identity does not match its message");
1128
+ if (resolution.state === "embedded") {
1129
+ 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");
1130
+ const author = { senderId: resolution.senderId ?? null, direction: null };
1131
+ if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) return unavailable("not permitted");
1132
+ guard();
1133
+ const quote2 = { state: "embedded", senderId: resolution.senderId ?? void 0, type: message.quote.type };
1134
+ const prefix2 = `${comment}${comment ? "\n\n" : ""}[Quoted ${message.quote.type === 3 ? "image" : "text"} from ${resolution.senderId ?? "unknown sender"}; embedded in quote, original not stored]
1135
+ `;
1136
+ if (message.quote.type === 1) return { body: `${prefix2}${resolution.embeddedText}
1137
+ [/Quoted message]`, pending: false, quote: quote2 };
1138
+ const beforeMediaAccess2 = async () => {
1139
+ await params.beforeMediaAccess?.();
1140
+ guard();
1141
+ if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) throw Error("Quoted message is no longer permitted");
1142
+ guard();
1143
+ };
1144
+ const prepared2 = await prepareNativeAttachment({ ...params, message: { ...message, type: 3, appMessageType: void 0, quote: void 0 }, beforeMediaAccess: beforeMediaAccess2 });
1145
+ return {
1146
+ ...prepared2,
1147
+ body: `${prefix2}${prepared2.body}
1148
+ [/Quoted message]`,
1149
+ pending: false,
1150
+ quote: quote2,
1151
+ ...prepared2.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {}
1152
+ };
1153
+ }
1154
+ if (resolution.state !== "resolved") {
1155
+ if (!["missing", "invalid", "unsupported", "ambiguous"].includes(resolution.state) || resolution.message != null) throw Error("Invalid native quote resolution");
1156
+ return unavailable(resolution.state);
1157
+ }
1158
+ const original = resolution.message;
1159
+ 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");
1160
+ if (params.allowQuotedMessage && !await params.allowQuotedMessage(original)) return unavailable("not permitted");
1161
+ guard();
1162
+ const quote = { state: "resolved", messageId: original.id, senderId: original.senderId ?? void 0, type: original.type };
1163
+ const prefix = `${comment}${comment ? "\n\n" : ""}[Quoted ${original.type === 3 ? "image" : "text"} from ${original.senderId ?? "unknown sender"}; message ${original.id}]
1164
+ `;
1165
+ if (original.type === 1) return { body: `${prefix}${(original.body ?? "").slice(0, 16384)}
1166
+ [/Quoted message]`, pending: false, quote };
1167
+ const beforeMediaAccess = async () => {
1168
+ await params.beforeMediaAccess?.();
1169
+ guard();
1170
+ if (params.allowQuotedMessage && !await params.allowQuotedMessage(original)) throw Error("Quoted message is no longer permitted");
1171
+ guard();
1172
+ };
1173
+ const prepared = await prepareNativeAttachment({ ...params, message: original, beforeMediaAccess });
1174
+ return {
1175
+ ...prepared,
1176
+ body: `${prefix}${prepared.body}
1177
+ [/Quoted message]`,
1178
+ pending: false,
1179
+ quote,
1180
+ ...prepared.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {},
1181
+ suppliedAssets: prepared.suppliedAssets?.map((asset) => ({ ...asset, sourceMessageId: original.id }))
1182
+ };
1183
+ }
1098
1184
  async function prepareNativeAttachment(params) {
1099
1185
  const { client, account, message, core } = params;
1100
1186
  const attachmentKind = kind(message.type);
@@ -1108,6 +1194,9 @@ async function prepareNativeAttachment(params) {
1108
1194
  await params.beforeMediaAccess?.();
1109
1195
  guard();
1110
1196
  };
1197
+ if (message.type === 49 && message.appMessageType === 57) return prepareNativeQuote(params);
1198
+ if (message.type === 49 && message.appMessageType === 19) return { body: message.body?.slice(0, 65536) || "[Forwarded chat history unavailable]", pending: false };
1199
+ if (message.type === 49 && message.appMessageType != null && message.appMessageType !== 6) return { body: `[Unsupported WeChat app message subtype ${message.appMessageType}]`, pending: false, retryable: false };
1111
1200
  if (!attachmentKind) return { body: message.type === 1 ? message.body ?? "" : `[Unsupported WeChat message type ${message.type}]`, pending: false };
1112
1201
  const accountId = account.native.accountId;
1113
1202
  const maxBytes = Math.min(account.mediaMaxMb * 1024 * 1024, 128 * 1024 * 1024);
@@ -1197,13 +1286,27 @@ async function prepareNativeAttachment(params) {
1197
1286
  };
1198
1287
  let suppliedPath = saved.path, suppliedMime = mime, suppliedHash = hash, suppliedLength = content.bytes.length;
1199
1288
  let body = renderAttachmentBody("", attachment);
1200
- if (attachmentKind === "image" && params.cfg) {
1201
- const representation = await prepareImageRepresentation(Buffer.from(content.bytes), mime, params.cfg);
1202
- if (representation.preview) {
1289
+ if (attachmentKind === "image" && (params.cfg || mime === "image/x-wxgf")) {
1290
+ let imageBytes = Buffer.from(content.bytes), imageMime = mime;
1291
+ const wxgf = mime === "image/x-wxgf";
1292
+ if (wxgf) {
1293
+ const preview = asset.preview;
1294
+ 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");
1295
+ await authorize();
1296
+ const derived = await client.mediaContent(accountId, asset.id, { maxBytes: preview.byteLength, representation: "preview", signal: params.signal });
1297
+ guard();
1298
+ const derivedMime = derived.contentType.split(";", 1)[0].trim().toLowerCase();
1299
+ 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");
1300
+ imageBytes = Buffer.from(derived.bytes);
1301
+ imageMime = derivedMime;
1302
+ }
1303
+ const representation = await prepareImageRepresentation(imageBytes, imageMime, params.cfg);
1304
+ if (wxgf || representation.preview) {
1305
+ const extension = representation.mime === "image/png" ? "png" : "jpg";
1203
1306
  const normalized = await saveManagedAttachment({
1204
1307
  data: representation.bytes.toString("base64"),
1205
1308
  mime: representation.mime,
1206
- filename: `${filename}.model.jpg`,
1309
+ filename: `${filename}.model.${extension}`,
1207
1310
  maxBytes: 5 * 1024 * 1024,
1208
1311
  saveMediaBuffer: core.channel.media.saveMediaBuffer
1209
1312
  });
@@ -1211,7 +1314,8 @@ async function prepareNativeAttachment(params) {
1211
1314
  suppliedMime = representation.mime;
1212
1315
  suppliedHash = normalized.sha256;
1213
1316
  suppliedLength = normalized.size;
1214
- body += `
1317
+ body += wxgf ? `
1318
+ [Model image: WXGF still preview; original retained separately; animation and transparency are not represented]` : `
1215
1319
  [Model image: prepared still preview; original retained separately]`;
1216
1320
  }
1217
1321
  }
@@ -1511,7 +1615,7 @@ function nativeDispatchManifest(params) {
1511
1615
  const history = params.history ?? [];
1512
1616
  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");
1513
1617
  const assets = Object.freeze((params.assets ?? []).map((asset) => {
1514
- 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.contextRole === "history" && !history.some((message) => message.id === asset.messageId)) throw Error("Invalid native supplied asset manifest");
1618
+ 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");
1515
1619
  return Object.freeze({ ...asset });
1516
1620
  }));
1517
1621
  const messageSid = `wechat-native:${params.message.accountId}:${params.message.id}`;
@@ -1537,14 +1641,16 @@ function nativeDispatchManifest(params) {
1537
1641
  params.message.type,
1538
1642
  params.message.body,
1539
1643
  params.message.mentions,
1540
- params.message.serverId
1644
+ params.message.serverId,
1645
+ params.message.appMessageType,
1646
+ params.message.quote
1541
1647
  ])),
1542
1648
  role: "trigger"
1543
1649
  })]),
1544
1650
  ...history.length ? { historyInputs: Object.freeze(history.map((message) => Object.freeze({
1545
1651
  id: message.id,
1546
1652
  version: message.version ?? "",
1547
- contentSha256: digest(JSON.stringify([message.id, message.version, message.senderId, message.type, message.body, message.mentions, message.serverId])),
1653
+ contentSha256: digest(JSON.stringify([message.id, message.version, message.senderId, message.type, message.body, message.mentions, message.serverId, message.appMessageType, message.quote])),
1548
1654
  role: "history"
1549
1655
  }))) } : {},
1550
1656
  assets,
@@ -1842,8 +1948,9 @@ async function dispatchNativeMessage(params) {
1842
1948
  return "ignored";
1843
1949
  }
1844
1950
  let preparedMedia;
1951
+ const allowQuotedMessage = async (original) => original.direction === "outgoing" || !!original.senderId && (await senderAdmission(original.senderId)).ingress.admission === "dispatch";
1845
1952
  try {
1846
- preparedMedia = await (params.prepareAttachment ?? prepareNativeAttachment)({ client, account, message, core, cfg });
1953
+ preparedMedia = await (params.prepareAttachment ?? prepareNativeAttachment)({ client, account, message, core, cfg, allowQuotedMessage });
1847
1954
  } catch {
1848
1955
  state.update(message.id, { state: "needs_review" });
1849
1956
  return "needs_review";
@@ -1856,7 +1963,7 @@ async function dispatchNativeMessage(params) {
1856
1963
  if (item.message.senderId && (await senderAdmission(item.message.senderId)).ingress.admission === "dispatch") admitted.push(item.message);
1857
1964
  }
1858
1965
  const history = await prepareNativeHistory(admitted, async (historical) => transcribeNativeAttachment(
1859
- await (params.prepareAttachment ?? prepareNativeAttachment)({ client, account, message: historical, core, cfg }),
1966
+ await (params.prepareAttachment ?? prepareNativeAttachment)({ client, account, message: historical, core, cfg, allowQuotedMessage }),
1860
1967
  core,
1861
1968
  cfg
1862
1969
  ));
@@ -1912,7 +2019,7 @@ async function dispatchNativeMessage(params) {
1912
2019
  history: admitted
1913
2020
  });
1914
2021
  state.beginDispatch(message.id, dispatchManifest, buffered.map((item) => item.message.id));
1915
- const callback = readPolicy !== "on_inbound" && (message.type === 1 || preparedMedia.attachment?.status === "ready" && preparedMedia.suppliedAssets?.length) ? state.preparedCallback([message.id], readPolicy) : void 0;
2022
+ const callback = readPolicy !== "on_inbound" && (message.type === 1 || message.type === 49 && message.appMessageType === 19 && !!message.body || preparedMedia.quote?.type === 1 && ["resolved", "embedded"].includes(preparedMedia.quote.state) || preparedMedia.attachment?.status === "ready" && preparedMedia.suppliedAssets?.length) ? state.preparedCallback([message.id], readPolicy) : void 0;
1916
2023
  if (callback) trackNativePreparedInput(dispatchManifest, callback);
1917
2024
  let failed = false;
1918
2025
  let delivery = 0;
@@ -2590,8 +2697,11 @@ function threadId(value) {
2590
2697
  return string(string(value, "thread ID").replace(/^(?:agent-)?wechat:(?:group:)?/i, ""), "thread ID");
2591
2698
  }
2592
2699
  function messageResult(message, retrievalTool = "wechat_media") {
2593
- const kind2 = message.type === 3 ? "image" : message.type === 47 ? "sticker" : message.type === 34 ? "voice" : message.type === 43 ? "video" : message.type === 49 ? "file" : "unsupported";
2594
- const body = message.type === 1 ? message.body ?? "" : `[WeChat ${kind2} message; attachment content has not been retrieved by this tool]`;
2700
+ const isQuote = message.type === 49 && message.appMessageType === 57;
2701
+ const isForward = message.type === 49 && message.appMessageType === 19;
2702
+ const kind2 = isQuote ? "quote" : isForward ? "forwarded_history" : 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";
2703
+ const body = message.type === 1 || isForward ? message.body ?? "" : isQuote ? `${message.body ?? ""}
2704
+ [Quoted original not retrieved; use ${retrievalTool} with this message ID]` : `[WeChat ${kind2} message; attachment content has not been retrieved by this tool]`;
2595
2705
  return {
2596
2706
  id: message.id,
2597
2707
  threadId: message.threadId,
@@ -2599,10 +2709,12 @@ function messageResult(message, retrievalTool = "wechat_media") {
2599
2709
  senderId: message.senderId,
2600
2710
  direction: message.direction,
2601
2711
  type: message.type,
2712
+ appMessageType: message.appMessageType,
2713
+ quote: message.quote,
2602
2714
  createdAt: message.createdAt,
2603
2715
  body: body.slice(0, 4e3),
2604
2716
  bodyTruncated: body.length > 4e3,
2605
- attachment: message.type !== 1 && kind2 !== "unsupported" ? { kind: kind2, retrievalTool, messageId: message.id } : void 0
2717
+ attachment: message.type !== 1 && !isForward && kind2 !== "unsupported" ? { kind: kind2, retrievalTool, messageId: message.id } : void 0
2606
2718
  };
2607
2719
  }
2608
2720
  function createNativeHistoryTools(context, dependencies = {}) {
@@ -2720,6 +2832,7 @@ function createLookupTools(context, dependencies, archive) {
2720
2832
  waitMs: 2e3,
2721
2833
  signal,
2722
2834
  assertCurrent: guard,
2835
+ allowQuotedMessage: async (original) => original.direction === "outgoing" || await allowed(original.senderId ?? void 0),
2723
2836
  beforeMediaAccess: async () => {
2724
2837
  if (!await allowed(requesterSenderId) || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("WeChat conversation access is no longer allowed");
2725
2838
  }
@@ -2741,8 +2854,9 @@ function createLookupTools(context, dependencies, archive) {
2741
2854
  const details = {
2742
2855
  messageId: message.id,
2743
2856
  threadId: message.threadId,
2744
- status: prepared.pending ? "pending" : prepared.attachment?.status ?? "unsupported",
2857
+ status: prepared.pending ? "pending" : prepared.attachment?.status ?? prepared.quote?.state ?? "unsupported",
2745
2858
  attachment: prepared.attachment,
2859
+ quote: prepared.quote,
2746
2860
  variant: prepared.variant,
2747
2861
  jobId: prepared.jobId,
2748
2862
  retryable: prepared.retryable ?? false,
@@ -2786,13 +2900,13 @@ function createLookupTools(context, dependencies, archive) {
2786
2900
  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"]),
2787
2901
  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"]),
2788
2902
  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"]),
2789
- tool(archiveNames[4], "media", "Retrieve an attachment from any thread in the configured WeChat account by its message ID. 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"])
2903
+ 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"])
2790
2904
  ];
2791
2905
  return [
2792
2906
  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 }),
2793
2907
  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"]),
2794
2908
  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"]),
2795
- tool(names[3], "media", "Retrieve a message attachment from the current WeChat conversation. 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"])
2909
+ 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"])
2796
2910
  ];
2797
2911
  }
2798
2912
  function registerNativeHistoryTools(api) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-wechat/agent-wechat",
3
- "version": "1.0.0-beta.6",
3
+ "version": "1.0.0-beta.8",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",