@insta-dev01/intclaw 1.0.11 → 1.1.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.
Files changed (49) hide show
  1. package/LICENSE +1 -1
  2. package/README.en.md +424 -0
  3. package/README.md +365 -164
  4. package/index.ts +28 -0
  5. package/openclaw.plugin.json +12 -41
  6. package/package.json +69 -40
  7. package/src/channel.ts +557 -0
  8. package/src/config/accounts.ts +230 -0
  9. package/src/config/schema.ts +144 -0
  10. package/src/core/connection.ts +733 -0
  11. package/src/core/message-handler.ts +1268 -0
  12. package/src/core/provider.ts +106 -0
  13. package/src/core/state.ts +54 -0
  14. package/src/directory.ts +95 -0
  15. package/src/gateway-methods.ts +237 -0
  16. package/src/onboarding.ts +387 -0
  17. package/src/policy.ts +19 -0
  18. package/src/probe.ts +213 -0
  19. package/src/reply-dispatcher.ts +674 -0
  20. package/src/runtime.ts +7 -0
  21. package/src/sdk/helpers.ts +317 -0
  22. package/src/sdk/types.ts +515 -0
  23. package/src/secret-input.ts +19 -0
  24. package/src/services/media/audio.ts +54 -0
  25. package/src/services/media/chunk-upload.ts +293 -0
  26. package/src/services/media/common.ts +154 -0
  27. package/src/services/media/file.ts +70 -0
  28. package/src/services/media/image.ts +67 -0
  29. package/src/services/media/index.ts +10 -0
  30. package/src/services/media/video.ts +162 -0
  31. package/src/services/media.ts +1134 -0
  32. package/src/services/messaging/index.ts +16 -0
  33. package/src/services/messaging/send.ts +137 -0
  34. package/src/services/messaging.ts +800 -0
  35. package/src/targets.ts +45 -0
  36. package/src/types/index.ts +52 -0
  37. package/src/utils/agent.ts +63 -0
  38. package/src/utils/async.ts +51 -0
  39. package/src/utils/constants.ts +9 -0
  40. package/src/utils/http-client.ts +84 -0
  41. package/src/utils/index.ts +8 -0
  42. package/src/utils/logger.ts +78 -0
  43. package/src/utils/session.ts +118 -0
  44. package/src/utils/token.ts +94 -0
  45. package/src/utils/utils-legacy.ts +506 -0
  46. package/.env.example +0 -11
  47. package/skills/intclaw_matrix/SKILL.md +0 -20
  48. package/src/channel/intclaw_channel.js +0 -155
  49. package/src/index.js +0 -23
