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

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 +110 -12
  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)}`);
@@ -1095,6 +1102,84 @@ async function prepareImageRepresentation(bytes, mime, cfg) {
1095
1102
  }
1096
1103
  throw Error("Native model image representation exceeds its bound");
1097
1104
  }
1105
+ async function prepareNativeQuote(params) {
1106
+ const { client, account, message } = params;
1107
+ const guard = () => {
1108
+ params.signal?.throwIfAborted();
1109
+ params.assertCurrent?.();
1110
+ };
1111
+ guard();
1112
+ const comment = (message.body ?? "").slice(0, 16384);
1113
+ 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 } });
1114
+ if (!message.quote) return unavailable("invalid");
1115
+ await params.beforeMediaAccess?.();
1116
+ guard();
1117
+ let resolution;
1118
+ try {
1119
+ resolution = await client.quotedMessage(account.native.accountId, message.id);
1120
+ } catch (error) {
1121
+ guard();
1122
+ if (!(error instanceof NativeApiError)) throw error;
1123
+ return unavailable("unavailable");
1124
+ }
1125
+ guard();
1126
+ if (resolution.accountId !== message.accountId || resolution.messageId !== message.id || resolution.threadId !== message.threadId) throw Error("Native quote identity does not match its message");
1127
+ if (resolution.state === "embedded") {
1128
+ 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");
1129
+ const author = { senderId: resolution.senderId ?? null, direction: null };
1130
+ if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) return unavailable("not permitted");
1131
+ guard();
1132
+ const quote2 = { state: "embedded", senderId: resolution.senderId ?? void 0, type: message.quote.type };
1133
+ const prefix2 = `${comment}${comment ? "\n\n" : ""}[Quoted ${message.quote.type === 3 ? "image" : "text"} from ${resolution.senderId ?? "unknown sender"}; embedded in quote, original not stored]
1134
+ `;
1135
+ if (message.quote.type === 1) return { body: `${prefix2}${resolution.embeddedText}
1136
+ [/Quoted message]`, pending: false, quote: quote2 };
1137
+ const beforeMediaAccess2 = async () => {
1138
+ await params.beforeMediaAccess?.();
1139
+ guard();
1140
+ if (params.allowQuotedMessage && !await params.allowQuotedMessage(author)) throw Error("Quoted message is no longer permitted");
1141
+ guard();
1142
+ };
1143
+ const prepared2 = await prepareNativeAttachment({ ...params, message: { ...message, type: 3, appMessageType: void 0, quote: void 0 }, beforeMediaAccess: beforeMediaAccess2 });
1144
+ return {
1145
+ ...prepared2,
1146
+ body: `${prefix2}${prepared2.body}
1147
+ [/Quoted message]`,
1148
+ pending: false,
1149
+ quote: quote2,
1150
+ ...prepared2.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {}
1151
+ };
1152
+ }
1153
+ if (resolution.state !== "resolved") {
1154
+ if (!["missing", "invalid", "unsupported", "ambiguous"].includes(resolution.state) || resolution.message != null) throw Error("Invalid native quote resolution");
1155
+ return unavailable(resolution.state);
1156
+ }
1157
+ const original = resolution.message;
1158
+ 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");
1159
+ if (params.allowQuotedMessage && !await params.allowQuotedMessage(original)) return unavailable("not permitted");
1160
+ guard();
1161
+ const quote = { state: "resolved", messageId: original.id, senderId: original.senderId ?? void 0, type: original.type };
1162
+ const prefix = `${comment}${comment ? "\n\n" : ""}[Quoted ${original.type === 3 ? "image" : "text"} from ${original.senderId ?? "unknown sender"}; message ${original.id}]
1163
+ `;
1164
+ if (original.type === 1) return { body: `${prefix}${(original.body ?? "").slice(0, 16384)}
1165
+ [/Quoted message]`, pending: false, quote };
1166
+ const beforeMediaAccess = async () => {
1167
+ await params.beforeMediaAccess?.();
1168
+ guard();
1169
+ if (params.allowQuotedMessage && !await params.allowQuotedMessage(original)) throw Error("Quoted message is no longer permitted");
1170
+ guard();
1171
+ };
1172
+ const prepared = await prepareNativeAttachment({ ...params, message: original, beforeMediaAccess });
1173
+ return {
1174
+ ...prepared,
1175
+ body: `${prefix}${prepared.body}
1176
+ [/Quoted message]`,
1177
+ pending: false,
1178
+ quote,
1179
+ ...prepared.pending ? { attachment: { kind: "image", status: "pending", filename: "quoted-image" } } : {},
1180
+ suppliedAssets: prepared.suppliedAssets?.map((asset) => ({ ...asset, sourceMessageId: original.id }))
1181
+ };
1182
+ }
1098
1183
  async function prepareNativeAttachment(params) {
1099
1184
  const { client, account, message, core } = params;
1100
1185
  const attachmentKind = kind(message.type);
@@ -1108,6 +1193,9 @@ async function prepareNativeAttachment(params) {
1108
1193
  await params.beforeMediaAccess?.();
1109
1194
  guard();
1110
1195
  };
1196
+ if (message.type === 49 && message.appMessageType === 57) return prepareNativeQuote(params);
1197
+ if (message.type === 49 && message.appMessageType === 19) return { body: message.body?.slice(0, 65536) || "[Forwarded chat history unavailable]", pending: false };
1198
+ if (message.type === 49 && message.appMessageType != null && message.appMessageType !== 6) return { body: `[Unsupported WeChat app message subtype ${message.appMessageType}]`, pending: false, retryable: false };
1111
1199
  if (!attachmentKind) return { body: message.type === 1 ? message.body ?? "" : `[Unsupported WeChat message type ${message.type}]`, pending: false };
1112
1200
  const accountId = account.native.accountId;
1113
1201
  const maxBytes = Math.min(account.mediaMaxMb * 1024 * 1024, 128 * 1024 * 1024);
@@ -1511,7 +1599,7 @@ function nativeDispatchManifest(params) {
1511
1599
  const history = params.history ?? [];
1512
1600
  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
1601
  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");
1602
+ 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
1603
  return Object.freeze({ ...asset });
1516
1604
  }));
1517
1605
  const messageSid = `wechat-native:${params.message.accountId}:${params.message.id}`;
@@ -1537,14 +1625,16 @@ function nativeDispatchManifest(params) {
1537
1625
  params.message.type,
1538
1626
  params.message.body,
1539
1627
  params.message.mentions,
1540
- params.message.serverId
1628
+ params.message.serverId,
1629
+ params.message.appMessageType,
1630
+ params.message.quote
1541
1631
  ])),
1542
1632
  role: "trigger"
1543
1633
  })]),
1544
1634
  ...history.length ? { historyInputs: Object.freeze(history.map((message) => Object.freeze({
1545
1635
  id: message.id,
1546
1636
  version: message.version ?? "",
1547
- contentSha256: digest(JSON.stringify([message.id, message.version, message.senderId, message.type, message.body, message.mentions, message.serverId])),
1637
+ contentSha256: digest(JSON.stringify([message.id, message.version, message.senderId, message.type, message.body, message.mentions, message.serverId, message.appMessageType, message.quote])),
1548
1638
  role: "history"
1549
1639
  }))) } : {},
1550
1640
  assets,
@@ -1842,8 +1932,9 @@ async function dispatchNativeMessage(params) {
1842
1932
  return "ignored";
1843
1933
  }
1844
1934
  let preparedMedia;
1935
+ const allowQuotedMessage = async (original) => original.direction === "outgoing" || !!original.senderId && (await senderAdmission(original.senderId)).ingress.admission === "dispatch";
1845
1936
  try {
1846
- preparedMedia = await (params.prepareAttachment ?? prepareNativeAttachment)({ client, account, message, core, cfg });
1937
+ preparedMedia = await (params.prepareAttachment ?? prepareNativeAttachment)({ client, account, message, core, cfg, allowQuotedMessage });
1847
1938
  } catch {
1848
1939
  state.update(message.id, { state: "needs_review" });
1849
1940
  return "needs_review";
@@ -1856,7 +1947,7 @@ async function dispatchNativeMessage(params) {
1856
1947
  if (item.message.senderId && (await senderAdmission(item.message.senderId)).ingress.admission === "dispatch") admitted.push(item.message);
1857
1948
  }
1858
1949
  const history = await prepareNativeHistory(admitted, async (historical) => transcribeNativeAttachment(
1859
- await (params.prepareAttachment ?? prepareNativeAttachment)({ client, account, message: historical, core, cfg }),
1950
+ await (params.prepareAttachment ?? prepareNativeAttachment)({ client, account, message: historical, core, cfg, allowQuotedMessage }),
1860
1951
  core,
1861
1952
  cfg
1862
1953
  ));
@@ -1912,7 +2003,7 @@ async function dispatchNativeMessage(params) {
1912
2003
  history: admitted
1913
2004
  });
1914
2005
  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;
2006
+ 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
2007
  if (callback) trackNativePreparedInput(dispatchManifest, callback);
1917
2008
  let failed = false;
1918
2009
  let delivery = 0;
@@ -2590,8 +2681,11 @@ function threadId(value) {
2590
2681
  return string(string(value, "thread ID").replace(/^(?:agent-)?wechat:(?:group:)?/i, ""), "thread ID");
2591
2682
  }
2592
2683
  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]`;
