@xgjktech/xg_cwork_im 1.11.1 → 1.11.4
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 +25 -18
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/{index.ts → dist/index.js} +33 -37
- package/dist/index.js.map +1 -0
- package/dist/src/auth.d.ts +25 -0
- package/dist/src/auth.d.ts.map +1 -0
- package/{src/auth.ts → dist/src/auth.js} +55 -79
- package/dist/src/auth.js.map +1 -0
- package/dist/src/channel.d.ts +13 -0
- package/dist/src/channel.d.ts.map +1 -0
- package/{src/channel.ts → dist/src/channel.js} +224 -368
- package/dist/src/channel.js.map +1 -0
- package/dist/src/connection.d.ts +51 -0
- package/dist/src/connection.d.ts.map +1 -0
- package/{src/connection.ts → dist/src/connection.js} +45 -122
- package/dist/src/connection.js.map +1 -0
- package/dist/src/group-history-tool.d.ts +23 -0
- package/dist/src/group-history-tool.d.ts.map +1 -0
- package/{src/group-history-tool.ts → dist/src/group-history-tool.js} +168 -204
- package/dist/src/group-history-tool.js.map +1 -0
- package/dist/src/inbound-media-local.d.ts +40 -0
- package/dist/src/inbound-media-local.d.ts.map +1 -0
- package/{src/inbound-media-local.ts → dist/src/inbound-media-local.js} +52 -90
- package/dist/src/inbound-media-local.js.map +1 -0
- package/dist/src/recommended-system-prompt.d.ts +3 -0
- package/dist/src/recommended-system-prompt.d.ts.map +1 -0
- package/dist/src/recommended-system-prompt.js +3 -0
- package/dist/src/recommended-system-prompt.js.map +1 -0
- package/dist/src/resource-file.d.ts +65 -0
- package/dist/src/resource-file.d.ts.map +1 -0
- package/{src/resource-file.ts → dist/src/resource-file.js} +317 -339
- package/dist/src/resource-file.js.map +1 -0
- package/dist/src/send-group-message-tool.d.ts +16 -0
- package/dist/src/send-group-message-tool.d.ts.map +1 -0
- package/{src/send-group-message-tool.ts → dist/src/send-group-message-tool.js} +17 -34
- package/dist/src/send-group-message-tool.js.map +1 -0
- package/dist/src/send-service.d.ts +34 -0
- package/dist/src/send-service.d.ts.map +1 -0
- package/dist/src/send-service.js +197 -0
- package/dist/src/send-service.js.map +1 -0
- package/{src/tool-json-result.ts → dist/src/tool-json-result.d.ts} +14 -24
- package/dist/src/tool-json-result.d.ts.map +1 -0
- package/dist/src/tool-json-result.js +20 -0
- package/dist/src/tool-json-result.js.map +1 -0
- package/{src/types.ts → dist/src/types.d.ts} +32 -96
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +32 -0
- package/dist/src/types.js.map +1 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +7 -6
- package/src/recommended-system-prompt.ts +0 -3
- package/src/send-service.ts +0 -228
|
@@ -1,339 +1,317 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 资源中心整文件上传 + OpenClaw deliver 媒体 URL 拉取
|
|
3
|
-
*
|
|
4
|
-
* 上传地址固定为 `baseUrl + /file/upDownload/uploadWholeFile`,与 IM 同域,使用与发消息相同的 access-token。
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
return "bin";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!/%[0-9A-Fa-f]{2}/.test(s))
|
|
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
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return "attachment";
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
*
|
|
130
|
-
* -
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// Linux/macOS 上 WSL 互操作常见:/mnt/c/... 已是 posix 绝对路径,上面已命中
|
|
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
|
-
log?.error(`[cwork_im:media]
|
|
221
|
-
throw
|
|
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
|
-
log?.
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// Unified IM API response: { data, resultCode: 1, resultMsg: "" } — resultCode=1 means success.
|
|
319
|
-
if (json && typeof json === "object") {
|
|
320
|
-
const j = json as Record<string, unknown>;
|
|
321
|
-
if (j.resultCode !== undefined && j.resultCode !== 1) {
|
|
322
|
-
const msg = `upload IM API error resultCode=${j.resultCode}${j.resultMsg ? ` resultMsg=${j.resultMsg}` : ""}`;
|
|
323
|
-
log?.error(`[cwork_im:upload] ${msg} body=${rawText.slice(0, 500)}`);
|
|
324
|
-
throw new Error(msg);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const fileId = extractFileIdFromUploadResponse(json ?? rawText);
|
|
329
|
-
if (!fileId) {
|
|
330
|
-
log?.error(`[cwork_im:upload] missing fileId in body=${rawText.slice(0, 500)}`);
|
|
331
|
-
throw new Error(`upload response missing fileId: ${rawText.slice(0, 500)}`);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return { fileId, size: buffer.length };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export function resolveMaxAttachmentBytes(config: { maxAttachmentBytes?: number }): number {
|
|
338
|
-
return config.maxAttachmentBytes ?? DEFAULT_MAX_BYTES;
|
|
339
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 资源中心整文件上传 + OpenClaw deliver 媒体 URL 拉取
|
|
3
|
+
*
|
|
4
|
+
* 上传地址固定为 `baseUrl + /file/upDownload/uploadWholeFile`,与 IM 同域,使用与发消息相同的 access-token。
|
|
5
|
+
*/
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
import { File } from "node:buffer";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
12
|
+
/** 整文件上传接口路径(拼在 channels.xg_cwork_im.baseUrl 上) */
|
|
13
|
+
export const RESOURCE_UPLOAD_PATH = "/file/upDownload/uploadWholeFile";
|
|
14
|
+
export function resolveResourceUploadUrl(baseUrl) {
|
|
15
|
+
const b = baseUrl.trim();
|
|
16
|
+
if (!b)
|
|
17
|
+
throw new Error("[cwork_im] baseUrl is required for resource upload");
|
|
18
|
+
return new URL(RESOURCE_UPLOAD_PATH, b.endsWith("/") ? b : `${b}/`).href;
|
|
19
|
+
}
|
|
20
|
+
/** 从上传接口 JSON 中解析 fileId(兼容常见嵌套 data) */
|
|
21
|
+
export function extractFileIdFromUploadResponse(data) {
|
|
22
|
+
if (data == null)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (typeof data === "string" && data.trim().length > 0)
|
|
25
|
+
return data.trim();
|
|
26
|
+
if (typeof data !== "object")
|
|
27
|
+
return undefined;
|
|
28
|
+
const o = data;
|
|
29
|
+
for (const key of ["fileId", "id", "resourceId", "file_id"]) {
|
|
30
|
+
const v = o[key];
|
|
31
|
+
if (typeof v === "string" && v.length > 0)
|
|
32
|
+
return v;
|
|
33
|
+
}
|
|
34
|
+
if (o.data != null) {
|
|
35
|
+
const nested = extractFileIdFromUploadResponse(o.data);
|
|
36
|
+
if (nested)
|
|
37
|
+
return nested;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
export function formatFromFilename(filename) {
|
|
42
|
+
const ext = path.extname(filename).replace(/^\./, "").toLowerCase();
|
|
43
|
+
if (ext && ext.length <= 16 && /^[a-z0-9]+$/i.test(ext))
|
|
44
|
+
return ext;
|
|
45
|
+
return "bin";
|
|
46
|
+
}
|
|
47
|
+
/** 仅当含 %XX 序列时 decodeURIComponent,避免对已解码的 Unicode 再 decode 抛错 */
|
|
48
|
+
export function safeDecodeUriFilenameSegment(segment) {
|
|
49
|
+
const s = segment.trim();
|
|
50
|
+
if (!s)
|
|
51
|
+
return s;
|
|
52
|
+
if (!/%[0-9A-Fa-f]{2}/.test(s))
|
|
53
|
+
return s;
|
|
54
|
+
try {
|
|
55
|
+
return decodeURIComponent(s.replace(/\+/g, " "));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return s;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 从 HTTP Content-Disposition 解析文件名(优先 RFC 5987 `filename*=` UTF-8,再普通 filename=)。
|
|
63
|
+
*/
|
|
64
|
+
export function parseFilenameFromContentDisposition(header) {
|
|
65
|
+
if (header == null || typeof header !== "string")
|
|
66
|
+
return undefined;
|
|
67
|
+
const h = header.trim();
|
|
68
|
+
if (!h)
|
|
69
|
+
return undefined;
|
|
70
|
+
const star = /filename\*\s*=\s*(?:UTF-8|utf-8)''([^;\r\n]+)/i.exec(h);
|
|
71
|
+
if (star?.[1]) {
|
|
72
|
+
const v = star[1].trim();
|
|
73
|
+
const decoded = safeDecodeUriFilenameSegment(v);
|
|
74
|
+
if (decoded)
|
|
75
|
+
return decoded;
|
|
76
|
+
}
|
|
77
|
+
const quoted = /filename\s*=\s*"((?:[^"\\]|\\.)*)"/i.exec(h);
|
|
78
|
+
if (quoted?.[1]) {
|
|
79
|
+
const inner = quoted[1].replace(/\\(.)/g, "$1").trim();
|
|
80
|
+
if (inner)
|
|
81
|
+
return safeDecodeUriFilenameSegment(inner);
|
|
82
|
+
}
|
|
83
|
+
const unquoted = /filename\s*=\s*([^;\r\n]+)/i.exec(h);
|
|
84
|
+
if (unquoted?.[1]) {
|
|
85
|
+
let v = unquoted[1].trim().replace(/^["']|["']$/g, "");
|
|
86
|
+
if (v.toLowerCase().startsWith("utf-8''"))
|
|
87
|
+
v = v.slice(7);
|
|
88
|
+
if (v)
|
|
89
|
+
return safeDecodeUriFilenameSegment(v);
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
export function displayNameFromUrl(url) {
|
|
94
|
+
try {
|
|
95
|
+
if (url.startsWith("file://")) {
|
|
96
|
+
return path.basename(fileURLToPath(url)) || "file";
|
|
97
|
+
}
|
|
98
|
+
const u = new URL(url);
|
|
99
|
+
const qName = u.searchParams.get("filename") ?? u.searchParams.get("name");
|
|
100
|
+
if (qName?.trim()) {
|
|
101
|
+
return safeDecodeUriFilenameSegment(qName.trim()) || "attachment";
|
|
102
|
+
}
|
|
103
|
+
const pathname = u.pathname || "/";
|
|
104
|
+
const last = pathname.split("/").filter(Boolean).pop() ?? "";
|
|
105
|
+
const base = safeDecodeUriFilenameSegment(last.split("?")[0] || "");
|
|
106
|
+
return base || "attachment";
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return "attachment";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** 日志用:截断过长路径/URL */
|
|
113
|
+
export function summarizeMediaRef(ref, maxLen = 160) {
|
|
114
|
+
const t = ref.trim().replace(/\s+/g, " ");
|
|
115
|
+
if (t.length <= maxLen)
|
|
116
|
+
return t;
|
|
117
|
+
return `${t.slice(0, maxLen)}…(len=${t.length})`;
|
|
118
|
+
}
|
|
119
|
+
function isHttpOrHttps(ref) {
|
|
120
|
+
try {
|
|
121
|
+
const u = new URL(ref.trim());
|
|
122
|
+
return u.protocol === "http:" || u.protocol === "https:";
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 将字符串解析为可交给 readFile 的本机路径(兼容 Windows / macOS / Linux)。
|
|
130
|
+
* - 使用当前进程的 path.isAbsolute(各 OS 语义正确)。
|
|
131
|
+
* - Windows:Git Bash / MSYS 的 `/c/Users/...`、Cygwin `/cygdrive/c/...`。
|
|
132
|
+
* - 非 Windows:若传入 `D:\...` 形式仍做一次规范化(多用于日志/失败提示,读文件通常会失败)。
|
|
133
|
+
*/
|
|
134
|
+
export function resolveLocalFsPath(ref) {
|
|
135
|
+
const raw = ref.trim();
|
|
136
|
+
if (!raw)
|
|
137
|
+
return null;
|
|
138
|
+
if (process.platform === "win32") {
|
|
139
|
+
// MSYS/Git Bash: /c/Users/foo → C:\Users\foo(要求 /<盘符>/ 后仍有路径,避免误伤 /Users)
|
|
140
|
+
const msys = /^\/([a-zA-Z])\/(.+)$/.exec(raw.replace(/\\/g, "/"));
|
|
141
|
+
if (msys) {
|
|
142
|
+
const letter = msys[1].toUpperCase();
|
|
143
|
+
const rest = msys[2].split("/").join(path.win32.sep);
|
|
144
|
+
const fsPath = path.win32.normalize(`${letter}:${path.win32.sep}${rest}`);
|
|
145
|
+
return { fsPath, via: "git-bash-msys" };
|
|
146
|
+
}
|
|
147
|
+
const cyg = /^\/cygdrive\/([a-zA-Z])\/(.+)$/i.exec(raw.replace(/\\/g, "/"));
|
|
148
|
+
if (cyg) {
|
|
149
|
+
const letter = cyg[1].toUpperCase();
|
|
150
|
+
const rest = cyg[2].split("/").join(path.win32.sep);
|
|
151
|
+
const fsPath = path.win32.normalize(`${letter}:${path.win32.sep}${rest}`);
|
|
152
|
+
return { fsPath, via: "cygwin" };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (path.isAbsolute(raw)) {
|
|
156
|
+
return { fsPath: path.normalize(raw), via: `native-${process.platform}` };
|
|
157
|
+
}
|
|
158
|
+
// Linux/macOS 上 WSL 互操作常见:/mnt/c/... 已是 posix 绝对路径,上面已命中
|
|
159
|
+
if (process.platform !== "win32" && /^[a-zA-Z]:[/\\]/.test(raw)) {
|
|
160
|
+
return { fsPath: path.win32.normalize(raw), via: "win-style-path" };
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
async function readLocalFsIntoBuffer(fsPath, maxBytes, log) {
|
|
165
|
+
const buffer = await readFile(fsPath);
|
|
166
|
+
if (buffer.length > maxBytes) {
|
|
167
|
+
throw new Error(`local file size ${buffer.length} exceeds maxAttachmentBytes=${maxBytes}`);
|
|
168
|
+
}
|
|
169
|
+
const filename = path.basename(fsPath) || "file";
|
|
170
|
+
log?.info(`[cwork_im:media] local ok bytes=${buffer.length} name=${filename}`);
|
|
171
|
+
return { buffer, filename };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 将 OpenClaw 约定的 `/workspace/<rel>` 路径解析为本机绝对路径(跨平台)。
|
|
175
|
+
*
|
|
176
|
+
* OpenClaw 在所有平台启动时均会 chdir 到 workspace 目录,因此
|
|
177
|
+
* `process.cwd()` 即为 workspace 根,/workspace/<rel> 等价于 cwd/<rel>:
|
|
178
|
+
* - Windows : C:\Users\..\.openclaw\workspace\<rel>
|
|
179
|
+
* - macOS : ~/.openclaw/workspace/<rel>
|
|
180
|
+
* - Linux容器: /workspace/<rel>(cwd=/workspace,path.join 结果不变)
|
|
181
|
+
*
|
|
182
|
+
* 返回 null 表示路径不符合 /workspace/ 格式。
|
|
183
|
+
*/
|
|
184
|
+
export function resolveWorkspacePrefixPath(raw) {
|
|
185
|
+
if (!raw.startsWith("/workspace/") && raw !== "/workspace")
|
|
186
|
+
return null;
|
|
187
|
+
const rel = raw.startsWith("/workspace/") ? raw.slice("/workspace/".length) : "";
|
|
188
|
+
const cwd = process.cwd();
|
|
189
|
+
return rel ? path.join(cwd, rel) : cwd;
|
|
190
|
+
}
|
|
191
|
+
export async function downloadMediaBuffer(url, maxBytes, log) {
|
|
192
|
+
const raw = url.trim();
|
|
193
|
+
log?.info(`[cwork_im:media] resolve ref=${summarizeMediaRef(raw)}`);
|
|
194
|
+
// Handle OpenClaw /workspace/ convention: map to actual workspace directory.
|
|
195
|
+
const workspaceResolved = resolveWorkspacePrefixPath(raw);
|
|
196
|
+
if (workspaceResolved && workspaceResolved !== raw) {
|
|
197
|
+
log?.info(`[cwork_im:media] /workspace/ -> ${summarizeMediaRef(workspaceResolved)}`);
|
|
198
|
+
try {
|
|
199
|
+
return await readLocalFsIntoBuffer(workspaceResolved, maxBytes, log);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
log?.error(`[cwork_im:media] readFile failed /workspace/ ${summarizeMediaRef(workspaceResolved)}: ${String(err)}`);
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (raw.startsWith("file://")) {
|
|
207
|
+
let fsPath;
|
|
208
|
+
try {
|
|
209
|
+
fsPath = fileURLToPath(raw);
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
log?.error(`[cwork_im:media] invalid file:// URL: ${summarizeMediaRef(raw)} ${String(e)}`);
|
|
213
|
+
throw e;
|
|
214
|
+
}
|
|
215
|
+
log?.info(`[cwork_im:media] read file:// -> ${summarizeMediaRef(fsPath)}`);
|
|
216
|
+
try {
|
|
217
|
+
return await readLocalFsIntoBuffer(fsPath, maxBytes, log);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
log?.error(`[cwork_im:media] readFile failed file:// ${summarizeMediaRef(fsPath)}: ${String(err)}`);
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (isHttpOrHttps(raw)) {
|
|
225
|
+
log?.info(`[cwork_im:media] GET ${summarizeMediaRef(raw)}`);
|
|
226
|
+
try {
|
|
227
|
+
const res = await axios.get(raw, {
|
|
228
|
+
responseType: "arraybuffer",
|
|
229
|
+
timeout: 120_000,
|
|
230
|
+
maxContentLength: maxBytes,
|
|
231
|
+
maxBodyLength: maxBytes,
|
|
232
|
+
validateStatus: (s) => s >= 200 && s < 300,
|
|
233
|
+
});
|
|
234
|
+
const buffer = Buffer.from(res.data);
|
|
235
|
+
let filename = displayNameFromUrl(raw);
|
|
236
|
+
const cdRaw = res.headers["content-disposition"];
|
|
237
|
+
const cd = Array.isArray(cdRaw) ? cdRaw[0] : cdRaw;
|
|
238
|
+
if (typeof cd === "string") {
|
|
239
|
+
const fromCd = parseFilenameFromContentDisposition(cd);
|
|
240
|
+
if (fromCd)
|
|
241
|
+
filename = fromCd;
|
|
242
|
+
}
|
|
243
|
+
log?.info(`[cwork_im:media] http ok bytes=${buffer.length} name=${filename}`);
|
|
244
|
+
return { buffer, filename };
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
const detail = axios.isAxiosError(err)
|
|
248
|
+
? `${err.message}${err.code ? ` axiosCode=${err.code}` : ""}${err.response?.status != null ? ` httpStatus=${err.response.status}` : ""}`
|
|
249
|
+
: String(err);
|
|
250
|
+
log?.error(`[cwork_im:media] http failed ref=${summarizeMediaRef(raw)}: ${detail}`);
|
|
251
|
+
throw err;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const resolved = resolveLocalFsPath(raw);
|
|
255
|
+
if (resolved) {
|
|
256
|
+
log?.info(`[cwork_im:media] local via=${resolved.via} -> ${summarizeMediaRef(resolved.fsPath)}`);
|
|
257
|
+
try {
|
|
258
|
+
return await readLocalFsIntoBuffer(resolved.fsPath, maxBytes, log);
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
log?.error(`[cwork_im:media] readFile failed ${summarizeMediaRef(resolved.fsPath)}: ${String(err)}`);
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const hint = "请使用 http(s)、file://(三系统通用),或本机绝对路径:Windows C:\\\\... / UNC;macOS/Linux /Users、/home、/mnt/c/...(WSL);Windows 下 Git Bash 可用 /c/...";
|
|
266
|
+
log?.error(`[cwork_im:media] unsupported ref=${summarizeMediaRef(raw)} (${hint})`);
|
|
267
|
+
throw new Error(`[cwork_im] 无法读取媒体(${hint}): ${summarizeMediaRef(raw)}`);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* multipart 上传整文件,返回 fileId 与字节数。
|
|
271
|
+
*/
|
|
272
|
+
export async function uploadWholeResourceFile(args) {
|
|
273
|
+
const { fileUploadUrl, token, buffer, filename, formField, log } = args;
|
|
274
|
+
const form = new FormData();
|
|
275
|
+
// 使用 File 携带 UTF-8 文件名,避免部分环境下 Blob+第三参在 multipart 里被错误编码导致服务端乱码
|
|
276
|
+
const file = new File([buffer], filename, { type: "application/octet-stream" });
|
|
277
|
+
form.append(formField, file);
|
|
278
|
+
log?.info(`[cwork_im:upload] POST ${fileUploadUrl} field=${formField} name=${filename} bytes=${buffer.length}`);
|
|
279
|
+
const res = await fetch(fileUploadUrl, {
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: {
|
|
282
|
+
"access-token": token,
|
|
283
|
+
},
|
|
284
|
+
body: form,
|
|
285
|
+
});
|
|
286
|
+
const rawText = await res.text();
|
|
287
|
+
let json;
|
|
288
|
+
try {
|
|
289
|
+
json = rawText ? JSON.parse(rawText) : undefined;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
json = undefined;
|
|
293
|
+
}
|
|
294
|
+
if (!res.ok) {
|
|
295
|
+
log?.error(`[cwork_im:upload] HTTP ${res.status} body=${rawText.slice(0, 500)}`);
|
|
296
|
+
throw new Error(`upload HTTP ${res.status}: ${rawText.slice(0, 500)}`);
|
|
297
|
+
}
|
|
298
|
+
// Unified IM API response: { data, resultCode: 1, resultMsg: "" } — resultCode=1 means success.
|
|
299
|
+
if (json && typeof json === "object") {
|
|
300
|
+
const j = json;
|
|
301
|
+
if (j.resultCode !== undefined && j.resultCode !== 1) {
|
|
302
|
+
const msg = `upload IM API error resultCode=${j.resultCode}${j.resultMsg ? ` resultMsg=${j.resultMsg}` : ""}`;
|
|
303
|
+
log?.error(`[cwork_im:upload] ${msg} body=${rawText.slice(0, 500)}`);
|
|
304
|
+
throw new Error(msg);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const fileId = extractFileIdFromUploadResponse(json ?? rawText);
|
|
308
|
+
if (!fileId) {
|
|
309
|
+
log?.error(`[cwork_im:upload] missing fileId in body=${rawText.slice(0, 500)}`);
|
|
310
|
+
throw new Error(`upload response missing fileId: ${rawText.slice(0, 500)}`);
|
|
311
|
+
}
|
|
312
|
+
return { fileId, size: buffer.length };
|
|
313
|
+
}
|
|
314
|
+
export function resolveMaxAttachmentBytes(config) {
|
|
315
|
+
return config.maxAttachmentBytes ?? DEFAULT_MAX_BYTES;
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=resource-file.js.map
|