@@ -0,0 +1,293 @@
1
+ /**
2
+ * IntClaw文件分块上传模块
3
+ * 支持大文件(>20MB)的分块上传
4
+ *
5
+ * API 文档:
6
+ * - 开启事务:https://open.intclaw.com/document/development/enable-upload-transaction
7
+ * - 上传块:https://open.intclaw.com/document/development/upload-file-blocks
8
+ * - 提交事务:https://open.intclaw.com/document/development/submit-a-file-upload-transaction
9
+ */
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { createLogger } from '../../utils/logger.ts';
14
+ import { intclawOapiHttp, intclawUploadHttp } from '../../utils/http-client.ts';
15
+ import { INTCLAW_CONFIG } from '../../../config.ts';
16
+
17
+ const INTCLAW_OAPI = INTCLAW_CONFIG.OAPI_BASE_URL;
18
+
19
+ /** 分块上传配置 */
20
+ export const CHUNK_CONFIG = {
21
+ MIN_CHUNK_SIZE: 100 * 1024, // 最小分块 100KB
22
+ MAX_CHUNK_SIZE: 8 * 1024 * 1024, // 最大分块 8MB
23
+ DEFAULT_CHUNK_SIZE: 5 * 1024 * 1024, // 默认分块 5MB
24
+ SIZE_THRESHOLD: 20 * 1024 * 1024, // 超过 20MB 使用分块上传
25
+ };
26
+
27
+ /** 开启上传事务响应 */
28
+ interface UploadTransactionResponse {
29
+ errcode: number;
30
+ errmsg: string;
31
+ upload_id: string;
32
+ }
33
+
34
+ /** 上传文件块响应 */
35
+ interface UploadBlockResponse {
36
+ errcode: number;
37
+ errmsg: string;
38
+ }
39
+
40
+ /** 提交上传事务响应 */
41
+ interface SubmitTransactionResponse {
42
+ errcode: number;
43
+ errmsg: string;
44
+ file_id?: string;
45
+ download_code?: string;
46
+ }
47
+
48
+ /**
49
+ * 步骤一:开启分块上传事务
50
+ * @param oapiToken IntClaw access_token
51
+ * @param fileName 文件名
52
+ * @param fileSize 文件大小(字节)
53
+ * @param log 日志对象
54
+ */
55
+ export async function enableUploadTransaction(
56
+ oapiToken: string,
57
+ fileName: string,
58
+ fileSize: number,
59
+ debug: boolean = false,
60
+ ): Promise<string | null> {
61
+ const log = createLogger(debug, 'IntClaw][ChunkUpload');
62
+
63
+ try {
64
+ log.info(`开启上传事务:${fileName}, 大小:${(fileSize / 1024 / 1024).toFixed(2)}MB`);
65
+
66
+ const resp = await intclawOapiHttp.get<UploadTransactionResponse>(
67
+ `${INTCLAW_OAPI}/file/upload/transaction/enable`,
68
+ {
69
+ params: {
70
+ access_token: oapiToken,
71
+ file_name: fileName,
72
+ file_size: fileSize,
73
+ },
74
+ timeout: 30_000,
75
+ }
76
+ );
77
+
78
+ if (resp.data.errcode === 0) {
79
+ log.info(`事务开启成功,upload_id: ${resp.data.upload_id}`);
80
+ return resp.data.upload_id;
81
+ } else {
82
+ log.error(`开启事务失败:${resp.data.errmsg}`);
83
+ return null;
84
+ }
85
+ } catch (err: any) {
86
+ log.error(`开启事务异常:${err.message}`);
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 步骤二:上传文件块
93
+ * @param oapiToken IntClaw access_token
94
+ * @param uploadId 上传事务 ID
95
+ * @param chunkData 文件块数据
96
+ * @param chunkNumber 块编号(从 1 开始)
97
+ * @param totalChunks 总块数
98
+ * @param log 日志对象
99
+ */
100
+ export async function uploadFileBlock(
101
+ oapiToken: string,
102
+ uploadId: string,
103
+ chunkData: Buffer,
104
+ chunkNumber: number,
105
+ totalChunks: number,
106
+ debug: boolean = false,
107
+ ): Promise<boolean> {
108
+ const log = createLogger(debug, 'IntClaw][ChunkUpload');
109
+
110
+ try {
111
+ log.info(`上传块 ${chunkNumber}/${totalChunks}, 大小:${(chunkData.length / 1024).toFixed(2)}KB`);
112
+
113
+ const FormData = (await import('form-data')).default;
114
+ const form = new FormData();
115
+ form.append('upload_id', uploadId);
116
+ form.append('chunk_number', chunkNumber.toString());
117
+ form.append('total_chunks', totalChunks.toString());
118
+ form.append('file', chunkData, {
119
+ filename: `chunk_${chunkNumber}`,
120
+ contentType: 'application/octet-stream',
121
+ });
122
+
123
+ const resp = await intclawUploadHttp.post<UploadBlockResponse>(
124
+ `${INTCLAW_OAPI}/file/upload/chunk`,
125
+ form,
126
+ {
127
+ params: { access_token: oapiToken },
128
+ headers: form.getHeaders(),
129
+ timeout: 60_000,
130
+ maxBodyLength: Infinity,
131
+ }
132
+ );
133
+
134
+ if (resp.data.errcode === 0) {
135
+ log.info(`块 ${chunkNumber} 上传成功`);
136
+ return true;
137
+ } else {
138
+ log.error(`块 ${chunkNumber} 上传失败:${resp.data.errmsg}`);
139
+ return false;
140
+ }
141
+ } catch (err: any) {
142
+ log.error(`块 ${chunkNumber} 上传异常:${err.message}`);
143
+ return false;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * 步骤三:提交分块上传事务
149
+ * @param oapiToken IntClaw access_token
150
+ * @param uploadId 上传事务 ID
151
+ * @param fileName 文件名
152
+ * @param log 日志对象
153
+ */
154
+ export async function submitUploadTransaction(
155
+ oapiToken: string,
156
+ uploadId: string,
157
+ fileName: string,
158
+ debug: boolean = false,
159
+ ): Promise<{ fileId?: string; downloadCode?: string } | null> {
160
+ const log = createLogger(debug, 'IntClaw][ChunkUpload');
161
+
162
+ try {
163
+ log.info(`提交上传事务:${uploadId}`);
164
+
165
+ const resp = await intclawOapiHttp.get<SubmitTransactionResponse>(
166
+ `${INTCLAW_OAPI}/file/upload/transaction/submit`,
167
+ {
168
+ params: {
169
+ access_token: oapiToken,
170
+ upload_id: uploadId,
171
+ file_name: fileName,
172
+ },
173
+ timeout: 60_000,
174
+ }
175
+ );
176
+
177
+ if (resp.data.errcode === 0) {
178
+ log.info(`事务提交成功,file_id: ${resp.data.file_id}, download_code: ${resp.data.download_code}`);
179
+ return {
180
+ fileId: resp.data.file_id,
181
+ downloadCode: resp.data.download_code,
182
+ };
183
+ } else {
184
+ log.error(`事务提交失败:${resp.data.errmsg}`);
185
+ return null;
186
+ }
187
+ } catch (err: any) {
188
+ log.error(`事务提交异常:${err.message}`);
189
+ return null;
190
+ }
191
+ }
192
+
193
+ /**
194
+ * 计算分块参数
195
+ */
196
+ function calculateChunkParams(fileSize: number): { chunkSize: number; totalChunks: number } {
197
+ // 根据文件大小动态调整分块大小
198
+ let chunkSize = CHUNK_CONFIG.DEFAULT_CHUNK_SIZE;
199
+
200
+ if (fileSize > 100 * 1024 * 1024) {
201
+ // >100MB,使用最大分块 8MB
202
+ chunkSize = CHUNK_CONFIG.MAX_CHUNK_SIZE;
203
+ } else if (fileSize > 50 * 1024 * 1024) {
204
+ // >50MB,使用 6MB 分块
205
+ chunkSize = 6 * 1024 * 1024;
206
+ }
207
+
208
+ const totalChunks = Math.ceil(fileSize / chunkSize);
209
+ return { chunkSize, totalChunks };
210
+ }
211
+
212
+ /**
213
+ * 分块上传大文件(>20MB)
214
+ * @param filePath 文件路径
215
+ * @param mediaType 媒体类型:video, file
216
+ * @param oapiToken IntClaw access_token
217
+ * @param log 日志对象
218
+ * @returns download_code 或 null
219
+ */
220
+ export async function uploadLargeFileByChunks(
221
+ filePath: string,
222
+ mediaType: 'video' | 'file',
223
+ oapiToken: string,
224
+ debug: boolean = false,
225
+ ): Promise<string | null> {
226
+ const log = createLogger(debug, 'IntClaw][ChunkUpload');
227
+
228
+ try {
229
+ const absPath = path.resolve(filePath);
230
+ if (!fs.existsSync(absPath)) {
231
+ log.warn(`文件不存在:${absPath}`);
232
+ return null;
233
+ }
234
+
235
+ const stats = fs.statSync(absPath);
236
+ const fileSize = stats.size;
237
+ const fileName = path.basename(absPath);
238
+ const fileSizeMB = (fileSize / 1024 / 1024).toFixed(2);
239
+
240
+ log.info(`开始分块上传:${fileName}, 大小:${fileSizeMB}MB, 类型:${mediaType}`);
241
+
242
+ // 步骤一:开启上传事务
243
+ const uploadId = await enableUploadTransaction(oapiToken, fileName, fileSize, debug);
244
+ if (!uploadId) {
245
+ log.error(`开启事务失败,终止上传`);
246
+ return null;
247
+ }
248
+
249
+ // 计算分块参数
250
+ const { chunkSize, totalChunks } = calculateChunkParams(fileSize);
251
+ log.info(`分块参数:chunkSize=${(chunkSize / 1024 / 1024).toFixed(2)}MB, totalChunks=${totalChunks}`);
252
+
253
+ // 步骤二:分块上传
254
+ const fileBuffer = fs.readFileSync(absPath);
255
+ let successCount = 0;
256
+
257
+ for (let i = 0; i < totalChunks; i++) {
258
+ const start = i * chunkSize;
259
+ const end = Math.min(start + chunkSize, fileSize);
260
+ const chunkData = fileBuffer.slice(start, end);
261
+
262
+ const success = await uploadFileBlock(
263
+ oapiToken,
264
+ uploadId,
265
+ chunkData,
266
+ i + 1, // chunkNumber 从 1 开始
267
+ totalChunks,
268
+ debug
269
+ );
270
+
271
+ if (!success) {
272
+ log.error(`块 ${i + 1} 上传失败,终止上传`);
273
+ return null;
274
+ }
275
+
276
+ successCount++;
277
+ log.info(`进度:${successCount}/${totalChunks} (${((successCount / totalChunks) * 100).toFixed(1)}%)`);
278
+ }
279
+
280
+ // 步骤三:提交上传事务
281
+ const result = await submitUploadTransaction(oapiToken, uploadId, fileName, debug);
282
+ if (!result || !result.downloadCode) {
283
+ log.error(`提交事务失败`);
284
+ return null;
285
+ }
286
+
287
+ log.info(`分块上传完成:${fileName}, download_code: ${result.downloadCode}`);
288
+ return result.downloadCode;
289
+ } catch (err: any) {
290
+ log.error(`分块上传异常:${err.message}`);
291
+ return null;
292
+ }
293
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * 媒体处理公共工具和常量
3
+ */
4
+
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ import { createLogger } from '../../utils/logger.ts';
8
+ import { CHUNK_CONFIG } from './chunk-upload.ts';
9
+ import { intclawOapiHttp, intclawUploadHttp } from '../../utils/http-client.ts';
10
+ import { INTCLAW_CONFIG } from '../../../config.ts';
11
+
12
+ // ============ 常量 ============
13
+
14
+ /** 文本文件扩展名 */
15
+ export const TEXT_FILE_EXTENSIONS = new Set([
16
+ '.txt', '.md', '.json', '.yaml', '.yml', '.xml', '.html', '.css',
17
+ '.js', '.ts', '.py', '.java', '.c', '.cpp', '.h', '.sh', '.bat', '.csv',
18
+ ]);
19
+
20
+ /** 图片文件扩展名 */
21
+ export const IMAGE_EXTENSIONS = /\.(png|jpg|jpeg|gif|bmp|webp|tiff|svg)$/i;
22
+
23
+ /** 本地图片路径正则表达式(跨平台) */
24
+ export const LOCAL_IMAGE_RE =
25
+ /!\[([^\]]*)\]\(((?:file:\/\/|MEDIA:|attachment:\/\/)[^)]+|\/(?:tmp|var|private|Users|home|root)[^)]+|[A-Za-z]:[\\/][^)]+)\)/g;
26
+
27
+ /** 纯文本图片路径正则表达式 */
28
+ export const BARE_IMAGE_PATH_RE =
29
+ /`?((?:\/(?:tmp|var|private|Users|home|root)\/[^\s`'",)]+|[A-Za-z]:[\\/][^\s`'",)]+)\.(?:png|jpg|jpeg|gif|bmp|webp))`?/gi;
30
+
31
+ /** 视频标记正则表达式 */
32
+ export const VIDEO_MARKER_PATTERN = /\[INTCLAW_VIDEO\](.*?)\[\/INTCLAW_VIDEO\]/gs;
33
+
34
+ /** 音频标记正则表达式 */
35
+ export const AUDIO_MARKER_PATTERN = /\[INTCLAW_AUDIO\](.*?)\[\/INTCLAW_AUDIO\]/gs;
36
+
37
+ /** 文件标记正则表达式 */
38
+ export const FILE_MARKER_PATTERN = /\[INTCLAW_FILE\](.*?)\[\/INTCLAW_FILE\]/gs;
39
+
40
+ // ============ 工具函数 ============
41
+
42
+ /**
43
+ * 去掉 file:// / MEDIA: / attachment:// 前缀,得到实际的绝对路径
44
+ */
45
+ export function toLocalPath(raw: string): string {
46
+ let filePath = raw;
47
+ if (filePath.startsWith('file://')) filePath = filePath.replace('file://', '');
48
+ else if (filePath.startsWith('MEDIA:')) filePath = filePath.replace('MEDIA:', '');
49
+ else if (filePath.startsWith('attachment://')) filePath = filePath.replace('attachment://', '');
50
+
51
+ try {
52
+ filePath = decodeURIComponent(filePath);
53
+ } catch {
54
+ // 解码失败则保持原样
55
+ }
56
+ return filePath;
57
+ }
58
+
59
+ /**
60
+ * 通用媒体文件上传函数
61
+ */
62
+ export async function uploadMediaToIntClaw(
63
+ filePath: string,
64
+ mediaType: 'image' | 'file' | 'video' | 'voice',
65
+ oapiToken: string,
66
+ maxSize: number = 20 * 1024 * 1024,
67
+ logOrDebug?: any,
68
+ debug?: boolean,
69
+ ): Promise<string | null> {
70
+ const debugEnabled =
71
+ typeof logOrDebug === 'boolean' ? logOrDebug === true : debug === true;
72
+ const externalLog = typeof logOrDebug === 'boolean' ? undefined : logOrDebug;
73
+ const log = externalLog ?? createLogger(debugEnabled, `IntClaw][${mediaType}`);
74
+
75
+ log?.info?.(
76
+ `[uploadMediaToIntClaw] 开始上传,filePath: ${filePath}, mediaType: ${mediaType}, debug: ${debugEnabled}`,
77
+ );
78
+
79
+ try {
80
+ const FormData = (await import('form-data')).default;
81
+
82
+ const absPath = toLocalPath(filePath);
83
+ log?.info?.(`检查文件是否存在:${absPath}`);
84
+ if (!fs.existsSync(absPath)) {
85
+ log?.warn?.(`文件不存在:${absPath}`);
86
+ return null;
87
+ }
88
+
89
+ const stats = fs.statSync(absPath);
90
+ const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
91
+ const fileSize = stats.size;
92
+
93
+ // ✅ 对于视频和文件类型,如果超过 20MB,使用分块上传
94
+ if ((mediaType === 'video' || mediaType === 'file') && fileSize > CHUNK_CONFIG.SIZE_THRESHOLD) {
95
+ log?.info?.(`文件超过 20MB,使用分块上传:${absPath} (${fileSizeMB}MB)`);
96
+ try {
97
+ const { uploadLargeFileByChunks } = await import('./chunk-upload.js');
98
+ const downloadCode = await uploadLargeFileByChunks(absPath, mediaType, oapiToken, debugEnabled);
99
+ if (downloadCode) {
100
+ log?.info?.(`分块上传成功:${absPath}, download_code: ${downloadCode}`);
101
+ return downloadCode;
102
+ }
103
+ log?.error?.(`分块上传失败:${absPath}`);
104
+ } catch (chunkErr: any) {
105
+ log?.error?.(`分块上传异常:${chunkErr.message}`);
106
+ }
107
+ return null;
108
+ }
109
+
110
+ // 检查文件大小(对于小于 20MB 的文件)
111
+ if (stats.size > maxSize) {
112
+ const maxSizeMB = (maxSize / (1024 * 1024)).toFixed(0);
113
+ log?.warn?.(
114
+ `文件过大:${absPath}, 大小:${fileSizeMB}MB, 超过限制 ${maxSizeMB}MB`,
115
+ );
116
+ return null;
117
+ }
118
+
119
+ const form = new FormData();
120
+ form.append('media', fs.createReadStream(absPath), {
121
+ filename: path.basename(absPath),
122
+ contentType: mediaType === 'image' ? 'image/jpeg' : 'application/octet-stream',
123
+ });
124
+
125
+ const uploadType = mediaType;
126
+
127
+ log?.info?.(`上传文件:${absPath} (${fileSizeMB}MB), uploadType=${uploadType}`);
128
+ const resp = await intclawUploadHttp.post(
129
+ `${INTCLAW_OAPI}/media/upload`,
130
+ form,
131
+ {
132
+ params: { access_token: oapiToken, type: mediaType },
133
+ headers: form.getHeaders(),
134
+ timeout: 60_000,
135
+ maxBodyLength: Infinity,
136
+ },
137
+ );
138
+
139
+ const mediaId = resp.data?.media_id;
140
+ if (mediaId) {
141
+ const cleanMediaId = mediaId.startsWith('@') ? mediaId.substring(1) : mediaId;
142
+ log?.info?.(`上传成功:mediaId=${cleanMediaId}`);
143
+ return cleanMediaId;
144
+ }
145
+ log?.warn?.(`上传返回无 media_id`);
146
+ return null;
147
+ } catch (err: any) {
148
+ log?.error?.(`上传失败:${err.message}`);
149
+ return null;
150
+ }
151
+ }
152
+
153
+ /** IntClaw OAPI 常量 */
154
+ export const INTCLAW_OAPI = INTCLAW_CONFIG.OAPI_BASE_URL;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 文件处理模块
3
+ * 支持文档解析(docx, pdf, txt 等)和文件上传
4
+ */
5
+
6
+ import * as fs from 'fs';
7
+ import type { IntclawConfig } from '../../types/index.ts';
8
+ import { FILE_MARKER_PATTERN, toLocalPath, uploadMediaToIntClaw, TEXT_FILE_EXTENSIONS } from './common.ts';
9
+
10
+ /**
11
+ * 解析文档文件,提取文本内容
12
+ */
13
+ export async function parseDocumentFile(filePath: string, log?: any): Promise<string | null> {
14
+ try {
15
+ const ext = '.' + filePath.split('.').pop()?.toLowerCase();
16
+
17
+ if (!TEXT_FILE_EXTENSIONS.has(ext)) {
18
+ log?.warn?.(`[IntClaw][File] 不支持的文件类型:${ext}`);
19
+ return null;
20
+ }
21
+
22
+ const content = fs.readFileSync(filePath, 'utf-8');
23
+ log?.info?.(`[IntClaw][File] 解析文件成功:${filePath}, 长度=${content.length}`);
24
+ return content;
25
+ } catch (err: any) {
26
+ log?.error?.(`[IntClaw][File] 解析文件失败:${err.message}`);
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * 提取文件标记并发送文件消息
33
+ */
34
+ export async function processFileMarkers(
35
+ content: string,
36
+ sessionWebhook: string,
37
+ config: IntclawConfig,
38
+ oapiToken: string | null,
39
+ log?: any,
40
+ useProactiveApi: boolean = false,
41
+ target?: any,
42
+ ): Promise<string> {
43
+ const logPrefix = useProactiveApi ? '[IntClaw][File][Proactive]' : '[IntClaw][File]';
44
+
45
+ if (!oapiToken) {
46
+ log?.warn?.(`${logPrefix} 无 oapiToken,跳过文件处理`);
47
+ return content;
48
+ }
49
+
50
+ const matches = [...content.matchAll(FILE_MARKER_PATTERN)];
51
+ if (matches.length === 0) return content;
52
+
53
+ log?.info?.(`${logPrefix} 检测到 ${matches.length} 个文件,开始上传...`);
54
+
55
+ let result = content;
56
+ for (const match of matches) {
57
+ const full = match[0];
58
+ try {
59
+ const fileData = JSON.parse(match[1]);
60
+ const absPath = toLocalPath(fileData.path);
61
+ const uploadResult = await uploadMediaToIntClaw(absPath, 'file', oapiToken, 20 * 1024 * 1024, log);
62
+ result = result.replace(full, uploadResult ? `[文件已上传:${uploadResult.downloadUrl}]` : '⚠️ 文件上传失败');
63
+ } catch {
64
+ log?.warn?.(`${logPrefix} 解析文件标记失败:${match[1]}`);
65
+ result = result.replace(full, '');
66
+ }
67
+ }
68
+
69
+ return result;
70
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * 图片处理模块
3
+ * 支持图片上传、本地路径处理
4
+ */
5
+
6
+ import type { Logger } from 'openclaw/plugin-sdk';
7
+ import {
8
+ LOCAL_IMAGE_RE,
9
+ BARE_IMAGE_PATH_RE,
10
+ toLocalPath,
11
+ uploadMediaToIntClaw,
12
+ } from './common.ts';
13
+
14
+ /**
15
+ * 扫描内容中的本地图片路径,上传到IntClaw并替换为 media_id
16
+ */
17
+ export async function processLocalImages(
18
+ content: string,
19
+ oapiToken: string | null,
20
+ log?: Logger,
21
+ ): Promise<string> {
22
+ if (!oapiToken) {
23
+ log?.warn?.(`[IntClaw][Media] 无 oapiToken,跳过图片后处理`);
24
+ return content;
25
+ }
26
+
27
+ let result = content;
28
+
29
+ // 第一步:匹配 markdown 图片语法 ![alt](path)
30
+ const mdMatches = [...content.matchAll(LOCAL_IMAGE_RE)];
31
+ if (mdMatches.length > 0) {
32
+ log?.info?.(`[IntClaw][Media] 检测到 ${mdMatches.length} 个 markdown 图片,开始上传...`);
33
+ for (const match of mdMatches) {
34
+ const [fullMatch, alt, rawPath] = match;
35
+ const cleanPath = rawPath.replace(/\\ /g, ' ');
36
+ const uploadResult = await uploadMediaToIntClaw(cleanPath, 'image', oapiToken, 20 * 1024 * 1024, log);
37
+ if (uploadResult) {
38
+ result = result.replace(fullMatch, `![${alt}](${uploadResult.downloadUrl})`);
39
+ }
40
+ }
41
+ }
42
+
43
+ // 第二步:匹配纯文本中的本地图片路径
44
+ const bareMatches = [...result.matchAll(BARE_IMAGE_PATH_RE)];
45
+ const newBareMatches = bareMatches.filter((m) => {
46
+ if (m.index === undefined) return false;
47
+ const idx = m.index;
48
+ const before = result.slice(Math.max(0, idx - 10), idx);
49
+ return !before.includes('](');
50
+ });
51
+
52
+ if (newBareMatches.length > 0) {
53
+ log?.info?.(`[IntClaw][Media] 检测到 ${newBareMatches.length} 个纯文本图片路径,开始上传...`);
54
+ for (const match of newBareMatches.reverse()) {
55
+ const [fullMatch, rawPath] = match;
56
+ log?.info?.(`[IntClaw][Media] 纯文本图片:"${fullMatch}" -> path="${rawPath}"`);
57
+ const uploadResult = await uploadMediaToIntClaw(rawPath, 'image', oapiToken, 20 * 1024 * 1024, log);
58
+ if (uploadResult) {
59
+ const replacement = `![](${uploadResult.downloadUrl})`;
60
+ result = result.slice(0, match.index!) + result.slice(match.index!).replace(fullMatch, replacement);
61
+ log?.info?.(`[IntClaw][Media] 替换纯文本路径为图片:${replacement}`);
62
+ }
63
+ }
64
+ }
65
+
66
+ return result;
67
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 媒体处理模块统一导出
3
+ */
4
+
5
+ export * from './common.ts';
6
+ export * from './image.ts';
7
+ export * from './video.ts';
8
+ export * from './audio.ts';
9
+ export * from './file.ts';
10
+ export * from './chunk-upload.ts';