@wzfukui/ani 2026.3.28 → 2026.3.281

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wzfukui/ani",
3
- "version": "2026.3.28",
3
+ "version": "2026.3.281",
4
4
  "type": "module",
5
5
  "description": "ANI Agent-Native IM channel plugin",
6
6
  "license": "MIT",
@@ -11,10 +11,12 @@ import {
11
11
  sendAniMessage,
12
12
  sendAniProgress,
13
13
  fetchConversation,
14
+ fetchAniMessage,
14
15
  fetchConversationMemories,
15
16
  toggleAniReaction,
16
17
  type AniArtifact,
17
18
  type AniConversation,
19
+ type AniMessage,
18
20
  type AniMemory,
19
21
  } from "./send.js";
20
22
  import { createInboundDebouncer } from "./debounce.js";
@@ -27,6 +29,7 @@ export type AniWsMessage = {
27
29
  conversation_id?: number;
28
30
  sender_id?: number;
29
31
  sender_type?: string;
32
+ reply_to?: number;
30
33
  layers?: {
31
34
  summary?: string;
32
35
  detail?: string;
@@ -164,6 +167,33 @@ export function isTextFile(mimeType?: string, filename?: string): boolean {
164
167
  return false;
165
168
  }
166
169
 
170
+ export function messageTextOf(message: Pick<AniMessage, "layers"> | null | undefined): string {
171
+ const dataBody = typeof message?.layers?.data === "object" && message.layers?.data !== null
172
+ ? (message.layers.data as Record<string, unknown>).body
173
+ : undefined;
174
+ return (typeof dataBody === "string" ? dataBody : null) ??
175
+ message?.layers?.summary ??
176
+ message?.layers?.detail ??
177
+ "";
178
+ }
179
+
180
+ export function formatReplyContext(message: AniMessage | null | undefined): string {
181
+ if (!message?.id) return "";
182
+ const senderName = message.sender?.display_name ?? `entity-${message.sender_id ?? "unknown"}`;
183
+ const sentAt = message.created_at ? ` at ${message.created_at}` : "";
184
+ const text = messageTextOf(message).trim();
185
+ const attachments = message.attachments ?? [];
186
+ const attachmentText = attachments.length > 0
187
+ ? ` Attachments: ${attachments.map((attachment) => attachment.filename || attachment.type || "file").join(", ")}.`
188
+ : "";
189
+ const excerpt = text ? ` ${text}` : "";
190
+ return [
191
+ "Reply context:",
192
+ `This message replies to ANI message #${message.id} from ${senderName}${sentAt}.`,
193
+ `${excerpt}${attachmentText}`.trim(),
194
+ ].filter(Boolean).join("\n");
195
+ }
196
+
167
197
  function formatFileSize(bytes?: number): string {
168
198
  if (bytes == null) return 'unknown size';
169
199
  if (bytes < 1024) return `${bytes} B`;
@@ -674,7 +704,16 @@ export function createAniMessageHandler(params: AniHandlerParams) {
674
704
  sessionKey: route.sessionKey,
675
705
  });
676
706
 
677
- const rawBody = attachmentText ? `${text}\n\n${attachmentText}` : text;
707
+ let replyContext = "";
708
+ if (msg.reply_to) {
709
+ const parentMessage = await fetchAniMessage({ serverUrl, apiKey, messageId: msg.reply_to });
710
+ replyContext = parentMessage
711
+ ? formatReplyContext(parentMessage)
712
+ : `Reply context:\nThis message replies to ANI message #${msg.reply_to}, but the original message could not be fetched. Use ani_fetch_chat_history_messages if that missing context matters.`;
713
+ }
714
+
715
+ const rawBodyParts = [replyContext, text, attachmentText].filter((part) => part && part.trim());
716
+ const rawBody = rawBodyParts.join("\n\n");
678
717
  logVerbose(`ani: rawBody for envelope (${rawBody.length} chars): ${rawBody.slice(0, 500)}`);
679
718
 
680
719
  const body = core.channel.reply.formatAgentEnvelope({
@@ -989,7 +1028,7 @@ export function createAniMessageHandler(params: AniHandlerParams) {
989
1028
  const dataBody = typeof msg.layers?.data === "object" && msg.layers?.data !== null
990
1029
  ? (msg.layers.data as Record<string, unknown>).body
991
1030
  : undefined;
992
- const text = (typeof dataBody === "string" ? dataBody : null) ?? msg.layers?.summary ?? msg.layers?.detail ?? "";
1031
+ const text = messageTextOf(msg);
993
1032
 
994
1033
  // Process attachments:
995
1034
  // 1. Download and save to disk for OpenClaw media pipeline (MediaPath/MediaType)
@@ -119,6 +119,26 @@ export interface AniAttachment {
119
119
  content?: string;
120
120
  }
121
121
 
122
+ export interface AniMessage {
123
+ id?: number;
124
+ conversation_id?: number;
125
+ sender_id?: number;
126
+ sender_type?: string;
127
+ reply_to?: number;
128
+ created_at?: string;
129
+ sender?: {
130
+ id?: number;
131
+ display_name?: string;
132
+ entity_type?: string;
133
+ };
134
+ layers?: {
135
+ summary?: string;
136
+ detail?: string;
137
+ data?: unknown;
138
+ };
139
+ attachments?: AniAttachment[];
140
+ }
141
+
122
142
  /** Send a message to an ANI conversation via REST API. */
123
143
  export async function sendAniMessage(opts: {
124
144
  serverUrl: string;
@@ -211,6 +231,26 @@ export async function fetchConversation(opts: {
211
231
  }
212
232
  }
213
233
 
234
+ /** Fetch a single message with sender details for reply-context reconstruction. */
235
+ export async function fetchAniMessage(opts: {
236
+ serverUrl: string;
237
+ apiKey: string;
238
+ messageId: number;
239
+ }): Promise<AniMessage | null> {
240
+ const url = `${opts.serverUrl}/api/v1/messages/${opts.messageId}`;
241
+ try {
242
+ const res = await fetchWithRetry(url, {
243
+ headers: { Authorization: `Bearer ${opts.apiKey}` },
244
+ signal: AbortSignal.timeout(30_000),
245
+ });
246
+ if (!res.ok) return null;
247
+ const json = (await res.json()) as { data?: AniMessage };
248
+ return json.data ?? null;
249
+ } catch {
250
+ return null;
251
+ }
252
+ }
253
+
214
254
  /** Fetch conversation memories. */
215
255
  export async function fetchConversationMemories(opts: {
216
256
  serverUrl: string;
package/src/tools.ts CHANGED
@@ -261,14 +261,16 @@ export function createGetHistoryTool(): ChannelAgentTool {
261
261
 
262
262
  // Format messages for LLM readability
263
263
  const formatted = messages.map((m: Record<string, unknown>) => {
264
+ const id = typeof m.id === "number" ? `#${m.id}` : "#?";
264
265
  const sender = (m.sender as Record<string, unknown>)?.display_name ?? `entity-${m.sender_id}`;
265
266
  const text = ((m.layers as Record<string, unknown>)?.summary as string) ?? "";
266
267
  const time = m.created_at as string;
268
+ const replyTo = typeof m.reply_to === "number" ? ` ↪ reply_to #${m.reply_to}` : "";
267
269
  const atts = (m.attachments as Array<Record<string, unknown>>) ?? [];
268
270
  const attDesc = atts.length > 0
269
271
  ? ` [${atts.length} attachment(s): ${atts.map((a) => a.filename ?? a.type).join(", ")}]`
270
272
  : "";
271
- return `[${time}] ${sender}: ${text}${attDesc}`;
273
+ return `[${time}] ${id}${replyTo} ${sender}: ${text}${attDesc}`;
272
274
  }).reverse(); // oldest first for readability
273
275
 
274
276
  return {