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