@xgjktech/xg_cwork_im 1.0.9 → 1.10.0

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/README.md CHANGED
@@ -232,7 +232,8 @@ openclaw gateway restart
232
232
  | `maxReconnectDelay` | number | `60000` | 最大重连延迟(ms) |
233
233
  | `reconnectJitter` | number | `0.3` | 重连抖动因子(0-1) |
234
234
  | `firstReplyTimeoutMs` | number | `300000` | **首条 AI 回复超时时间(毫秒)**。用于保护「思考中」占位消息:若在该时间窗口内 AI 没有任何回复,则自动将占位消息更新为「当前请求处理超时,请稍后重试」。|
235
- | `inboundMediaWorkspaceSubdir` | string | — | **可选**。非空时,将入站附件先下载到**当前 Agent workspace** 下该**相对子目录**(如 `xg_im_inbound`),路径约定:`{子目录}/{发送者 userId}/{YYYY-MM-DD}/{文件名}_{HHmmssmmm}.ext`(时间为本机写入时刻),再向 OpenClaw 传入本地 `file://` 路径;未配置则仍使用 IM 返回的 URL。需 OpenClaw runtime 提供 `resolveAgentWorkspaceDir`。 |
235
+ | `inboundMediaWorkspaceSubdir` | string | — | **可选**。非空时,将入站附件先下载到**当前 Agent workspace** 下该**相对子目录**(如 `xg_im_inbound`),路径约定:`{子目录}/{发送者 userId}/{YYYY-MM-DD}/{文件名}_{HHmmssmmm}.ext`(时间为本机写入时刻),再向 OpenClaw 传入 **MediaPath**;默认使用 **workspace 相对路径**(如 `./xg_im_inbound/...`),便于 **沙箱** 将 `/workspace` 映射到同一套目录树;未配置则仍使用 IM 返回的 URL。需 runtime 提供 `resolveAgentWorkspaceDir`。 |
236
+ | `inboundMediaOpenClawPath` | string | `workspaceRelative`(默认) | `workspaceRelative`:入站落盘后 `MediaPath` 为 `./子目录/...`;`absoluteFileUrl`:宿主机 `file://...`(仅适合无沙箱、网关与 Agent 同盘场景)。 |
236
237
 
237
238
  ### 账户配置(`accounts.<accountId>` / `accounts[n]`)
238
239
 
@@ -243,6 +244,7 @@ openclaw gateway restart
243
244
  | `name` | string | — | 账户显示名称(仅用于日志标识) |
244
245
  | `groupPolicy` | string | 继承顶层 | 可覆盖顶层的 groupPolicy |
245
246
  | `inboundMediaWorkspaceSubdir` | string | 继承顶层 | 可覆盖顶层的入站附件落盘子目录 |
