openclaw-extension-typex 1.0.16 → 1.0.17

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.
@@ -1,14 +1,52 @@
1
- import { TypeXMessageEnum, type TypeXClientOptions } from "./types.js";
1
+ import { TypeXMessageEnum, type TypeXClientOptions, type TypeXMessageEntry } from "./types.js";
2
2
  export declare class TypeXClient {
3
3
  private options;
4
4
  private accessToken?;
5
5
  private userId?;
6
6
  constructor(options: TypeXClientOptions);
7
+ get mode(): "user" | "bot";
7
8
  getAccessToken(): Promise<string>;
8
9
  getCurUserId(): Promise<string>;
9
10
  fetchQrcodeUrl(): Promise<any>;
10
11
  checkLoginStatus(qrcodeId: string): Promise<boolean>;
11
- sendMessage(content: string | object, msgType?: TypeXMessageEnum): Promise<any>;
12
- fetchMessages(pos: number): Promise<any>;
12
+ /**
13
+ * Send a message to a specific chat (group or DM).
14
+ * @param to chat_id to send to
15
+ * @param content message text or object
16
+ */
17
+ sendMessage(to: string, content: string | object, msgType?: TypeXMessageEnum, options?: {
18
+ replyMsgId?: string;
19
+ }): Promise<any>;
20
+ /**
21
+ * Upload resource for the robot to send.
22
+ * @param fileName Name of the file
23
+ * @param fileType "image" | "audio" | "video" | "application"
24
+ * @param fileContent Buffer or Blob containing the file data
25
+ * @param chatId Optional chat_id
26
+ */
27
+ uploadResource(fileName: string, fileType: "image" | "audio" | "video" | "application", fileContent: Buffer | Blob, chatId?: string): Promise<any>;
28
+ /**
29
+ * Fetch messages. Dispatches to user or bot endpoint based on mode.
30
+ */
31
+ fetchMessages(pos: number): Promise<TypeXMessageEntry[]>;
32
+ /** Pull messages for a regular user account (sessionid cookie auth). */
33
+ private fetchUserMessages;
34
+ /**
35
+ * Pull messages for a bot account (Bearer token auth).
36
+ * TODO: replace /open/bot/message with the actual endpoint path once confirmed.
37
+ */
38
+ private fetchBotMessages;
39
+ /**
40
+ * Fetch a single message by ID (used to resolve quoted/parent messages).
41
+ */
42
+ getMessage(messageId: string): Promise<TypeXMessageEntry | null>;
43
+ /**
44
+ * Fetch file binary stream from TypeX using object_key.
45
+ * Requires Bot Token authentication.
46
+ */
47
+ fetchFileBuffer(objectKey: string, size?: string): Promise<{
48
+ buffer: Buffer;
49
+ mimeType: string;
50
+ } | null>;
13
51
  }
14
52
  export declare function getTypeXClient(accountId?: string, manualOptions?: TypeXClientOptions): TypeXClient;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TypeXClient = void 0;
4
4
  exports.getTypeXClient = getTypeXClient;
5
+ const types_js_1 = require("./types.js");
5
6
  const TYPEX_DOMAIN = "https://api-coco.typex.im";
6
7
  // const TYPEX_DOMAIN = "https://api-tx.bossjob.net.cn";
7
8
  let prompter;
@@ -15,191 +16,281 @@ class TypeXClient {
15
16
  this.accessToken = options.token;
16
17
  }
17
18
  }
19
+ get mode() {
20
+ return this.options.mode ?? "user";
21
+ }
18
22
  async getAccessToken() {
19
- if (this.accessToken) {
20
- return this.accessToken;
21
- }
22
- return "";
23
+ return this.accessToken ?? "";
23
24
  }
24
25
  async getCurUserId() {
25
- if (this.userId) {
26
- return this.userId;
27
- }
28
- return "";
26
+ return this.userId ?? "";
29
27
  }