2684
+ const isQuote = message.type === 49 && message.appMessageType === 57;
2685
+ const isForward = message.type === 49 && message.appMessageType === 19;
2686
+ 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";
2687
+ const body = message.type === 1 || isForward ? message.body ?? "" : isQuote ? `${message.body ?? ""}
2688
+ [Quoted original not retrieved; use ${retrievalTool} with this message ID]` : `[WeChat ${kind2} message; attachment content has not been retrieved by this tool]`;
2595
2689
  return {
2596
2690
  id: message.id,
2597
2691
  threadId: message.threadId,
@@ -2599,10 +2693,12 @@ function messageResult(message, retrievalTool = "wechat_media") {
2599
2693
  senderId: message.senderId,
2600
2694
  direction: message.direction,
2601
2695
  type: message.type,
2696
+ appMessageType: message.appMessageType,
2697
+ quote: message.quote,
2602
2698
  createdAt: message.createdAt,
2603
2699
  body: body.slice(0, 4e3),
2604
2700
  bodyTruncated: body.length > 4e3,
2605
- attachment: message.type !== 1 && kind2 !== "unsupported" ? { kind: kind2, retrievalTool, messageId: message.id } : void 0
2701
+ attachment: message.type !== 1 && !isForward && kind2 !== "unsupported" ? { kind: kind2, retrievalTool, messageId: message.id } : void 0
2606
2702
  };
2607
2703
  }
