openclaw-extension-typex 1.0.19 → 1.0.21

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.
@@ -11,13 +11,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.stripBotMention = exports.normalizeMessageToText = exports.checkBotMentioned = exports.buildAgentBody = void 0;
13
13
  exports.processTypeXMessage = processTypeXMessage;
14
- const plugin_sdk_1 = require("openclaw/plugin-sdk");
14
+ const reply_history_1 = require("openclaw/plugin-sdk/reply-history");
15
15
  const message_helpers_js_1 = require("./message-helpers.js");
16
16
  const runtime_js_1 = require("./runtime.js");
17
17
  const send_js_1 = require("./send.js");
18
+ const agent_tools_send_js_1 = require("../agent-tools-send.js");
18
19
  const types_js_1 = require("./types.js");
19
20
  const node_fs_1 = __importDefault(require("node:fs"));
20
21
  const node_path_1 = __importDefault(require("node:path"));
22
+ const node_os_1 = __importDefault(require("node:os"));
21
23
  // Re-export for convenience
22
24
  var message_helpers_js_2 = require("./message-helpers.js");
23
25
  Object.defineProperty(exports, "buildAgentBody", { enumerable: true, get: function () { return message_helpers_js_2.buildAgentBody; } });
@@ -25,6 +27,102 @@ Object.defineProperty(exports, "checkBotMentioned", { enumerable: true, get: fun
25
27
  Object.defineProperty(exports, "normalizeMessageToText", { enumerable: true, get: function () { return message_helpers_js_2.normalizeMessageToText; } });
26
28
  Object.defineProperty(exports, "stripBotMention", { enumerable: true, get: function () { return message_helpers_js_2.stripBotMention; } });
27
29
  const CHANNEL_ID = "openclaw-extension-typex";
30
+ function cleanupRecipient(value) {
31
+ return value
32
+ .replace(/<[^>]*>/g, " ")
33
+ .replace(/&nbsp;/gi, " ")
34
+ .replace(/\s+/g, " ")
35
+ .trim()
36
+ .replace(/[,。!?!?,;;::]+$/g, "")
37
+ .replace(/<\/?[a-z][^>]*$/i, "")
38
+ .replace(/(?:<\/?(?:p|li|ul|ol|span|div|br)[^>]*)+$/gi, "")
39
+ .replace(/^(给|到)/, "")
40
+ .replace(/(吗|吧|呀|啊)$/g, "")
41
+ .trim();
42
+ }
43
+ function normalizeIntentSource(value) {
44
+ return value
45
+ .replace(/<[^>]*>/g, " ")
46
+ .replace(/&nbsp;/gi, " ")
47
+ .replace(/\s+/g, " ")
48
+ .trim();
49
+ }
50
+ function shouldIncludeGeneratedTextForExplicitSend(text) {
51
+ return /(生成|写|整理|总结|编|拟|回复|文案|消息|内容|代码|祝福|介绍|说明)/.test(text);
52
+ }
53
+ function shouldIncludeAttachmentsForExplicitSend(text, attachmentPaths) {
54
+ if (attachmentPaths.length === 0) {
55
+ return false;
56
+ }
57
+ return /(图|图片|照片|附件|文件|发这张|把这张|这个文件|这个附件)/.test(text);
58
+ }
59
+ function resolveMentionTargetName(mentions, appId, fallbackToken) {
60
+ const visibleMentions = (mentions ?? []).filter((mention) => {
61
+ const userId = mention.id?.user_id ?? mention.id?.open_id ?? "";
62
+ return userId && userId !== appId;
63
+ });
64
+ if (visibleMentions.length === 1) {
65
+ return visibleMentions[0].name?.trim() || fallbackToken;
66
+ }
67
+ return fallbackToken;
68
+ }
69
+ function detectExplicitSendIntent(params) {
70
+ const source = normalizeIntentSource(params.text);
71
+ if (!source) {
72
+ return null;
73
+ }
74
+ const includeGeneratedText = params.attachmentPaths.length === 0 ||
75
+ shouldIncludeGeneratedTextForExplicitSend(source);
76
+ const includeAttachments = shouldIncludeAttachmentsForExplicitSend(source, params.attachmentPaths);
77
+ const sendPatterns = [
78
+ /(?:帮我|麻烦你|请你|你帮我|你帮忙)?(?:把)?(?:.*?)(?:发给|发送给|转给|转发给|代发给)\s*([^\n,。!?!?,;;]+)/,
79
+ /(?:帮我|麻烦你|请你|你帮我|你帮忙)?发(?:一条消息|个消息|消息|一句话|一段话|一下)?给\s*([^\n,。!?!?,;;]+)/,
80
+ /(?:帮我|麻烦你|请你|你帮我|你帮忙)?发.+?给\s*([^\n,。!?!?,;;]+)/,
81
+ /(?:帮我|麻烦你|请你|你帮我|你帮忙)?把(?:.+?)(?:发|发送|转发)给\s*([^\n,。!?!?,;;]+)/,
82
+ ];
83
+ const sendMatch = sendPatterns
84
+ .map((pattern) => source.match(pattern))
85
+ .find((match) => Boolean(match?.[1]));
86
+ if (sendMatch?.[1]) {
87
+ const rawRecipient = cleanupRecipient(sendMatch[1]);
88
+ if (!rawRecipient) {
89
+ return null;
90
+ }
91
+ if (params.isGroup) {
92
+ const pronounTarget = /^(他|她|它|TA|ta|对方)$/.test(rawRecipient)
93
+ ? resolveMentionTargetName(params.mentions, params.appId, rawRecipient)
94
+ : rawRecipient;
95
+ return {
96
+ kind: "group-send-in-group",
97
+ memberName: pronounTarget,
98
+ includeGeneratedText,
99
+ includeAttachments,
100
+ };
101
+ }
102
+ return {
103
+ kind: "dm-send-by-name",
104
+ recipient: rawRecipient,
105
+ includeGeneratedText,
106
+ includeAttachments,
107
+ };
108
+ }
109
+ if (params.isGroup) {
110
+ const hasTellIntent = source.includes("告诉") ||
111
+ /跟.+?(?:说|讲)/.test(source) ||
112
+ /对.+?(?:说|讲)/.test(source) ||
113
+ /让.+?知道/.test(source);
114
+ if (!hasTellIntent) {
115
+ return null;
116
+ }
117
+ return {
118
+ kind: "group-send-in-group",
119
+ memberName: source,
120
+ includeGeneratedText: true,
121
+ includeAttachments,
122
+ };
123
+ }
124
+ return null;
125
+ }
28
126
  async function processTypeXMessage(client, payload, appId, options = {}) {
29
127
  const cfg = options.cfg;
30
128
  const accountId = options.accountId ?? appId;
@@ -48,9 +146,9 @@ async function processTypeXMessage(client, payload, appId, options = {}) {
48
146
  const chatId = payload.chat_id;
49
147
  const senderId = payload.sender_id;
50
148
  const senderName = payload.sender_name ?? senderId;
51
- const isGroup = payload.chat_type === "group";
149
+ const isGroup = payload.chat_type === "group" || client.mode === "bot";
52
150
  const rawText = (0, message_helpers_js_1.normalizeMessageToText)(payload);
53
- logger?.info(`[typex:${accountId}] ${isGroup ? "group" : "DM"} message from ${senderId} in ${chatId}`);
151
+ logger?.info(`[typex:${accountId}] ${isGroup ? "group" : "DM"} message from ${senderId} in ${chatId} (chat_type=${payload.chat_type ?? "unknown"} mode=${client.mode})`);
54
152
  if (!rawText.trim()) {
55
153
  logger?.info(`[typex:${accountId}] skipping empty/system message`);
56
154
  return;
@@ -82,10 +180,10 @@ async function processTypeXMessage(client, payload, appId, options = {}) {
82
180
  if (requireMention && !mentionedBot) {
83
181
  logger?.info(`[typex:${accountId}] no @mention — buffering to history`);
84
182
  if (chatHistories) {
85
- (0, plugin_sdk_1.recordPendingHistoryEntryIfEnabled)({
183
+ (0, reply_history_1.recordPendingHistoryEntryIfEnabled)({
86
184
  historyMap: chatHistories,
87
185
  historyKey: chatId,
88
- limit: typexCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? plugin_sdk_1.DEFAULT_GROUP_HISTORY_LIMIT,
186
+ limit: typexCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? reply_history_1.DEFAULT_GROUP_HISTORY_LIMIT,
89
187
  entry: {
90
188
  sender: senderId,
91
189
  body: `${senderName}: ${rawText}`,
@@ -183,55 +281,39 @@ async function processTypeXMessage(client, payload, appId, options = {}) {
183
281
  });
184
282
  }
185
283
  if (objectKeys.size > 0) {
186
- if (client.mode === "bot") {
187
- for (const objectKey of objectKeys) {
188
- logger?.info(`[typex:${accountId}] fetching attachment objectKey=${objectKey}`);
189
- const fileData = await client.fetchFileBuffer(objectKey);
190
- if (fileData) {
191
- const isImage = fileData.mimeType.startsWith("image/");
192
- // Persist to local disk so the agent can open the image via the read tool
193
- // even if this surface can't transport binary attachments to the model.
194
- try {
195
- const extRaw = String(fileData.mimeType ?? "application/octet-stream").split("/")[1] ?? "bin";
196
- const ext = extRaw === "jpeg" ? "jpg" : extRaw;
197
- const outDir = node_path_1.default.join(process.env.HOME ?? ".", ".openclaw", "typex-attachments", String(payload.message_id));
198
- node_fs_1.default.mkdirSync(outDir, { recursive: true });
199
- const baseName = String(objectKey).replace(/[^a-zA-Z0-9._-]+/g, "_");
200
- const outPath = node_path_1.default.join(outDir, `${baseName}.${ext}`);
201
- node_fs_1.default.writeFileSync(outPath, fileData.buffer);
202
- attachmentPaths.push(outPath);
203
- logger?.info?.(`[typex:${accountId}] saved attachment to ${outPath}`);
204
- console.log('saved attachment to', outPath);
205
- }
206
- catch (e) {
207
- const msg = e?.message ?? String(e);
208
- logger?.warn?.(`[typex:${accountId}] failed to persist attachment: ${msg}`);
209
- console.log('failed to persist attachment', msg);
210
- try {
211
- console.log('persist debug', {
212
- home: process.env.HOME,
213
- cwd: process.cwd(),
214
- messageId: payload.message_id,
215
- objectKey,
216
- mimeType: fileData.mimeType,
217
- });
218
- }
219
- catch { }
220
- }
221
- attachments.push({
222
- contentType: fileData.mimeType,
223
- mimeType: fileData.mimeType, // Alias
224
- size: fileData.buffer.length,
225
- payload: fileData.buffer.toString("base64"),
226
- data: fileData.buffer.toString("base64"), // Alias
227
- type: isImage ? "image" : "file", // Alias
228
- });
284
+ for (const objectKey of objectKeys) {
285
+ logger?.info(`[typex:${accountId}] fetching attachment objectKey=${objectKey}`);
286
+ const fileData = await client.fetchFileBuffer(objectKey);
287
+ if (fileData) {
288
+ const isImage = fileData.mimeType.startsWith("image/");
289
+ // Persist to local disk so the agent can open the image via the read tool
290
+ // even if this surface can't transport binary attachments to the model.
291
+ try {
292
+ const extRaw = String(fileData.mimeType ?? "application/octet-stream").split("/")[1] ?? "bin";
293
+ const ext = extRaw === "jpeg" ? "jpg" : extRaw;
294
+ const homeDir = node_os_1.default.homedir?.() ?? ".";
295
+ // Store TypeX attachments under the OpenClaw workspace so newer
296
+ // local-media allowlists can safely expose them to the model.
297
+ const outDir = node_path_1.default.join(homeDir, ".openclaw", "workspace", "typex-attachments", String(payload.message_id));
298
+ node_fs_1.default.mkdirSync(outDir, { recursive: true });
299
+ const baseName = String(objectKey).replace(/[^a-zA-Z0-9._-]+/g, "_");
300
+ const outPath = node_path_1.default.join(outDir, `${baseName}.${ext}`);
301
+ node_fs_1.default.writeFileSync(outPath, fileData.buffer);
302
+ attachmentPaths.push(outPath);
229
303
  }
304
+ catch (e) {
305
+ const msg = e?.message ?? String(e);
306
+ logger?.warn?.(`[typex:${accountId}] failed to persist attachment: ${msg}`);
307
+ }
308
+ attachments.push({
309
+ contentType: fileData.mimeType,
310
+ mimeType: fileData.mimeType, // Alias
311
+ size: fileData.buffer.length,
312
+ payload: fileData.buffer.toString("base64"),
313
+ data: fileData.buffer.toString("base64"), // Alias
314
+ type: isImage ? "image" : "file", // Alias
315
+ });
230
316
  }
231
- console.log('processed attachments:', attachments.map(a => ({ type: a.contentType, size: a.size })));
232
- }
233
- else {
234
- logger?.info(`[typex:${accountId}] skipping attachment fetch because mode is not bot`);
235
317
  }
236
318
  }
237
319
  }
@@ -242,6 +324,20 @@ async function processTypeXMessage(client, payload, appId, options = {}) {
242
324
  const cleanTextWithAttachments = attachmentPaths.length > 0
243
325
  ? `${cleanText}\n\n[TypeX attachments saved locally]\n${attachmentPaths.map(p => `- ${p}`).join("\n")}`
244
326
  : cleanText;
327
+ const contentForAgent = cleanTextWithAttachments;
328
+ const explicitSendIntent = detectExplicitSendIntent({
329
+ text: cleanText,
330
+ isGroup,
331
+ attachmentPaths,
332
+ mentions: payload.mentions,
333
+ appId,
334
+ });
335
+ if (explicitSendIntent) {
336
+ const targetLabel = explicitSendIntent.kind === "dm-send-by-name"
337
+ ? explicitSendIntent.recipient
338
+ : explicitSendIntent.memberName;
339
+ logger?.info(`[typex:${accountId}] detected explicit send intent kind=${explicitSendIntent.kind} target=${targetLabel}`);
340
+ }
245
341
  // ── Session routing ───────────────────────────────────────────────────────
246
342
  const peerId = isGroup ? chatId : senderId;
247
343
  let route = {};
@@ -257,12 +353,12 @@ async function processTypeXMessage(client, payload, appId, options = {}) {
257
353
  logger?.error(`[typex:${accountId}] resolveAgentRoute failed: ${e?.message ?? String(e)}`);
258
354
  }
259
355
  // ── Build message body ────────────────────────────────────────────────────
260
- const historyLimit = typexCfg?.historyLimit ?? cfg?.messages?.groupChat?.historyLimit ?? plugin_sdk_1.DEFAULT_GROUP_HISTORY_LIMIT;
356
+ const historyLimit = typexCfg?.historyLimit ?? cfg?.messages?.groupChat?.historyLimit ?? reply_history_1.DEFAULT_GROUP_HISTORY_LIMIT;
261
357
  const envelopeOptions = channel.reply?.resolveEnvelopeFormatOptions?.(cfg) ?? {};
262
358
  const agentBody = (0, message_helpers_js_1.buildAgentBody)({
263
359
  messageId: payload.message_id,
264
360
  senderLabel: senderName,
265
- content: cleanTextWithAttachments,
361
+ content: contentForAgent,
266
362
  quotedContent,
267
363
  });
268
364
  const envelopeFrom = isGroup ? `${chatId}:${senderId}` : senderId;
@@ -275,7 +371,7 @@ async function processTypeXMessage(client, payload, appId, options = {}) {
275
371
  }) ?? agentBody;
276
372
  let combinedBody = formattedBody;
277
373
  if (isGroup && chatHistories) {
278
- combinedBody = (0, plugin_sdk_1.buildPendingHistoryContextFromMap)({
374
+ combinedBody = (0, reply_history_1.buildPendingHistoryContextFromMap)({
279
375
  historyMap: chatHistories,
280
376
  historyKey: chatId,
281
377
  limit: historyLimit,
@@ -311,6 +407,11 @@ async function processTypeXMessage(client, payload, appId, options = {}) {
311
407
  SenderId: senderId,
312
408
  Provider: CHANNEL_ID,
313
409
  Surface: CHANNEL_ID,
410
+ DeliveryContext: {
411
+ channel: CHANNEL_ID,
412
+ to: typexTo,
413
+ accountId: route.accountId ?? accountId,
414
+ },
314
415
  MessageSid: payload.message_id,
315
416
  ReplyToBody: quotedContent,
316
417
  Timestamp: typeof payload.create_time === "number" ? payload.create_time : Date.now(),
@@ -348,9 +449,52 @@ async function processTypeXMessage(client, payload, appId, options = {}) {
348
449
  extractedFromText.push(v);
349
450
  return "";
350
451
  }).trim();
351
- if (extractedFromText.length > 0) {
352
- logger?.info?.(`[typex:${accountId}] extracted outbound media directives: ${extractedFromText.join(", ")}`);
353
- console.log('extracted outbound media directives', extractedFromText);
452
+ }
453
+ const generatedText = text?.trim() ?? "";
454
+ if (explicitSendIntent) {
455
+ const targetLabel = explicitSendIntent.kind === "dm-send-by-name"
456
+ ? explicitSendIntent.recipient
457
+ : explicitSendIntent.memberName;
458
+ logger?.info(`[typex:${accountId}] executing explicit send fallback kind=${explicitSendIntent.kind} target=${targetLabel}`);
459
+ const hasSendableContent = (explicitSendIntent.includeGeneratedText && generatedText.length > 0) ||
460
+ (explicitSendIntent.includeAttachments && attachmentPaths.length > 0);
461
+ if (!hasSendableContent) {
462
+ return;
463
+ }
464
+ try {
465
+ if (explicitSendIntent.kind === "dm-send-by-name") {
466
+ const messageToSend = explicitSendIntent.includeGeneratedText ? generatedText : "";
467
+ const mediaPathsToSend = explicitSendIntent.includeAttachments ? attachmentPaths : [];
468
+ await (0, agent_tools_send_js_1.executeTypeXSendByName)({
469
+ cfg,
470
+ accountId,
471
+ recipient: explicitSendIntent.recipient,
472
+ message: messageToSend,
473
+ mediaPaths: mediaPathsToSend,
474
+ });
475
+ await (0, send_js_1.sendMessageTypeX)(client, chatId, `已代发给 ${explicitSendIntent.recipient}。`, { msgType: types_js_1.TypeXMessageEnum.richText, replyMsgId: payload.message_id });
476
+ logger?.info(`[typex:${accountId}] explicit send completed kind=${explicitSendIntent.kind} target=${explicitSendIntent.recipient}`);
477
+ return;
478
+ }
479
+ const messageToSend = explicitSendIntent.includeGeneratedText ? generatedText : "";
480
+ const mediaPathsToSend = explicitSendIntent.includeAttachments ? attachmentPaths : [];
481
+ await (0, agent_tools_send_js_1.executeTypeXSendInGroup)({
482
+ cfg,
483
+ accountId,
484
+ chatId,
485
+ memberName: explicitSendIntent.memberName,
486
+ message: messageToSend,
487
+ mediaPaths: mediaPathsToSend,
488
+ });
489
+ await (0, send_js_1.sendMessageTypeX)(client, chatId, `已在群里发送给 ${explicitSendIntent.memberName}。`, { msgType: types_js_1.TypeXMessageEnum.richText, replyMsgId: payload.message_id });
490
+ logger?.info(`[typex:${accountId}] explicit send completed kind=${explicitSendIntent.kind} target=${explicitSendIntent.memberName}`);
491
+ return;
492
+ }
493
+ catch (error) {
494
+ const message = error?.message ?? String(error);
495
+ logger?.warn?.(`[typex:${accountId}] explicit send fallback failed: ${message}`);
496
+ await (0, send_js_1.sendMessageTypeX)(client, chatId, `发送失败:${message}`, { msgType: types_js_1.TypeXMessageEnum.richText, replyMsgId: payload.message_id });
497
+ return;
354
498
  }
355
499
  }
356
500
  if (text) {
@@ -362,8 +506,6 @@ async function processTypeXMessage(client, payload, appId, options = {}) {
362
506
  ? [rp.mediaUrl]
363
507
  : [];
364
508
  for (const url of [...urls, ...extractedFromText]) {
365
- logger?.info?.(`[typex:${accountId}] sending outbound mediaUrl=${url}`);
366
- console.log('sending outbound mediaUrl', url);
367
509
  await (0, send_js_1.sendMessageTypeX)(client, chatId, {}, { mediaUrl: url, msgType: types_js_1.TypeXMessageEnum.richText, replyMsgId: payload.message_id });
368
510
  }
369
511
  },
@@ -375,6 +517,6 @@ async function processTypeXMessage(client, payload, appId, options = {}) {
375
517
  });
376
518
  // ── Clear group history after dispatch ────────────────────────────────────
377
519
  if (isGroup && chatHistories) {
378
- (0, plugin_sdk_1.clearHistoryEntriesIfEnabled)({ historyMap: chatHistories, historyKey: chatId, limit: historyLimit });
520
+ (0, reply_history_1.clearHistoryEntriesIfEnabled)({ historyMap: chatHistories, historyKey: chatId, limit: historyLimit });
379
521
  }
380
522
  }
@@ -1,4 +1,5 @@
1
- import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/channel-core";
2
+ import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
2
3
  export type MonitorTypeXOpts = {
3
4
  account: unknown;
4
5
  runtime: RuntimeEnv;
@@ -41,12 +41,13 @@ const client_js_1 = require("./client.js");
41
41
  const message_js_1 = require("./message.js");
42
42
  const runtime_js_1 = require("./runtime.js");
43
43
  const POS_STORE_VERSION = 1;
44
- const OPENCLAW_CONFIG_PATH = process.env.OPENCLAW_CONFIG_PATH || path.join(os.homedir(), ".openclaw", "openclaw.json");
44
+ const OPENCLAW_HOME_DIR = path.join(os.homedir(), ".openclaw");
45
+ const OPENCLAW_CONFIG_PATH = path.join(OPENCLAW_HOME_DIR, "openclaw.json");
45
46
  function normalizeAccountIdForFile(accountId) {
46
47
  return (accountId || "default").replace(/[^a-z0-9._-]+/gi, "_");
47
48
  }
48
49
  function resolveTypeXPosStatePath(accountId) {
49
- const stateDir = (0, runtime_js_1.getTypeXRuntime)().state.resolveStateDir(process.env, os.homedir);
50
+ const stateDir = (0, runtime_js_1.getTypeXRuntime)().state.resolveStateDir({}, os.homedir);
50
51
  const normalized = normalizeAccountIdForFile(accountId);
51
52
  return path.join(stateDir, "typex", `update-pos-${normalized}.json`);
52
53
  }
@@ -206,18 +207,46 @@ async function monitorTypeXProvider(opts) {
206
207
  // Group history buffer: lives for the entire monitor lifetime.
207
208
  const chatHistories = new Map();
208
209
  let fatalError = null;
210
+ let consecutiveFetchFailures = 0;
211
+ const resolveBackoffMs = (failures) => {
212
+ if (failures <= 0)
213
+ return 3000;
214
+ if (failures === 1)
215
+ return 10000;
216
+ if (failures === 2)
217
+ return 30000;
218
+ if (failures === 3)
219
+ return 60000;
220
+ return 300000;
221
+ };
222
+ const isFatalFetchError = (err) => {
223
+ const message = err instanceof Error ? err.message : String(err);
224
+ const normalized = message.toLowerCase();
225
+ return (normalized.includes("non-json response") ||
226
+ normalized.includes("session auth error") ||
227
+ normalized.includes("http 401") ||
228
+ normalized.includes("http 403") ||
229
+ normalized.includes("http 404"));
230
+ };
209
231
  // --- Polling Loop ---
210
232
  while (!abortSignal.aborted) {
211
233
  let messages;
212
234
  try {
213
235
  messages = await client.fetchMessages(currentPos);
236
+ consecutiveFetchFailures = 0;
214
237
  }
215
238
  catch (err) {
216
- // fetchMessages threw — this is treated as a fatal error (e.g. auth failure,
217
- // bad token, server unreachable). Stop the monitor so we don't spam the API.
218
- logger?.error(`[${accountObj.accountId}] Fatal error fetching TypeX messages; stopping monitor: ${err instanceof Error ? err.stack : String(err)}`);
219
- fatalError = err instanceof Error ? err : new Error(String(err));
220
- break;
239
+ consecutiveFetchFailures += 1;
240
+ const backoffMs = resolveBackoffMs(consecutiveFetchFailures);
241
+ const error = err instanceof Error ? err : new Error(String(err));
242
+ logger?.error(`[${accountObj.accountId}] Failed to fetch TypeX messages (attempt ${consecutiveFetchFailures}); next retry in ${Math.round(backoffMs / 1000)}s: ${error.message}`);
243
+ if (isFatalFetchError(error) || consecutiveFetchFailures >= 5) {
244
+ logger?.error(`[${accountObj.accountId}] Fatal error fetching TypeX messages; stopping monitor to avoid request spam.`);
245
+ fatalError = error;
246
+ break;
247
+ }
248
+ await new Promise((resolve) => setTimeout(resolve, backoffMs));
249
+ continue;
221
250
  }
222
251
  if (messages && messages.length > 0) {
223
252
  for (const msg of messages) {
@@ -1,2 +1,2 @@
1
- import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
1
+ import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/core";
2
2
  export declare const typexOutbound: ChannelOutboundAdapter;
@@ -3,29 +3,74 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.typexOutbound = void 0;
4
4
  const client_js_1 = require("./client.js");
5
5
  const send_js_1 = require("./send.js");
6
+ const types_js_1 = require("./types.js");
7
+ function resolveTypeXTarget(rawTarget) {
8
+ const trimmed = (rawTarget ?? "").trim();
9
+ if (/^user:\d+$/i.test(trimmed)) {
10
+ return { chatId: trimmed.replace(/^user:/i, ""), receiverId: trimmed.replace(/^user:/i, "") };
11
+ }
12
+ if (/^(?:chat|group):\d+$/i.test(trimmed)) {
13
+ return { chatId: trimmed.replace(/^(?:chat|group):/i, "") };
14
+ }
15
+ return { chatId: trimmed };
16
+ }
6
17
  exports.typexOutbound = {
7
18
  deliveryMode: "direct",
8
19
  // chunker: ... (optional, default might be used or I can skip for MVP)
9
20
  chunkerMode: "markdown",
10
21
  textChunkLimit: 2000,
22
+ resolveTarget: ({ to }) => {
23
+ const trimmed = (to ?? "").trim();
24
+ if (!trimmed) {
25
+ return { ok: false, error: new Error("TypeX target is required.") };
26
+ }
27
+ if (/^(?:user|chat|group):\d+$/i.test(trimmed) || /^\d+$/.test(trimmed)) {
28
+ return { ok: true, to: trimmed };
29
+ }
30
+ return {
31
+ ok: false,
32
+ error: new Error("TypeX target must be a numeric id or a user:/chat:/group: target."),
33
+ };
34
+ },
11
35
  sendText: async ({ to, text, accountId, cfg }) => {
12
36
  const typexCfg = (cfg?.channels?.["openclaw-extension-typex"] ?? {});
13
37
  const client = (0, client_js_1.getTypeXClient)(accountId ?? undefined, { typexCfg });
14
- const result = await (0, send_js_1.sendMessageTypeX)(client, to, text);
38
+ const target = resolveTypeXTarget(to);
39
+ let hasMention = false;
40
+ const finalText = (text || "").replace(/<at\s+user_id="([^"]+)"(?:>([^<]*)<\/at>|\s*\/>)/g, (match, userId, name) => {
41
+ hasMention = true;
42
+ const label = name || userId;
43
+ return `@${label} `;
44
+ });
45
+ const result = await (0, send_js_1.sendMessageTypeX)(client, target.chatId, finalText, {
46
+ receiverId: target.receiverId,
47
+ msgType: hasMention ? types_js_1.TypeXMessageEnum.text : undefined,
48
+ });
15
49
  return {
16
50
  channel: "openclaw-extension-typex",
17
51
  messageId: result?.message_id || "unknown",
18
- chatId: to,
52
+ chatId: target.chatId,
19
53
  };
20
54
  },
21
55
  sendMedia: async ({ to, text, mediaUrl, accountId, cfg }) => {
22
56
  const typexCfg = (cfg?.channels?.["openclaw-extension-typex"] ?? {});
23
57
  const client = (0, client_js_1.getTypeXClient)(accountId ?? undefined, { typexCfg });
24
- const result = await (0, send_js_1.sendMessageTypeX)(client, to, text || "", { mediaUrl });
58
+ const target = resolveTypeXTarget(to);
59
+ let hasMention = false;
60
+ const finalText = (text || "").replace(/<at\s+user_id="([^"]+)"(?:>([^<]*)<\/at>|\s*\/>)/g, (match, userId, name) => {
61
+ hasMention = true;
62
+ const label = name || userId;
63
+ return `@${label} `;
64
+ });
65
+ const result = await (0, send_js_1.sendMessageTypeX)(client, target.chatId, finalText, {
66
+ mediaUrl,
67
+ receiverId: target.receiverId,
68
+ msgType: hasMention ? types_js_1.TypeXMessageEnum.text : undefined,
69
+ });
25
70
  return {
26
71
  channel: "openclaw-extension-typex",
27
72
  messageId: result?.message_id || "unknown",
28
- chatId: to,
73
+ chatId: target.chatId,
29
74
  };
30
75
  },
31
76
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Global runtime reference for the TypeX plugin.
3
3
  */
4
- import type { PluginRuntime } from "openclaw/plugin-sdk";
4
+ import type { PluginRuntime } from "openclaw/plugin-sdk/channel-core";
5
5
  export declare function setTypeXRuntime(next: PluginRuntime): void;
6
6
  export declare function getTypeXRuntime(): PluginRuntime;
@@ -5,6 +5,13 @@ export type TypeXSendOpts = {
5
5
  mediaUrl?: string;
6
6
  maxBytes?: number;
7
7
  replyMsgId?: string;
8
+ receiverId?: string;
9
+ isDelegate?: boolean;
10
+ atUserIds?: string[];
11
+ atMentions?: Array<{
12
+ id: string;
13
+ name: string;
14
+ }>;
8
15
  };
9
16
  /**
10
17
  * Send a message via the TypeX client to a specific chat.
@@ -16,6 +23,7 @@ export type TypeXSendOpts = {
16
23
  export declare function sendMessageTypeX(client: TypeXClient, chatId: string, content: string | {
17
24
  text?: string;
18
25
  object_url?: string;
26
+ thumb_url?: string;
19
27
  file_name?: string;
20
28
  file_size?: number;
21
29
  file_type?: string;
@@ -77,6 +77,16 @@ async function sendMessageTypeX(client, chatId, content, opts = {}) {
77
77
  finalContent = opts.mediaUrl;
78
78
  }
79
79
  }
80
- const res = await client.sendMessage(chatId, finalContent, msgType, { replyMsgId: opts.replyMsgId });
80
+ const res = client.mode === "bot"
81
+ ? await client.sendBotGroupMessage(chatId, finalContent, msgType, {
82
+ replyMsgId: opts.replyMsgId,
83
+ atUserIds: opts.atUserIds,
84
+ atMentions: opts.atMentions,
85
+ })
86
+ : opts.isDelegate && opts.receiverId
87
+ ? await client.sendDelegatedContactMessage(opts.receiverId, finalContent, msgType)
88
+ : opts.isDelegate
89
+ ? await client.sendDelegatedChatMessage(chatId, finalContent, msgType)
90
+ : await client.sendUserChatMessage(chatId, finalContent, msgType);
81
91
  return res;
82
92
  }
@@ -1,4 +1,4 @@
1
- import { WizardPrompter } from "openclaw/plugin-sdk";
1
+ import type { WizardPrompter } from "openclaw/plugin-sdk/setup";
2
2
  export declare enum TypeXMessageEnum {
3
3
  text = 0,
4
4
  image = 1,
@@ -73,3 +73,19 @@ export interface TypeXMessageEntry {
73
73
  /** Monotonic position cursor used by the polling loop. */
74
74
  position: number;
75
75
  }
76
+ export interface TypeXFeedSearchEntry {
77
+ id: string;
78
+ chat_id: string;
79
+ name?: string;
80
+ }
81
+ export interface TypeXContactSearchEntry {
82
+ friend_id: string;
83
+ name?: string;
84
+ }
85
+ export interface TypeXGroupMemberEntry {
86
+ user_id: string;
87
+ name?: string;
88
+ avatar?: string;
89
+ member_role?: number;
90
+ joined_at?: number;
91
+ }
@@ -7,9 +7,10 @@ export declare const TypeXConfigSchema: z.ZodObject<{
7
7
  botName: z.ZodOptional<z.ZodString>;
8
8
  markdown: z.ZodOptional<z.ZodObject<{
9
9
  tables: z.ZodOptional<z.ZodEnum<{
10
+ code: "code";
10
11
  off: "off";
11
12
  bullets: "bullets";
12
- code: "code";
13
+ block: "block";
13
14
  }>>;
14
15
  }, z.core.$strict>>;
15
16
  allowFrom: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
@@ -36,9 +37,10 @@ export declare const TypeXConfigSchema: z.ZodObject<{
36
37
  botName: z.ZodOptional<z.ZodString>;
37
38
  markdown: z.ZodOptional<z.ZodObject<{
38
39
  tables: z.ZodOptional<z.ZodEnum<{
40
+ code: "code";
39
41
  off: "off";
40
42
  bullets: "bullets";
41
- code: "code";
43
+ block: "block";
42
44
  }>>;
43
45
  }, z.core.$strict>>;
44
46
  allowFrom: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
@@ -1,15 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TypeXConfigSchema = void 0;
4
- const plugin_sdk_1 = require("openclaw/plugin-sdk");
4
+ const channel_config_schema_1 = require("openclaw/plugin-sdk/channel-config-schema");
5
5
  const zod_1 = require("zod");
6
6
  const allowFromEntry = zod_1.z.union([zod_1.z.string(), zod_1.z.number()]);
7
- const toolsBySenderSchema = zod_1.z.record(zod_1.z.string(), plugin_sdk_1.ToolPolicySchema).optional();
7
+ const toolsBySenderSchema = zod_1.z.record(zod_1.z.string(), channel_config_schema_1.ToolPolicySchema).optional();
8
8
  // Simplify Group Schema for TypeX MVP
9
9
  const TypeXGroupSchema = zod_1.z
10
10
  .object({
11
11
  enabled: zod_1.z.boolean().optional(),
12
- tools: plugin_sdk_1.ToolPolicySchema,
12
+ tools: channel_config_schema_1.ToolPolicySchema,
13
13
  toolsBySender: toolsBySenderSchema,
14
14
  systemPrompt: zod_1.z.string().optional(),
15
15
  })
@@ -21,7 +21,7 @@ const TypeXAccountSchema = zod_1.z
21
21
  appId: zod_1.z.string().optional(),
22
22
  appSecret: zod_1.z.string().optional(),
23
23
  botName: zod_1.z.string().optional(),
24
- markdown: plugin_sdk_1.MarkdownConfigSchema,
24
+ markdown: channel_config_schema_1.MarkdownConfigSchema,
25
25
  // Policy settings
26
26
  allowFrom: zod_1.z.array(allowFromEntry).optional(),
27
27
  responsePrefix: zod_1.z.string().optional(),