30
28
  async fetchQrcodeUrl() {
31
- try {
32
- const qrResponse = await fetch(`${TYPEX_DOMAIN}/user/qrcode?login_type=open`, {
33
- method: "POST",
34
- headers: { "Content-Type": "application/json" },
35
- body: JSON.stringify({}),
36
- });
37
- if (!qrResponse.ok) {
38
- throw new Error(`Failed to get QR code: ${qrResponse.statusText}`);
39
- }
40
- const qrResult = await qrResponse.json();
41
- if (qrResult.code !== 0 || !qrResult.data) {
42
- throw new Error(`Failed to get QR code: ${qrResult.msg}`);
43
- }
44
- return qrResult.data;
29
+ const qrResponse = await fetch(`${TYPEX_DOMAIN}/user/qrcode?login_type=open`, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({}),
33
+ });
34
+ if (!qrResponse.ok) {
35
+ throw new Error(`Failed to get QR code: ${qrResponse.statusText}`);
45
36
  }
46
- catch (error) {
47
- throw error;
37
+ const qrResult = await qrResponse.json();
38
+ if (qrResult.code !== 0 || !qrResult.data) {
39
+ throw new Error(`Failed to get QR code: ${qrResult.msg}`);
48
40
  }
41
+ return qrResult.data;
49
42
  }
50
43
  async checkLoginStatus(qrcodeId) {
51
- try {
52
- const checkRes = await fetch(`${TYPEX_DOMAIN}/open/qrcode/check_auth`, {
53
- method: "POST",
54
- headers: {
55
- "Content-Type": "application/json",
56
- },
57
- body: JSON.stringify({
58
- qr_code_id: qrcodeId,
59
- }),
60
- });
61
- const setCookieHeader = checkRes.headers.get("set-cookie");
62
- if (setCookieHeader) {
63
- const match = setCookieHeader.match(/(sessionid=[^;]+)/);
64
- if (match && match[1]) {
65
- this.accessToken = match[1];
66
- }
67
- }
68
- const checkData = await checkRes.json();
69
- if (checkData.code === 0) {
70
- const { user_id } = checkData.data;
71
- this.userId = user_id;
72
- return true;
73
- }
74
- else if (checkData.code === 10001) {
75
- return false;
76
- }
77
- else {
78
- return false;
79
- }
44
+ const checkRes = await fetch(`${TYPEX_DOMAIN}/open/qrcode/check_auth`, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({ qr_code_id: qrcodeId }),
48
+ });
49
+ const setCookieHeader = checkRes.headers.get("set-cookie");
50
+ if (setCookieHeader) {
51
+ const match = setCookieHeader.match(/(sessionid=[^;]+)/);
52
+ if (match?.[1])
53
+ this.accessToken = match[1];
80
54
  }
81
- catch (error) {
82
- throw error;
55
+ const checkData = await checkRes.json();
56
+ if (checkData.code === 0) {
57
+ this.userId = checkData.data.user_id;
58
+ return true;
83
59
  }
60
+ return false;
84
61
  }
85
- async sendMessage(content, msgType = 0) {
86
- console.info("msgType: ", msgType);
62
+ /**
63
+ * Send a message to a specific chat (group or DM).
64
+ * @param to chat_id to send to
65
+ * @param content message text or object
66
+ */
67
+ async sendMessage(to, content, msgType = 0, options = {}) {
87
68
  const token = this.accessToken;
88
- if (!token) {
69
+ if (!token)
89
70
  throw new Error("TypeXClient: Not authenticated.");
90
- }
91
71
  let finalContent = content;
92
72
  if (typeof content === "object") {
93
73
  try {
94
74
  finalContent = JSON.stringify(content);
95
75
  }
96
- catch (e) {
97
- if (e instanceof Error) {
98
- if (prompter)
99
- prompter.note("Failed to stringify message content");
100
- else
101
- console.log("Failed to stringify message content");
102
- }
76
+ catch {
103
77
  finalContent = String(content);
104
78
  }
105
79
  }
106
80
  if (prompter)
107
- prompter.note(`TypeXClient sending message: content=${typeof finalContent === "string" ? finalContent : JSON.stringify(finalContent)}`);
81
+ prompter.note(`TypeXClient sending to ${to}: ${String(finalContent).slice(0, 80)}`);
108
82
  else
109
- console.log(`TypeXClient sending message: content=${typeof finalContent === "string" ? finalContent : JSON.stringify(finalContent)}`);
110
- try {
111
- const url = `${TYPEX_DOMAIN}/open/claw/send_message`;
112
- const response = await fetch(url, {
113
- method: "POST",
114
- headers: {
115
- "Content-Type": "application/json",
116
- Cookie: token,
117
- },
118
- body: JSON.stringify({
119
- content: {
120
- text: finalContent,
121
- },
122
- msg_type: msgType,
123
- }),
124
- });
125
- const resJson = await response.json();
126
- if (resJson.code !== 0) {
127
- throw new Error(`Send message failed: [${resJson.code}] ${resJson.message}`);
83
+ console.log(`TypeXClient sending to ${to}: ${String(finalContent).slice(0, 80)}`);
84
+ const isBot = this.mode === "bot";
85
+ const endpoint = isBot ? "/open/robot/send_message" : "/open/claw/send_message";
86
+ let payloadStr;
87
+ if (isBot) {
88
+ let botContentObj;
89
+ if (msgType === types_js_1.TypeXMessageEnum.text || msgType === types_js_1.TypeXMessageEnum.richText) {
90
+ // According to docs, text type content format: {"text":"test"}
91
+ // Assuming content or finalContent holds the actual string text.
92
+ botContentObj = {
93
+ text: typeof content === "string" ? content : (typeof finalContent === "string" ? finalContent : JSON.stringify(content))
94
+ };
95
+ // Ensure msgType is 0 when sending to `/open/robot/send_message` since 8 might not be supported natively by robot API
96
+ msgType = types_js_1.TypeXMessageEnum.text;
97
+ }
98
+ else {
99
+ // Image or File object payload for bot
100
+ botContentObj = typeof finalContent === "string" ? { text: finalContent } : content;
128
101
  }
129
- if (prompter)
130
- prompter.note("Message sent successfully", resJson.data);
131
- else
132
- console.log("Message sent successfully", JSON.stringify(resJson.data));
133
- return (resJson.data || {
134
- message_id: `msg_${Date.now()}`,
102
+ payloadStr = JSON.stringify({
103
+ chat_id: to,
104
+ content: botContentObj,
105
+ msg_type: msgType,
106
+ reply_msg_id: options.replyMsgId || "0",
135
107
  });
136
108
  }
137
- catch (error) {
138
- if (prompter)
139
- prompter.note(`Error sending message to TypeX API: ${error}`);
140
- else
141
- console.log(`Error sending message to TypeX API: ${error}`);
142
- throw error;
109
+ else {
110
+ payloadStr = JSON.stringify({
111
+ chat_id: to,
112
+ content: { text: finalContent },
113
+ msg_type: msgType,
114
+ });
115
+ }
116
+ const response = await fetch(`${TYPEX_DOMAIN}${endpoint}`, {
117
+ method: "POST",
118
+ headers: {
119
+ "Content-Type": "application/json",
120
+ ...(isBot ? { Authorization: `Bearer ${token}`, "x-developer": "ryan" } : { Cookie: token }),
121
+ },
122
+ body: payloadStr,
123
+ });
124
+ const bodyText = await response.text();
125
+ let resJson;
126
+ try {
127
+ resJson = JSON.parse(bodyText);
128
+ }
129
+ catch (e) {
130
+ throw new Error(`Send message failed (invalid JSON): HTTP ${response.status} - ${bodyText}`);
131
+ }
132
+ if (resJson.code !== 0) {
133
+ throw new Error(`Send message failed: [${resJson.code}] ${resJson.msg || resJson.message}`);
134
+ }
135
+ return resJson.data || { message_id: `msg_${Date.now()}` };
136
+ }
137
+ /**
138
+ * Upload resource for the robot to send.
139
+ * @param fileName Name of the file
140
+ * @param fileType "image" | "audio" | "video" | "application"
141
+ * @param fileContent Buffer or Blob containing the file data
142
+ * @param chatId Optional chat_id
143
+ */
144
+ async uploadResource(fileName, fileType, fileContent, chatId) {
145
+ if (this.mode !== "bot" || !this.accessToken) {
146
+ throw new Error("TypeXClient: uploadResource requires bot mode and an access token.");
147
+ }
148
+ const formData = new FormData();
149
+ if (chatId)
150
+ formData.append("chat_id", chatId);
151
+ formData.append("file_name", fileName);
152
+ formData.append("file_type", fileType);
153
+ // Node.js fetch implementation of FormData requires a Blob-like object for files.
154
+ // By providing a Blob we ensure it correctly adds boundaries and content types per form part.
155
+ const blob = fileContent instanceof Buffer ? new Blob([fileContent]) : fileContent;
156
+ formData.append("file_content", blob, fileName);
157
+ const response = await fetch(`${TYPEX_DOMAIN}/open/robot/upload`, {
158
+ method: "POST",
159
+ headers: {
160
+ Authorization: `Bearer ${this.accessToken}`,
161
+ "x-developer": "ryan"
162
+ // Note: fetch will automatically set the Content-Type boundary
163
+ },
164
+ body: formData,
165
+ });
166
+ const resJson = await response.json();
167
+ if (resJson.code !== 0) {
168
+ throw new Error(`Upload resource failed: [${resJson.code}] ${resJson.msg || resJson.message}`);
143
169
  }
170
+ return resJson.data;
144
171
  }
172
+ /**
173
+ * Fetch messages. Dispatches to user or bot endpoint based on mode.
174
+ */
145
175
  async fetchMessages(pos) {
146
- if (!this.accessToken) {
147
- if (prompter)
148
- prompter.note("TypeXClient: No token, skipping fetch.");
149
- else
150
- console.log("TypeXClient: No token, skipping fetch.");
176
+ return this.mode === "bot"
177
+ ? this.fetchBotMessages()
178
+ : this.fetchUserMessages(pos);
179
+ }
180
+ /** Pull messages for a regular user account (sessionid cookie auth). */
181
+ async fetchUserMessages(pos) {
182
+ if (!this.accessToken)
151
183
  return [];
152
- }
153
184
  try {
154
- const url = `${TYPEX_DOMAIN}/open/claw/message`;
155
- if (prompter)
156
- prompter.note(`Fetching messages from pos: ${pos}`);
157
- // else console.log(`Fetching messages from pos: ${pos}`);
158
- const response = await fetch(url, {
185
+ const response = await fetch(`${TYPEX_DOMAIN}/open/claw/message`, {
159
186
  method: "POST",
160
- headers: {
161
- Cookie: this.accessToken,
162
- "Content-Type": "application/json",
163
- },
164
- body: JSON.stringify({ pos: pos }),
187
+ headers: { Cookie: this.accessToken, "Content-Type": "application/json", 'x-developer': 'ryan' },
188
+ body: JSON.stringify({ pos }),
165
189
  });
166
190
  const resJson = await response.json();
167
- if (resJson.code !== 0) {
168
- if (prompter)
169
- prompter.note(`Fetch failed with code ${resJson.code}: ${resJson.message}`);
170
- else
171
- console.log(`Fetch failed with code ${resJson.code}: ${resJson.message}`);
191
+ if (resJson.code !== 0)
172
192
  return [];
173
- }
174
- if (Array.isArray(resJson.data)) {
175
- return resJson.data;
176
- }
193
+ return Array.isArray(resJson.data) ? resJson.data : [];
194
+ }
195
+ catch (e) {
196
+ console.log(`Fetch messages error: ${e}`);
177
197
  return [];
178
198
  }
199
+ }
200
+ /**
201
+ * Pull messages for a bot account (Bearer token auth).
202
+ * TODO: replace /open/bot/message with the actual endpoint path once confirmed.
203
+ */
204
+ async fetchBotMessages() {
205
+ if (!this.accessToken)
206
+ return [];
207
+ try {
208
+ const response = await fetch(`${TYPEX_DOMAIN}/open/robot/message/pull`, {
209
+ method: "POST",
210
+ headers: { Authorization: `Bearer ${this.accessToken}`, "Content-Type": "application/json", 'x-developer': 'ryan' },
211
+ body: JSON.stringify({ limit: 5 }),
212
+ });
213
+ const resJson = await response.json();
214
+ if (resJson.code !== 0)
215
+ return [];
216
+ return Array.isArray(resJson.data?.messages) ? resJson.data.messages : [];
217
+ }
179
218
  catch (e) {
180
- if (prompter)
181
- prompter.note(`Fetch messages network error: ${e}`);
182
- else
183
- console.log(`Fetch messages network error: ${e}`);
219
+ console.log(`Bot fetch messages error: ${e}`);
184
220
  return [];
185
221
  }
186
222
  }
223
+ /**
224
+ * Fetch a single message by ID (used to resolve quoted/parent messages).
225
+ */
226
+ async getMessage(messageId) {
227
+ if (!this.accessToken)
228
+ return null;
229
+ try {
230
+ const isBot = this.mode === "bot";
231
+ const response = await fetch(`${TYPEX_DOMAIN}/open/claw/message/${messageId}`, {
232
+ method: "GET",
233
+ headers: isBot
234
+ ? { Authorization: `Bearer ${this.accessToken}`, "Content-Type": "application/json", 'x-developer': 'ryan' }
235
+ : { Cookie: this.accessToken, "Content-Type": "application/json" },
236
+ });
237
+ const resJson = await response.json();
238
+ return resJson.code === 0 && resJson.data ? resJson.data : null;
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ }
244
+ /**
245
+ * Fetch file binary stream from TypeX using object_key.
246
+ * Requires Bot Token authentication.
247
+ */
248
+ async fetchFileBuffer(objectKey, size) {
249
+ if (!this.accessToken || this.mode !== "bot")
250
+ return null;
251
+ try {
252
+ const query = new URLSearchParams({ object_key: objectKey });
253
+ if (size)
254
+ query.append("size", size);
255
+ const url = `${TYPEX_DOMAIN}/open/robot/chat/file?${query.toString()}`;
256
+ const response = await fetch(url, {
257
+ method: "GET",
258
+ headers: { Authorization: `Bearer ${this.accessToken}`, 'x-developer': 'ryan' },
259
+ });
260
+ if (!response.ok) {
261
+ console.log(`fetchFileBuffer failed with status: ${response.status} ${response.statusText} for url: ${url}`);
262
+ return null;
263
+ }
264
+ const arrayBuffer = await response.arrayBuffer();
265
+ const buffer = Buffer.from(arrayBuffer);
266
+ const mimeType = response.headers.get("content-type") ?? "application/octet-stream";
267
+ return { buffer, mimeType };
268
+ }
269
+ catch (e) {
270
+ console.log(`fetchFileBuffer error: ${e}`);
271
+ return null;
272
+ }
273
+ }
187
274
  }
188
275
  exports.TypeXClient = TypeXClient;
189
276
  function getTypeXClient(accountId, manualOptions) {
190
277
  const typexCfg = (manualOptions?.typexCfg ?? {});
191
- const clawPrompter = manualOptions?.prompter;
192
- if (clawPrompter) {
193
- prompter = clawPrompter;
194
- }
278
+ if (manualOptions?.prompter)
279
+ prompter = manualOptions.prompter;
195
280
  let token = manualOptions?.token;
281
+ let mode = manualOptions?.mode ?? "user";
196
282
  if (accountId && typexCfg.accounts?.[accountId]) {
197
- token = typexCfg.accounts[accountId].token;
283
+ const acctCfg = typexCfg.accounts[accountId];
284
+ token = token ?? acctCfg.token;
285
+ if (acctCfg.mode === "bot" || acctCfg.mode === "user")
286
+ mode = acctCfg.mode;
198
287
  }
288
+ // Config check: outbound sends should fail only when we truly lack credentials.
289
+ // Historically this was a stub that always threw, which broke outbound delivery.
199
290
  if (!manualOptions?.skipConfigCheck) {
200
- throw new Error("TypeX not configured yet.");
291
+ if (!token?.trim()) {
292
+ throw new Error("TypeX not configured: missing token. Run the TypeX onboarding (QR login / bot token) or set channels.openclaw-extension-typex.accounts.<accountId>.token");
293
+ }
201
294
  }
202
- return new TypeXClient({
203
- token: token,
204
- });
295
+ return new TypeXClient({ token, mode });
205
296
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * message-helpers.ts
3
+ *
4
+ * Pure, stateless helper functions for TypeX message processing.
5
+ * All functions are free of side-effects and easy to unit-test.
6
+ */
7
+ import { type TypeXMessageEntry, type TypeXMention } from "./types.js";
8
+ export declare function normalizeMessageToText(msg: TypeXMessageEntry): string;
9
+ export declare function checkBotMentioned(msg: TypeXMessageEntry, botId?: string): boolean;
10
+ export declare function stripBotMention(text: string, mentions?: TypeXMention[]): string;
11
+ export type TypeXGroupConfig = {
12
+ enabled?: boolean;
13
+ requireMention?: boolean;
14
+ allowFrom?: Array<string | number>;
15
+ };
16
+ export declare function resolveGroupConfig(typexCfg: Record<string, any>, groupId: string): TypeXGroupConfig | undefined;
17
+ export declare function normalizeAllowEntry(raw: string): string;
18
+ export declare function isAllowedBySenderId(allowFrom: Array<string | number>, senderId: string): boolean;
19
+ export declare function buildAgentBody(params: {
20
+ messageId: string;
21
+ senderLabel: string;
22
+ content: string;
23
+ quotedContent?: string;
24
+ }): string;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * message-helpers.ts
4
+ *
5
+ * Pure, stateless helper functions for TypeX message processing.
6
+ * All functions are free of side-effects and easy to unit-test.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.normalizeMessageToText = normalizeMessageToText;
10
+ exports.checkBotMentioned = checkBotMentioned;
11
+ exports.stripBotMention = stripBotMention;
12
+ exports.resolveGroupConfig = resolveGroupConfig;
13
+ exports.normalizeAllowEntry = normalizeAllowEntry;
14
+ exports.isAllowedBySenderId = isAllowedBySenderId;
15
+ exports.buildAgentBody = buildAgentBody;
16
+ const types_js_1 = require("./types.js");
17
+ // ---------------------------------------------------------------------------
18
+ // Message content normalisation
19
+ // ---------------------------------------------------------------------------
20
+ function normalizeMessageToText(msg) {
21
+ const typeNum = Number(msg.msg_type);
22
+ switch (typeNum) {
23
+ case types_js_1.TypeXMessageEnum.text:
24
+ case types_js_1.TypeXMessageEnum.richText: {
25
+ const parsedContent = typeof msg.content === "string" ? JSON.parse(msg.content) : msg.content;
26
+ return parsedContent.text ?? parsedContent.preview_text ?? "";
27
+ }
28
+ case types_js_1.TypeXMessageEnum.image:
29
+ case types_js_1.TypeXMessageEnum.photoCollageMsg: {
30
+ const parsedContent = typeof msg.content === "string" ? JSON.parse(msg.content) : msg.content;
31
+ const text = (parsedContent.text ?? parsedContent.preview_text ?? "").trim();
32
+ return text ? `${text} <media:image>` : "<media:image>";
33
+ }
34
+ case types_js_1.TypeXMessageEnum.file:
35
+ case types_js_1.TypeXMessageEnum.fileGroup:
36
+ return msg.content.file_name ? `<media:file:${msg.content.file_name}>` : "<media:file>";
37
+ case types_js_1.TypeXMessageEnum.video:
38
+ return "<media:video>";
39
+ case types_js_1.TypeXMessageEnum.emoji:
40
+ return "<media:sticker>";
41
+ case types_js_1.TypeXMessageEnum.newCard: {
42
+ const card = msg.content.card;
43
+ if (!card)
44
+ return "[Card message]";
45
+ try {
46
+ const raw = typeof card === "string" ? card : JSON.stringify(card);
47
+ const parsed = JSON.parse(raw);
48
+ const title = parsed.header?.title ?? parsed.title ?? "";
49
+ const body = parsed.body ?? parsed.content ?? parsed.text ?? "";
50
+ const parts = [title, body].map((p) => String(p ?? "").trim()).filter(Boolean);
51
+ return parts.length > 0 ? parts.join("\n") : "[Card message]";
52
+ }
53
+ catch {
54
+ return "[Card message]";
55
+ }
56
+ }
57
+ case types_js_1.TypeXMessageEnum.forward: {
58
+ const items = msg.content.items ?? [];
59
+ if (items.length === 0)
60
+ return "[Merged and Forwarded Messages]";
61
+ const lines = ["[Merged and Forwarded Messages]"];
62
+ const limit = Math.min(items.length, 50);
63
+ for (let i = 0; i < limit; i++) {
64
+ const item = items[i];
65
+ const itemText = item.content?.text?.trim() ?? `[type: ${item.msg_type ?? "unknown"}]`;
66
+ const sender = item.sender?.name ?? item.sender?.id ?? "unknown";
67
+ lines.push(`- ${sender}: ${itemText}`);
68
+ }
69
+ if (items.length > 50)
70
+ lines.push(`... and ${items.length - 50} more messages`);
71
+ return lines.join("\n");
72
+ }
73
+ case types_js_1.TypeXMessageEnum.mentioned:
74
+ case types_js_1.TypeXMessageEnum.custom:
75
+ case types_js_1.TypeXMessageEnum.via:
76
+ return msg.content.text ?? `[Message type: ${typeNum}]`;
77
+ default:
78
+ return `[Unsupported message type: ${msg.msg_type}]`;
79
+ }
80
+ }
81
+ // ---------------------------------------------------------------------------
82
+ // @mention helpers
83
+ // ---------------------------------------------------------------------------
84
+ function checkBotMentioned(msg, botId) {
85
+ if (!botId)
86
+ return false;
87
+ return (msg.mentions ?? []).some((m) => m.id.open_id === botId || m.id.user_id === botId);
88
+ }
89
+ function stripBotMention(text, mentions) {
90
+ if (!mentions || mentions.length === 0)
91
+ return text;
92
+ let result = text;
93
+ for (const mention of mentions) {
94
+ result = result.replace(new RegExp(`@${escapeRegExp(mention.name)}\\s*`, "g"), "");
95
+ if (mention.key)
96
+ result = result.replace(new RegExp(escapeRegExp(mention.key), "g"), "");
97
+ }
98
+ return result.trim();
99
+ }
100
+ function escapeRegExp(s) {
101
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
102
+ }
103
+ function resolveGroupConfig(typexCfg, groupId) {
104
+ const groups = typexCfg?.groups ?? {};
105
+ return groups[groupId] ?? groups["*"];
106
+ }
107
+ function normalizeAllowEntry(raw) {
108
+ return raw.trim().toLowerCase().replace(/^typex:/i, "");
109
+ }
110
+ function isAllowedBySenderId(allowFrom, senderId) {
111
+ const norm = allowFrom.map((e) => normalizeAllowEntry(String(e))).filter(Boolean);
112
+ if (norm.includes("*"))
113
+ return true;
114
+ return norm.includes(normalizeAllowEntry(senderId));
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // Agent message body builder
118
+ // ---------------------------------------------------------------------------
119
+ function buildAgentBody(params) {
120
+ const { messageId, senderLabel, content, quotedContent } = params;
121
+ let body = content;
122
+ if (quotedContent)
123
+ body = `[Replying to: "${quotedContent}"]\n\n${body}`;
124
+ return `[message_id: ${messageId}]\n${senderLabel}: ${body}`;
125
+ }
@@ -1,16 +1,23 @@
1
+ /**
2
+ * message.ts
3
+ *
4
+ * Main message dispatch orchestrator for the TypeX standalone plugin.
5
+ * Pure helpers live in ./message-helpers.ts.
6
+ */
7
+ import type { HistoryEntry, OpenClawConfig } from "openclaw/plugin-sdk";
1
8
  import type { TypeXClient } from "./client.js";
2
9
  import { type TypeXMessageEntry } from "./types.js";
3
- import type { OpenClawConfig } from "openclaw/plugin-sdk";
4
10
  export type ProcessTypeXMessageOptions = {
5
- /** Full OpenClaw gateway config (needed for bindings/routing). */
6
11
  cfg?: OpenClawConfig;
7
12
  accountId?: string;
8
13
  botName?: string;
9
14
  typexCfg?: Record<string, any>;
15
+ chatHistories?: Map<string, HistoryEntry[]>;
10
16
  logger?: {
11
17
  warn: (msg: string) => void;
12
18
  info: (msg: string) => void;
13
19
  error: (msg: string) => void;
14
- } | undefined;
20
+ };
15
21
  };
22
+ export { buildAgentBody, checkBotMentioned, normalizeMessageToText, stripBotMention } from "./message-helpers.js";
16
23
  export declare function processTypeXMessage(client: TypeXClient, payload: TypeXMessageEntry, appId: string, options?: ProcessTypeXMessageOptions): Promise<void>;