247
+ | `inboundMediaOpenClawPath` | string | 继承顶层 | 可覆盖顶层的 MediaPath 形式(相对路径 vs file://) |
246
248
 
247
249
  ---
248
250
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xgjktech/xg_cwork_im",
3
- "version": "1.0.9",
3
+ "version": "1.10.0",
4
4
  "description": "XG CWork IM channel plugin for OpenClaw",
5
5
  "keywords": [
6
6
  "bot",
package/src/channel.ts CHANGED
@@ -55,6 +55,7 @@ const XgImAccountConfigSchema = z.object({
55
55
  fileUploadFormField: z.string().min(1).optional(),
56
56
  maxAttachmentBytes: z.number().int().positive().optional(),
57
57
  inboundMediaWorkspaceSubdir: z.string().optional(),
58
+ inboundMediaOpenClawPath: z.enum(["workspaceRelative", "absoluteFileUrl"]).optional(),
58
59
  });
59
60
 
60
61
  const XgImConfigSchema = z.object({
@@ -75,9 +76,10 @@ const XgImConfigSchema = z.object({
75
76
  maxAttachmentBytes: z.number().int().positive().optional(),
76
77
  /**
77
78
  * 非空时:将入站附件下载到 `resolveAgentWorkspaceDir(cfg)` 下该相对子目录,
78
- * 再向 OpenClaw 传本地 file:// 路径(见 inbound-media-local)。
79
+ * 再向 OpenClaw workspace 相对路径或 file://(见 inboundMediaOpenClawPath、inbound-media-local)。
79
80
  */
80
81
  inboundMediaWorkspaceSubdir: z.string().optional(),
82
+ inboundMediaOpenClawPath: z.enum(["workspaceRelative", "absoluteFileUrl"]).optional(),
81
83
  // 多账户:对象 map(key 为 accountId)
82
84
  accounts: z.record(z.string(), XgImAccountConfigSchema).optional(),
83
85
  }).superRefine((val, ctx) => {
@@ -84,6 +84,38 @@ function sanitizeStem(stem: string): string {
84
84
  return out.length > MAX_STEM_LEN ? out.slice(0, MAX_STEM_LEN) : out;
85
85
  }
86
86
 
87
+ /**
88
+ * 将落盘绝对路径转为 OpenClaw / 沙箱可用的 workspace 相对引用(POSIX 风格,带 `./` 前缀)。
89
+ * 若文件不在 workspaceRoot 之下则返回 null(调用方应回退 file:// 或报错)。
90
+ */
91
+ export function absPathToWorkspaceRelativeOpenClawRef(workspaceRoot: string, absPath: string): string | null {
92
+ const root = path.resolve(workspaceRoot.trim());
93
+ const abs = path.resolve(absPath);
94
+ const rel = path.relative(root, abs);
95
+ if (rel.startsWith("..") || path.isAbsolute(rel)) return null;
96
+ const piece = rel.length > 0 ? rel : path.basename(abs);
97
+ const posix = piece.split(path.sep).join("/");
98
+ return posix.startsWith("./") ? posix : `./${posix}`;
99
+ }
100
+
101
+ function openClawMediaRefForSavedFile(args: {
102
+ workspaceRoot: string;
103
+ absPath: string;
104
+ style: "workspaceRelative" | "absoluteFileUrl" | undefined;
105
+ log?: Log;
106
+ }): string {
107
+ const { workspaceRoot, absPath, style, log } = args;
108
+ if (style === "absoluteFileUrl") {
109
+ return pathToFileURL(absPath).href;
110
+ }
111
+ const rel = absPathToWorkspaceRelativeOpenClawRef(workspaceRoot, absPath);
112
+ if (rel) return rel;
113
+ log?.warn?.(
114
+ `[cwork_im:inbound-local] file not under workspace root, fallback to file:// absPath=${absPath.slice(0, 120)}`,
115
+ );
116
+ return pathToFileURL(absPath).href;
117
+ }
118
+
87
119
  /** `report_120459237.pdf` */
88
120
  export function buildInboundStoredFilename(originalFilename: string, at: Date): string {
89
121
  const base = path.basename(originalFilename) || "file";
@@ -140,11 +172,19 @@ export async function saveInboundFilesToWorkspace(args: {
140
172
  await mkdir(dir, { recursive: true });
141
173
  const { buffer } = await downloadMediaBuffer(srcUrl, maxBytes, log);
142
174
  await writeFile(absPath, buffer);
143
- const fileUrl = pathToFileURL(absPath).href;
144
- log?.info?.(`[cwork_im:inbound-local] saved ${localName} bytes=${buffer.length} -> ${absPath}`);
175
+ const pathStyle = config.inboundMediaOpenClawPath ?? "workspaceRelative";
176
+ const mediaRef = openClawMediaRefForSavedFile({
177
+ workspaceRoot,
178
+ absPath,
179
+ style: pathStyle,
180
+ log,
181
+ });
182
+ log?.info?.(
183
+ `[cwork_im:inbound-local] saved ${localName} bytes=${buffer.length} abs=${absPath} openclawRef=${mediaRef.slice(0, 120)}`,
184
+ );
145
185
  out.push({
146
186
  ...item,
147
- url: fileUrl,
187
+ url: mediaRef,
148
188
  size: buffer.length,
149
189
  name: localName,
150
190
  });
package/src/types.ts CHANGED
@@ -1,301 +1,312 @@
1
- /**
2
- * XG-IM Channel Plugin — 类型定义
3
- */
4
-
5
- import type {
6
- OpenClawConfig,
7
- OpenClawPluginApi,
8
- ChannelLogSink as SDKChannelLogSink,
9
- ChannelAccountSnapshot as SDKChannelAccountSnapshot,
10
- ChannelGatewayContext as SDKChannelGatewayContext,
11
- ChannelPlugin as SDKChannelPlugin,
12
- PluginRuntime,
13
- } from "openclaw/plugin-sdk";
14
-
15
- // ─── 插件模块 ───────────────────────────────────────────────────────────────
16
-
17
- export interface XgImPluginModule {
18
- id: string;
19
- name: string;
20
- description?: string;
21
- configSchema?: unknown;
22
- register?: (api: OpenClawPluginApi) => void | Promise<void>;
23
- }
24
-
25
- /** 与 openclaw.plugin.json 中 id: xg_cwork_im 对应 */
26
- export type XgCworkImPluginModule = XgImPluginModule;
27
-
28
- // ─── Channel 配置 ────────────────────────────────────────────────────────────
29
-
30
- /** 单个机器人账户配置 */
31
- export interface XgImAccountConfig {
32
- /** 机器人 appKey,从 IM 后台注册获取 */
33
- appKey?: string;
34
- /** 对应 OpenClaw 的 Agent ID,默认为 'main' */
35
- agentId?: string;
36
- /** 账户显示名称 */
37
- name?: string;
38
- /** 群聊策略:open = 不需要 @,mention = 必须 @ 机器人才触发 */
39
- groupPolicy?: "open" | "mention";
40
- /** multipart 整文件上传字段名,默认 file(与 `/file/upDownload/uploadWholeFile` 约定一致时可不改) */
41
- fileUploadFormField?: string;
42
- maxAttachmentBytes?: number;
43
- /** 覆盖顶层的入站附件 workspace 子目录(相对 Agent workspace) */
44
- inboundMediaWorkspaceSubdir?: string;
45
- }
46
-
47
- export interface XgImConfig extends OpenClawConfig {
48
- /** 多账户列表 */
49
- accounts?: Record<string, XgImAccountConfig>;
50
-
51
- /** 机器人 appKey(单账户模式) */
52
- appKey?: string;
53
- /** 对应 OpenClaw Agent ID(单账户模式) */
54
- agentId?: string;
55
- /** IM 服务域名,如 https://test.xgjktech.com.cn */
56
- baseUrl: string;
57
- /** WebSocket 服务域名,如 wss://test.xgjktech.com.cn */
58
- wsBaseUrl?: string;
59
- /** 是否启用 */
60
- enabled?: boolean;
61
- /** 账户显示名称(单账户模式使用) */
62
- name?: string;
63
- /** 群聊策略:open = 不需要 @,mention = 必须 @ 机器人才触发 */
64
- groupPolicy?: "open" | "mention";
65
- /** 允许的发送者 userId 白名单(空表示全部允许) */
66
- allowFrom?: string[];
67
- /** 是否开启调试日志 */
68
- debug?: boolean;
69
- /** 最大重连次数(默认 10) */
70
- maxConnectionAttempts?: number;
71
- /** 初始重连延迟 ms(默认 1000) */
72
- initialReconnectDelay?: number;
73
- /** 最大重连延迟 ms(默认 60000) */
74
- maxReconnectDelay?: number;
75
- /** 重连延迟抖动因子 0-1(默认 0.3) */
76
- reconnectJitter?: number;
77
- /**
78
- * 首次 AI 回复超时时间(毫秒)。
79
- *
80
- * - 未配置时默认 30 分钟(1800_000ms)。
81
- * - 仅用于保护「思考中」占位消息,避免长时间不被更新。
82
- */
83
- firstReplyTimeoutMs?: number;
84
-
85
- /**
86
- * 附件上传地址固定为 `{baseUrl}/file/upDownload/uploadWholeFile`,请求头 `access-token` 与发 IM 相同。
87
- * multipart 字段名默认 `file`,可通过 fileUploadFormField 覆盖。
88
- */
89
- fileUploadFormField?: string;
90
- /** 单个附件下载/上传允许的最大字节数,默认 50MB */
91
- maxAttachmentBytes?: number;
92
-
93
- /**
94
- * 若配置为非空相对路径(相对于当前 Agent workspace 根目录):入站附件会先下载到
95
- * `{workspace}/{本字段}/{userId}/{YYYY-MM-DD}/{name}_{HHmmssmmm}.ext`,再向 OpenClaw 传入本地 `file://` 路径。
96
- * 未配置或为空则仍使用 IM 返回的远程 URL。
97
- */
98
- inboundMediaWorkspaceSubdir?: string;
99
- }
100
-
101
- // ─── IM 接口 Request / Response ─────────────────────────────────────────────
102
-
103
- /** GET /user/login/appkey 的响应 */
104
- export interface GetTokenResponse {
105
- data?: {
106
- xgToken: string;
107
- empId: string;
108
- userName?: string;
109
- avatar?: string;
110
- corpId?: string;
111
- deptList?: unknown[];
112
- appCode?: string;
113
- telephone?: string;
114
- personId?: string;
115
- };
116
- resultCode?: number;
117
- resultMsg?: string | null;
118
- }
119
-
120
- /** 机器人身份信息(认证成功后缓存) */
121
- export interface BotIdentity {
122
- token: string;
123
- userId: string;
124
- name: string;
125
- }
126
-
127
- // ─── WebSocket 消息 ──────────────────────────────────────────────────────────
128
-
129
- /** WebSocket 消息通知(cmd = robotMention) */
130
- export interface WsMessage {
131
- cmd: string;
132
- params: WsMessageParams;
133
- ts: number;
134
- }
135
-
136
- /**
137
- * 与 IM 服务 `MsgFileVO` 对齐的附件项(WebSocket `msgContent.files[]`)。
138
- */
139
- export interface MsgFileVO {
140
- /** 文件下载链接,短期有效 */
141
- url: string;
142
- /** 文件格式,小写:pdf、png、docx、doc、ppt、txt、md 等 */
143
- format?: string;
144
- /** 文件 ID;若来自七牛等无业务 id 的场景可为空 */
145
- fileId?: string;
146
- /** 文件大小(字节) */
147
- size?: number;
148
- /** 文件名称,如 xxx.pdf */
149
- name?: string;
150
- /**
151
- * IM 侧已解析的正文(如图片 OCR/说明、文档抽取的文本等)。
152
- * 非空时插件会拼入入站 `Body`/`RawBody`,一并交给 OpenClaw。
153
- */
154
- content?: string;
155
- }
156
-
157
- /** @deprecated 使用 {@link MsgFileVO} */
158
- export type WsInboundFileItem = MsgFileVO;
159
-
160
- /** 常见 IM format → MIME,供 OpenClaw 媒体理解;未知格式返回 undefined */
161
- export function imFormatToMimeType(format: string | undefined): string | undefined {
162
- const f = format?.trim().toLowerCase();
163
- if (!f) return undefined;
164
- const map: Record<string, string> = {
165
- pdf: "application/pdf",
166
- png: "image/png",
167
- jpg: "image/jpeg",
168
- jpeg: "image/jpeg",
169
- gif: "image/gif",
170
- webp: "image/webp",
171
- doc: "application/msword",
172
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
173
- ppt: "application/vnd.ms-powerpoint",
174
- pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
175
- xls: "application/vnd.ms-excel",
176
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
177
- txt: "text/plain",
178
- md: "text/markdown",
179
- csv: "text/csv",
180
- zip: "application/zip",
181
- mp3: "audio/mpeg",
182
- wav: "audio/wav",
183
- mp4: "video/mp4",
184
- };
185
- return map[f];
186
- }
187
-
188
- /**
189
- * 入站消息内容体(与 IM WebSocket `params.msgContent` 对齐)。
190
- *
191
- * - **type=`text`**:纯文本;**text** 建议非空。
192
- * - **type=`file`**:带 1..N 个文件,**files** 建议至少 1 项;**text** 可为配文(同时上传多文件 + 一句话)。**files[].content** 可由 IM 填已解析文本(如图/文档),插件会拼入入站正文。
193
- * - 语音在 IM 侧已转写为文字时,应以 **type=`text`** 推送转写结果,不再使用 voice。
194
- */
195
- export interface WsMessageContent {
196
- /** 文本正文;纯文本消息为主内容;file 消息上为可选配文 */
197
- text?: string;
198
- /** `text` | `file`(可扩展其它枚举,插件对未知类型按文本兜底) */
199
- type: string;
200
- /** 多附件;type file 时由 IM 填至少一项 */
201
- files?: MsgFileVO[];
202
- ext?: Record<string, any>;
203
- }
204
-
205
- /** robotMention 消息的 params */
206
- export interface WsMessageParams {
207
- msgId: string;
208
- groupId: string;
209
- /** 发送人信息 */
210
- userInfo: {
211
- /** 发送人 ID */
212
- id: string;
213
- /** 发送人显示名 */
214
- name: string;
215
- /**
216
- * 用户背景信息。
217
- * - 若非空,需要透传给 AI(可能是一段 JSON 字符串)。
218
- */
219
- background?: string;
220
- };
221
- msgContent: WsMessageContent;
222
- /** 服务端可能返回 msgSendTime 或 timestamp */
223
- msgSendTime?: number;
224
- timestamp?: number;
225
- /** 被 @ 的人员 ID 列表 */
226
- mentions?: string[] | null;
227
- }
228
-
229
- // ─── IM 发送消息 ─────────────────────────────────────────────────────────────
230
-
231
- /** POST /im/message/send 请求体 */
232
- export interface SendMessageReply {
233
- /** 被回复消息ID */
234
- targetMsgId: string;
235
- /** 被回复消息发送者ID */
236
- targetUserId: string;
237
- /** 被回复消息发送者显示名 */
238
- targetUserName: string;
239
- /** 被回复消息摘要 */
240
- previewText: string;
241
- }
242
-
243
- /** IM 发送 FILE 类型消息时的附件项(与发消息接口约定一致) */
244
- export interface SendMessageFileAttachment {
245
- fileId: string;
246
- /** 固定为 FILE */
247
- fileType: "FILE";
248
- /** 小写扩展名,如 md、pdf */
249
- format: string;
250
- name: string;
251
- size: number;
252
- /** 固定为 resource */
253
- source: "resource";
254
- }
255
-
256
- export type SendMessageBody =
257
- | {
258
- type: "TEXT" | "RICH_TEXT" | "VOICE";
259
- groupId?: string;
260
- toUserId?: string;
261
- text: string;
262
- atUserIds?: string[];
263
- msgId?: string;
264
- reply?: SendMessageReply;
265
- }
266
- | {
267
- type: "FILE";
268
- groupId?: string;
269
- toUserId?: string;
270
- text: string;
271
- attachments: SendMessageFileAttachment[];
272
- atUserIds?: string[];
273
- msgId?: string;
274
- reply?: SendMessageReply;
275
- };
276
-
277
- /** GET /im/message/getLatestMsgListForAI 响应 */
278
- export interface GetLatestMsgListResponse {
279
- data?: WsMessageParams[];
280
- resultCode?: number;
281
- message?: string;
282
- }
283
-
284
- // ─── OpenClaw 插件 SDK 类型别名 ───────────────────────────────────────────────
285
-
286
- export type ChannelLogSink = SDKChannelLogSink;
287
- export type ChannelAccountSnapshot = SDKChannelAccountSnapshot;
288
-
289
- export interface ResolvedAccount {
290
- accountId: string;
291
- config: XgImConfig;
292
- enabled: boolean;
293
- configured: boolean;
294
- name?: string | null;
295
- }
296
-
297
- export type GatewayStartContext = SDKChannelGatewayContext<ResolvedAccount>;
298
- export type XgImChannelPlugin = SDKChannelPlugin<ResolvedAccount & { configured: boolean }>;
299
-
300
- /** PluginRuntime 的类型(内部 API 丰富,用 any 表示) */
301
- export type { PluginRuntime };
1
+ /**
2
+ * XG-IM Channel Plugin — 类型定义
3
+ */
4
+
5
+ import type {
6
+ OpenClawConfig,
7
+ OpenClawPluginApi,
8
+ ChannelLogSink as SDKChannelLogSink,
9
+ ChannelAccountSnapshot as SDKChannelAccountSnapshot,
10
+ ChannelGatewayContext as SDKChannelGatewayContext,
11
+ ChannelPlugin as SDKChannelPlugin,
12
+ PluginRuntime,
13
+ } from "openclaw/plugin-sdk";
14
+
15
+ // ─── 插件模块 ───────────────────────────────────────────────────────────────
16
+
17
+ export interface XgImPluginModule {
18
+ id: string;
19
+ name: string;
20
+ description?: string;
21
+ configSchema?: unknown;
22
+ register?: (api: OpenClawPluginApi) => void | Promise<void>;
23
+ }
24
+
25
+ /** 与 openclaw.plugin.json 中 id: xg_cwork_im 对应 */
26
+ export type XgCworkImPluginModule = XgImPluginModule;
27
+
28
+ // ─── Channel 配置 ────────────────────────────────────────────────────────────
29
+
30
+ /** 单个机器人账户配置 */
31
+ export interface XgImAccountConfig {
32
+ /** 机器人 appKey,从 IM 后台注册获取 */
33
+ appKey?: string;
34
+ /** 对应 OpenClaw 的 Agent ID,默认为 'main' */
35
+ agentId?: string;
36
+ /** 账户显示名称 */
37
+ name?: string;
38
+ /** 群聊策略:open = 不需要 @,mention = 必须 @ 机器人才触发 */
39
+ groupPolicy?: "open" | "mention";
40
+ /** multipart 整文件上传字段名,默认 file(与 `/file/upDownload/uploadWholeFile` 约定一致时可不改) */
41
+ fileUploadFormField?: string;
42
+ maxAttachmentBytes?: number;
43
+ /** 覆盖顶层的入站附件 workspace 子目录(相对 Agent workspace) */
44
+ inboundMediaWorkspaceSubdir?: string;
45
+ /**
46
+ * 覆盖顶层:入站落盘后交给 OpenClaw 的 `MediaPath` 形式。
47
+ * - `workspaceRelative`(默认):`./xg_im_inbound/...`,沙箱内 `/workspace` 可解析。
48
+ * - `absoluteFileUrl`:宿主机 `file://...`,仅适合无沙箱、同进程读盘场景。
49
+ */
50
+ inboundMediaOpenClawPath?: "workspaceRelative" | "absoluteFileUrl";
51
+ }
52
+
53
+ export interface XgImConfig extends OpenClawConfig {
54
+ /** 多账户列表 */
55
+ accounts?: Record<string, XgImAccountConfig>;
56
+
57
+ /** 机器人 appKey(单账户模式) */
58
+ appKey?: string;
59
+ /** 对应 OpenClaw 的 Agent ID(单账户模式) */
60
+ agentId?: string;
61
+ /** IM 服务域名,如 https://test.xgjktech.com.cn */
62
+ baseUrl: string;
63
+ /** WebSocket 服务域名,如 wss://test.xgjktech.com.cn */
64
+ wsBaseUrl?: string;
65
+ /** 是否启用 */
66
+ enabled?: boolean;
67
+ /** 账户显示名称(单账户模式使用) */
68
+ name?: string;
69
+ /** 群聊策略:open = 不需要 @,mention = 必须 @ 机器人才触发 */
70
+ groupPolicy?: "open" | "mention";
71
+ /** 允许的发送者 userId 白名单(空表示全部允许) */
72
+ allowFrom?: string[];
73
+ /** 是否开启调试日志 */
74
+ debug?: boolean;
75
+ /** 最大重连次数(默认 10) */
76
+ maxConnectionAttempts?: number;
77
+ /** 初始重连延迟 ms(默认 1000) */
78
+ initialReconnectDelay?: number;
79
+ /** 最大重连延迟 ms(默认 60000) */
80
+ maxReconnectDelay?: number;
81
+ /** 重连延迟抖动因子 0-1(默认 0.3) */
82
+ reconnectJitter?: number;
83
+ /**
84
+ * 首次 AI 回复超时时间(毫秒)。
85
+ *
86
+ * - 未配置时默认 30 分钟(1800_000ms)。
87
+ * - 仅用于保护「思考中」占位消息,避免长时间不被更新。
88
+ */
89
+ firstReplyTimeoutMs?: number;
90
+
91
+ /**
92
+ * 附件上传地址固定为 `{baseUrl}/file/upDownload/uploadWholeFile`,请求头 `access-token` 与发 IM 相同。
93
+ * multipart 字段名默认 `file`,可通过 fileUploadFormField 覆盖。
94
+ */
95
+ fileUploadFormField?: string;
96
+ /** 单个附件下载/上传允许的最大字节数,默认 50MB */
97
+ maxAttachmentBytes?: number;
98
+
99
+ /**
100
+ * 若配置为非空相对路径(相对于当前 Agent workspace 根目录):入站附件会先下载到
101
+ * `{workspace}/{本字段}/{userId}/{YYYY-MM-DD}/{name}_{HHmmssmmm}.ext`,再向 OpenClaw 传入 **workspace 相对路径**(默认 `./子目录/...`,便于沙箱挂载 `/workspace`)或 `file://`(见 `inboundMediaOpenClawPath`)。
102
+ * 未配置或为空则仍使用 IM 返回的远程 URL。
103
+ */
104
+ inboundMediaWorkspaceSubdir?: string;
105
+ /**
106
+ * 入站落盘后写入 `MediaPath` 的引用形式;默认与 `workspaceRelative` 等价(未填时)。
107
+ * Agent 在沙箱中运行时应用 `workspaceRelative`,避免宿主机绝对 `file://` 在沙箱内不可读。
108
+ */
109
+ inboundMediaOpenClawPath?: "workspaceRelative" | "absoluteFileUrl";
110
+ }
111
+
112
+ // ─── IM 接口 Request / Response ─────────────────────────────────────────────
113
+
114
+ /** GET /user/login/appkey 的响应 */
115
+ export interface GetTokenResponse {
116
+ data?: {
117
+ xgToken: string;
118
+ empId: string;
119
+ userName?: string;
120
+ avatar?: string;
121
+ corpId?: string;
122
+ deptList?: unknown[];
123
+ appCode?: string;
124
+ telephone?: string;
125
+ personId?: string;
126
+ };
127
+ resultCode?: number;
128
+ resultMsg?: string | null;
129
+ }
130
+
131
+ /** 机器人身份信息(认证成功后缓存) */
132
+ export interface BotIdentity {
133
+ token: string;
134
+ userId: string;
135
+ name: string;
136
+ }
137
+
138
+ // ─── WebSocket 消息 ──────────────────────────────────────────────────────────
139
+
140
+ /** WebSocket 消息通知(cmd = robotMention) */
141
+ export interface WsMessage {
142
+ cmd: string;
143
+ params: WsMessageParams;
144
+ ts: number;
145
+ }
146
+
147
+ /**
148
+ * 与 IM 服务 `MsgFileVO` 对齐的附件项(WebSocket `msgContent.files[]`)。
149
+ */
150
+ export interface MsgFileVO {
151
+ /** 远程下载链接,或入站落盘后的 `file://` / workspace 相对引用(如 `./xg_im_inbound/...`) */
152
+ url: string;
153
+ /** 文件格式,小写:pdf、png、docx、doc、ppt、txt、md 等 */
154
+ format?: string;
155
+ /** 文件 ID;若来自七牛等无业务 id 的场景可为空 */
156
+ fileId?: string;
157
+ /** 文件大小(字节) */
158
+ size?: number;
159
+ /** 文件名称,如 xxx.pdf */
160
+ name?: string;
161
+ /**
162
+ * IM 侧已解析的正文(如图片 OCR/说明、文档抽取的文本等)。
163
+ * 非空时插件会拼入入站 `Body`/`RawBody`,一并交给 OpenClaw。
164
+ */
165
+ content?: string;
166
+ }
167
+
168
+ /** @deprecated 使用 {@link MsgFileVO} */
169
+ export type WsInboundFileItem = MsgFileVO;
170
+
171
+ /** 常见 IM format → MIME,供 OpenClaw 媒体理解;未知格式返回 undefined */
172
+ export function imFormatToMimeType(format: string | undefined): string | undefined {
173
+ const f = format?.trim().toLowerCase();
174
+ if (!f) return undefined;
175
+ const map: Record<string, string> = {
176
+ pdf: "application/pdf",
177
+ png: "image/png",
178
+ jpg: "image/jpeg",
179
+ jpeg: "image/jpeg",
180
+ gif: "image/gif",
181
+ webp: "image/webp",
182
+ doc: "application/msword",
183
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
184
+ ppt: "application/vnd.ms-powerpoint",
185
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
186
+ xls: "application/vnd.ms-excel",
187
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
188
+ txt: "text/plain",
189
+ md: "text/markdown",
190
+ csv: "text/csv",
191
+ zip: "application/zip",
192
+ mp3: "audio/mpeg",
193
+ wav: "audio/wav",
194
+ mp4: "video/mp4",
195
+ };
196
+ return map[f];
197
+ }
198
+
199
+ /**
200
+ * 入站消息内容体(与 IM WebSocket `params.msgContent` 对齐)。
201
+ *
202
+ * - **type=`text`**:纯文本;**text** 建议非空。
203
+ * - **type=`file`**:带 1..N 个文件,**files** 建议至少 1 项;**text** 可为配文(同时上传多文件 + 一句话)。**files[].content** 可由 IM 填已解析文本(如图/文档),插件会拼入入站正文。
204
+ * - 语音在 IM 侧已转写为文字时,应以 **type=`text`** 推送转写结果,不再使用 voice。
205
+ */
206
+ export interface WsMessageContent {
207
+ /** 文本正文;纯文本消息为主内容;file 消息上为可选配文 */
208
+ text?: string;
209
+ /** `text` | `file`(可扩展其它枚举,插件对未知类型按文本兜底) */
210
+ type: string;
211
+ /** 多附件;type file 时由 IM 填至少一项 */
212
+ files?: MsgFileVO[];
213
+ ext?: Record<string, any>;
214
+ }
215
+
216
+ /** robotMention 消息的 params */
217
+ export interface WsMessageParams {
218
+ msgId: string;
219
+ groupId: string;
220
+ /** 发送人信息 */
221
+ userInfo: {
222
+ /** 发送人 ID */
223
+ id: string;
224
+ /** 发送人显示名 */
225
+ name: string;
226
+ /**
227
+ * 用户背景信息。
228
+ * - 若非空,需要透传给 AI(可能是一段 JSON 字符串)。
229
+ */
230
+ background?: string;
231
+ };
232
+ msgContent: WsMessageContent;
233
+ /** 服务端可能返回 msgSendTime 或 timestamp */
234
+ msgSendTime?: number;
235
+ timestamp?: number;
236
+ /** 被 @ 的人员 ID 列表 */
237
+ mentions?: string[] | null;
238
+ }
239
+
240
+ // ─── IM 发送消息 ─────────────────────────────────────────────────────────────
241
+
242
+ /** POST /im/message/send 请求体 */
243
+ export interface SendMessageReply {
244
+ /** 被回复消息ID */
245
+ targetMsgId: string;
246
+ /** 被回复消息发送者ID */
247
+ targetUserId: string;
248
+ /** 被回复消息发送者显示名 */
249
+ targetUserName: string;
250
+ /** 被回复消息摘要 */
251
+ previewText: string;
252
+ }
253
+
254
+ /** IM 发送 FILE 类型消息时的附件项(与发消息接口约定一致) */
255
+ export interface SendMessageFileAttachment {
256
+ fileId: string;
257
+ /** 固定为 FILE */
258
+ fileType: "FILE";
259
+ /** 小写扩展名,如 md、pdf */
260
+ format: string;
261
+ name: string;
262
+ size: number;
263
+ /** 固定为 resource */
264
+ source: "resource";
265
+ }
266
+
267
+ export type SendMessageBody =
268
+ | {
269
+ type: "TEXT" | "RICH_TEXT" | "VOICE";
270
+ groupId?: string;
271
+ toUserId?: string;
272
+ text: string;
273
+ atUserIds?: string[];
274
+ msgId?: string;
275
+ reply?: SendMessageReply;
276
+ }
277
+ | {
278
+ type: "FILE";
279
+ groupId?: string;
280
+ toUserId?: string;
281
+ text: string;
282
+ attachments: SendMessageFileAttachment[];
283
+ atUserIds?: string[];
284
+ msgId?: string;
285
+ reply?: SendMessageReply;
286
+ };
287
+
288
+ /** GET /im/message/getLatestMsgListForAI 响应 */
289
+ export interface GetLatestMsgListResponse {
290
+ data?: WsMessageParams[];
291
+ resultCode?: number;
292
+ message?: string;
293
+ }
294
+
295
+ // ─── OpenClaw 插件 SDK 类型别名 ───────────────────────────────────────────────
296
+
297
+ export type ChannelLogSink = SDKChannelLogSink;
298
+ export type ChannelAccountSnapshot = SDKChannelAccountSnapshot;
299
+
300
+ export interface ResolvedAccount {
301
+ accountId: string;
302
+ config: XgImConfig;
303
+ enabled: boolean;
304
+ configured: boolean;
305
+ name?: string | null;
306
+ }
307
+
308
+ export type GatewayStartContext = SDKChannelGatewayContext<ResolvedAccount>;
309
+ export type XgImChannelPlugin = SDKChannelPlugin<ResolvedAccount & { configured: boolean }>;
310
+
311
+ /** PluginRuntime 的类型(内部 API 丰富,用 any 表示) */
312
+ export type { PluginRuntime };