2608
2704
  function createNativeHistoryTools(context, dependencies = {}) {
@@ -2720,6 +2816,7 @@ function createLookupTools(context, dependencies, archive) {
2720
2816
  waitMs: 2e3,
2721
2817
  signal,
2722
2818
  assertCurrent: guard,
2819
+ allowQuotedMessage: async (original) => original.direction === "outgoing" || await allowed(original.senderId ?? void 0),
2723
2820
  beforeMediaAccess: async () => {
2724
2821
  if (!await allowed(requesterSenderId) || message.direction !== "outgoing" && !await allowed(message.senderId ?? void 0)) throw Error("WeChat conversation access is no longer allowed");
2725
2822
  }
@@ -2741,8 +2838,9 @@ function createLookupTools(context, dependencies, archive) {
2741
2838
  const details = {
2742
2839
  messageId: message.id,
2743
2840
  threadId: message.threadId,
2744
- status: prepared.pending ? "pending" : prepared.attachment?.status ?? "unsupported",
2841
+ status: prepared.pending ? "pending" : prepared.attachment?.status ?? prepared.quote?.state ?? "unsupported",
2745
2842
  attachment: prepared.attachment,
2843
+ quote: prepared.quote,
2746
2844
  variant: prepared.variant,
2747
2845
  jobId: prepared.jobId,
2748
2846
  retryable: prepared.retryable ?? false,
@@ -2786,13 +2884,13 @@ function createLookupTools(context, dependencies, archive) {
2786
2884
  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
2885
  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
2886
  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"])
2887
+ 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
2888
  ];
2791
2889
  return [
2792
2890
  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
2891
  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
2892
  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"])
2893
+ 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
2894
  ];
2797
2895
  }
2798
2896
  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.7",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",