@xgjktech/xg_cwork_im 1.0.6 → 1.0.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.
@@ -1,68 +1,215 @@
1
- /**
2
- * XG-IM 消息发送模块
3
- *
4
- * 调用 IM 接口将 AI 回复发送到指定群聊,并 @ 原始发送者。
5
- */
6
-
7
- import axios from "axios";
8
- import type { Log } from "./auth.js";
9
- import type { GetLatestMsgListResponse, SendMessageBody, WsMessageParams, XgImConfig } from "./types.js";
10
-
11
- /**
12
- * 发送文本消息到指定 IM 群聊。
13
- *
14
- * @param config 插件配置
15
- * @param token 鉴权 token
16
- * @param groupId 目标群聊 ID(gid / groupId)
17
- * @param content 消息内容文本
18
- * @param atUserIds 需要 @ 的用户 ID 列表(可为空)
19
- * @param log 日志接口
20
- * @param msgId 可选的业务消息 ID(用于覆盖/更新已有消息)
21
- * @param reply 可选的被回复消息信息(用于在 IM 中建立“回复某条消息”的关联)
22
- */
23
- export async function sendTextMessage(
24
- config: XgImConfig,
25
- token: string,
26
- groupId: string,
27
- content: string,
28
- atUserIds: string[] = [],
29
- log?: Log,
30
- msgId?: string,
31
- reply?: SendMessageBody["reply"],
32
- ): Promise<void> {
33
- const url = `${config.baseUrl}/im/message/send`;
34
-
35
- const body: SendMessageBody = {
36
- type: "RICH_TEXT",
37
- groupId,
38
- text: content,
39
- ...(atUserIds.length > 0 ? { atUserIds } : {}),
40
- ...(msgId ? { msgId } : {}),
41
- ...(reply ? { reply } : {}),
42
- };
43
-
44
- log?.info(`[cwork_im:send] POST ${url} groupId=${groupId} atUsers=${JSON.stringify(atUserIds)}`);
45
- if (config.debug) {
46
- log?.debug?.(`[cwork_im:send] Request body: ${JSON.stringify(body)}`);
47
- }
48
-
49
- try {
50
- const res = await axios.post(url, body, {
51
- headers: {
52
- "Content-Type": "application/json",
53
- "access-token": token,
54
- },
55
- timeout: 10_000,
56
- });
57
-
58
- log?.info(`[cwork_im:send] Response status=${res.status}`);
59
- if (config.debug) {
60
- log?.debug?.(`[cwork_im:send] Response body: ${JSON.stringify(res.data)}`);
61
- }
62
- } catch (err: unknown) {
63
- const msg = err instanceof Error ? err.message : String(err);
64
- log?.error(`[cwork_im:send] Failed to send message to groupId=${groupId}: ${msg}`);
65
- throw err;
66
- }
67
- }
68
-
1
+ /**
2
+ * XG-IM 消息发送模块
3
+ *
4
+ * 调用 IM 接口将 AI 回复发送到指定群聊,并 @ 原始发送者。
5
+ * 支持 RICH_TEXT 与带资源 fileId 的 FILE 类型(attachments)。
6
+ */
7
+
8
+ import axios from "axios";
9
+ import type { Log } from "./auth.js";
10
+ import {
11
+ downloadMediaBuffer,
12
+ formatFromFilename,
13
+ resolveMaxAttachmentBytes,
14
+ resolveResourceUploadUrl,
15
+ uploadWholeResourceFile,
16
+ } from "./resource-file.js";
17
+ import type { SendMessageBody, SendMessageFileAttachment, SendMessageReply, XgImConfig } from "./types.js";
18
+
19
+ function formatDeliverError(err: unknown): string {
20
+ if (axios.isAxiosError(err)) {
21
+ const parts = [err.message];
22
+ if (err.code) parts.push(`axiosCode=${err.code}`);
23
+ if (err.response?.status != null) parts.push(`httpStatus=${err.response.status}`);
24
+ return parts.join(" ");
25
+ }
26
+ if (err instanceof Error) return err.message;
27
+ return String(err);
28
+ }
29
+
30
+ async function postImMessage(
31
+ config: XgImConfig,
32
+ token: string,
33
+ groupId: string,
34
+ body: SendMessageBody,
35
+ atUserIds: string[] = [],
36
+ log?: Log,
37
+ ): Promise<void> {
38
+ const url = `${config.baseUrl}/im/message/send`;
39
+ const full: SendMessageBody = {
40
+ ...body,
41
+ groupId,
42
+ ...(atUserIds.length > 0 ? { atUserIds } : {}),
43
+ } as SendMessageBody;
44
+
45
+ log?.info(`[cwork_im:send] POST ${url} groupId=${groupId} type=${full.type} atUsers=${JSON.stringify(atUserIds)}`);
46
+ if (config.debug) {
47
+ log?.debug?.(`[cwork_im:send] Request body: ${JSON.stringify(full)}`);
48
+ }
49
+
50
+ try {
51
+ const res = await axios.post(url, full, {
52
+ headers: {
53
+ "Content-Type": "application/json",
54
+ "access-token": token,
55
+ },
56
+ timeout: 10_000,
57
+ });
58
+
59
+ log?.info(`[cwork_im:send] Response status=${res.status}`);
60
+ if (config.debug) {
61
+ log?.debug?.(`[cwork_im:send] Response body: ${JSON.stringify(res.data)}`);
62
+ }
63
+ } catch (err: unknown) {
64
+ const msg = err instanceof Error ? err.message : String(err);
65
+ log?.error(`[cwork_im:send] Failed to send message to groupId=${groupId}: ${msg}`);
66
+ throw err;
67
+ }
68
+ }
69
+
70
+ /** OpenClaw dispatch deliver 单块载荷:文本 + 可选媒体 URL(与 plugin-sdk 约定对齐)。 */
71
+ export type ReplyDeliverPayload = {
72
+ markdown?: string;
73
+ text?: string;
74
+ isThinking?: boolean;
75
+ mediaUrl?: string;
76
+ mediaUrls?: string[];
77
+ };
78
+
79
+ /**
80
+ * 将 deliver 块发到 IM:若有媒体则先 POST baseUrl/file/upDownload/uploadWholeFile 再发 FILE;否则发 RICH_TEXT。
81
+ */
82
+ export async function sendReplyDeliverBlock(
83
+ config: XgImConfig,
84
+ token: string,
85
+ groupId: string,
86
+ payload: ReplyDeliverPayload,
87
+ atUserIds: string[] = [],
88
+ log?: Log,
89
+ msgId?: string,
90
+ reply?: SendMessageReply,
91
+ ): Promise<void> {
92
+ const textToSend = (payload.markdown || payload.text || "").trim();
93
+ const mediaList = [
94
+ ...(payload.mediaUrl ? [payload.mediaUrl] : []),
95
+ ...((payload.mediaUrls ?? []).filter((u) => typeof u === "string" && u.trim())),
96
+ ];
97
+ const uniqueUrls = [...new Set(mediaList)];
98
+ const maxBytes = resolveMaxAttachmentBytes(config);
99
+ const formField = config.fileUploadFormField?.trim() || "file";
100
+
101
+ const wantFile = uniqueUrls.length > 0 && !payload.isThinking;
102
+
103
+ if (wantFile) {
104
+ const fileUploadUrl = resolveResourceUploadUrl(config.baseUrl);
105
+ try {
106
+ const attachments: SendMessageFileAttachment[] = [];
107
+ for (const u of uniqueUrls) {
108
+ const { buffer, filename } = await downloadMediaBuffer(u, maxBytes, log);
109
+ const { fileId } = await uploadWholeResourceFile({
110
+ fileUploadUrl,
111
+ token,
112
+ buffer,
113
+ filename,
114
+ formField,
115
+ log,
116
+ });
117
+ attachments.push({
118
+ fileId,
119
+ fileType: "FILE",
120
+ format: formatFromFilename(filename),
121
+ name: filename,
122
+ size: buffer.length,
123
+ source: "resource",
124
+ });
125
+ }
126
+ const caption = textToSend.length > 0 ? textToSend : " ";
127
+ await postImMessage(
128
+ config,
129
+ token,
130
+ groupId,
131
+ {
132
+ type: "FILE",
133
+ text: caption,
134
+ attachments,
135
+ ...(msgId ? { msgId } : {}),
136
+ ...(reply ? { reply } : {}),
137
+ },
138
+ atUserIds,
139
+ log,
140
+ );
141
+ return;
142
+ } catch (err: unknown) {
143
+ log?.error(`[cwork_im:send] FILE deliver failed, fallback to text if any: ${formatDeliverError(err)}`);
144
+ if (textToSend.length > 0) {
145
+ await postImMessage(
146
+ config,
147
+ token,
148
+ groupId,
149
+ {
150
+ type: "RICH_TEXT",
151
+ text: `${textToSend}\n\n(媒体未发出:${formatDeliverError(err)})`,
152
+ ...(msgId ? { msgId } : {}),
153
+ ...(reply ? { reply } : {}),
154
+ },
155
+ atUserIds,
156
+ log,
157
+ );
158
+ }
159
+ return;
160
+ }
161
+ }
162
+
163
+ if (textToSend.length > 0) {
164
+ await postImMessage(
165
+ config,
166
+ token,
167
+ groupId,
168
+ {
169
+ type: "RICH_TEXT",
170
+ text: textToSend,
171
+ ...(msgId ? { msgId } : {}),
172
+ ...(reply ? { reply } : {}),
173
+ },
174
+ atUserIds,
175
+ log,
176
+ );
177
+ }
178
+ }
179
+
180
+ /**
181
+ * 发送文本消息到指定 IM 群聊。
182
+ *
183
+ * @param config 插件配置
184
+ * @param token 鉴权 token
185
+ * @param groupId 目标群聊 ID(gid / groupId)
186
+ * @param content 消息内容文本
187
+ * @param atUserIds 需要 @ 的用户 ID 列表(可为空)
188
+ * @param log 日志接口
189
+ * @param msgId 可选的业务消息 ID(用于覆盖/更新已有消息)
190
+ * @param reply 可选的被回复消息信息(用于在 IM 中建立“回复某条消息”的关联)
191
+ */
192
+ export async function sendTextMessage(
193
+ config: XgImConfig,
194
+ token: string,
195
+ groupId: string,
196
+ content: string,
197
+ atUserIds: string[] = [],
198
+ log?: Log,
199
+ msgId?: string,
200
+ reply?: SendMessageReply,
201
+ ): Promise<void> {
202
+ await postImMessage(
203
+ config,
204
+ token,
205
+ groupId,
206
+ {
207
+ type: "RICH_TEXT",
208
+ text: content,
209
+ ...(msgId ? { msgId } : {}),
210
+ ...(reply ? { reply } : {}),
211
+ },
212
+ atUserIds,
213
+ log,
214
+ );
215
+ }
@@ -0,0 +1,24 @@
1
+ import type { AnyAgentTool } from "openclaw/plugin-sdk";
2
+
3
+ type ToolExecuteReturn = Awaited<ReturnType<NonNullable<AnyAgentTool["execute"]>>>;
4
+
5
+ /**
6
+ * 与 `openclaw/plugin-sdk` 的 `jsonResult` 返回结构一致(AgentToolResult),
7
+ * 供工具 `execute` 使用。
8
+ *
9
+ * 部分环境下网关打包插件后,`import { jsonResult } from "openclaw/plugin-sdk"` 会变成
10
+ * `_pluginSdk.jsonResult` 且值为 `undefined`,调用时报
11
+ * `(0 , _pluginSdk.jsonResult) is not a function`。
12
+ * 使用本函数可避免依赖该命名导出在打包产物中是否保留。
13
+ */
14
+ export function toolJsonResult(payload: unknown): ToolExecuteReturn {
15
+ if (payload === undefined) {
16
+ return { content: [{ type: "text", text: "" }] } as ToolExecuteReturn;
17
+ }
18
+ const text =
19
+ typeof payload === "string" ? JSON.stringify(payload) : JSON.stringify(payload, null, 2);
20
+ return {
21
+ content: [{ type: "text", text }],
22
+ details: payload,
23
+ } as ToolExecuteReturn;
24
+ }