adp-openclaw 0.0.33 → 0.0.35
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/index.ts +3 -3
- package/package.json +1 -1
- package/src/adp-upload-tool.ts +40 -222
package/index.ts
CHANGED
|
@@ -49,7 +49,6 @@ export {
|
|
|
49
49
|
adpUploadFilesWithConfig,
|
|
50
50
|
getStorageCredential,
|
|
51
51
|
uploadFileToCos,
|
|
52
|
-
getSignedDownloadUrl,
|
|
53
52
|
resolveClientToken,
|
|
54
53
|
// Class
|
|
55
54
|
AdpUploader,
|
|
@@ -101,7 +100,8 @@ const plugin = {
|
|
|
101
100
|
"If you can see this tool description, you are connected to the ADP server. " +
|
|
102
101
|
"Use this tool to send 1-10 local files of any type (including images and documents) to the user. " +
|
|
103
102
|
"A successful call means the files were uploaded and download URLs are returned. " +
|
|
104
|
-
"
|
|
103
|
+
"IMPORTANT: You MUST include the download URLs in your response to the user, " +
|
|
104
|
+
"and remind the user that the download links are valid for 24 hours. " +
|
|
105
105
|
"Parameters must be local filesystem paths only.",
|
|
106
106
|
parameters: ADP_UPLOAD_TOOL_SCHEMA,
|
|
107
107
|
async execute(toolCallId: string, params: unknown) {
|
|
@@ -199,7 +199,7 @@ const plugin = {
|
|
|
199
199
|
|
|
200
200
|
content.push({
|
|
201
201
|
type: "text",
|
|
202
|
-
text: `Files uploaded successfully:\n${urlSummary}
|
|
202
|
+
text: `Files uploaded successfully:\n${urlSummary}\n\n⚠️ Note: Please include these download links in your response to the user and remind them that the links are valid for 24 hours.`,
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
return {
|
package/package.json
CHANGED
package/src/adp-upload-tool.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* ADP Upload Tool
|
|
3
3
|
*
|
|
4
4
|
* 功能:
|
|
5
|
-
* 1.
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
5
|
+
* 1. 通过固定接口获取预签名的上传URL和下载URL
|
|
6
|
+
* 2. 使用预签名URL直接PUT上传文件到COS
|
|
7
|
+
* 3. 返回文件下载链接
|
|
8
8
|
*
|
|
9
9
|
* 注意:机器人 token 从 adp-openclaw 插件配置的 clientToken 读取
|
|
10
10
|
*/
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
import { constants } from "node:fs";
|
|
13
13
|
import { access, readFile, stat } from "node:fs/promises";
|
|
14
14
|
import { basename, extname } from "node:path";
|
|
15
|
-
import * as crypto from "node:crypto";
|
|
16
15
|
import type { AdpOpenclawChannelConfig } from "./channel.js";
|
|
17
16
|
|
|
18
17
|
// ==================== 类型定义 ====================
|
|
@@ -32,14 +31,14 @@ export interface Credentials {
|
|
|
32
31
|
|
|
33
32
|
/** 存储凭证响应 */
|
|
34
33
|
export interface DescribeRemoteBotStorageCredentialRsp {
|
|
35
|
-
credentials: Credentials; //
|
|
34
|
+
credentials: Credentials; // 密钥信息(兼容旧字段,新逻辑不使用)
|
|
36
35
|
expired_time: number; // 失效时间
|
|
37
36
|
start_time: number; // 起始时间
|
|
38
37
|
bucket: string; // 对象存储 桶
|
|
39
38
|
region: string; // 对象存储 可用区
|
|
40
39
|
file_path: string; // 文件目录
|
|
41
|
-
upload_url: string; //
|
|
42
|
-
file_url: string; //
|
|
40
|
+
upload_url: string; // 预签名上传URL(需要decodeURIComponent)
|
|
41
|
+
file_url: string; // 预签名下载URL(需要decodeURIComponent)
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
/** 上传结果 */
|
|
@@ -65,9 +64,6 @@ const CREDENTIAL_API_URL = "http://101.32.33.231:9876/describeRemoteBotStorageCr
|
|
|
65
64
|
/** 请求超时时间 (毫秒) */
|
|
66
65
|
const REQUEST_TIMEOUT_MS = 30000;
|
|
67
66
|
|
|
68
|
-
/** COS 签名有效期 (秒) */
|
|
69
|
-
const SIGN_EXPIRE_SECONDS = 3600;
|
|
70
|
-
|
|
71
67
|
// ==================== 工具函数 ====================
|
|
72
68
|
|
|
73
69
|
/**
|
|
@@ -158,81 +154,6 @@ async function validateLocalFile(filePath: string): Promise<FileMetadata> {
|
|
|
158
154
|
};
|
|
159
155
|
}
|
|
160
156
|
|
|
161
|
-
// ==================== COS 签名工具 ====================
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* 生成 COS 签名
|
|
165
|
-
* 参考:https://cloud.tencent.com/document/product/436/7778
|
|
166
|
-
*/
|
|
167
|
-
function generateCosSignature(
|
|
168
|
-
secretId: string,
|
|
169
|
-
secretKey: string,
|
|
170
|
-
method: string,
|
|
171
|
-
pathname: string,
|
|
172
|
-
headers: Record<string, string> = {},
|
|
173
|
-
params: Record<string, string> = {},
|
|
174
|
-
expireSeconds: number = SIGN_EXPIRE_SECONDS
|
|
175
|
-
): string {
|
|
176
|
-
const now = Math.floor(Date.now() / 1000);
|
|
177
|
-
const keyTime = `${now};${now + expireSeconds}`;
|
|
178
|
-
|
|
179
|
-
// 1. 生成 SignKey
|
|
180
|
-
const signKey = crypto
|
|
181
|
-
.createHmac("sha1", secretKey)
|
|
182
|
-
.update(keyTime)
|
|
183
|
-
.digest("hex");
|
|
184
|
-
|
|
185
|
-
// 2. 生成 UrlParamList 和 HttpParameters
|
|
186
|
-
const sortedParams = Object.keys(params)
|
|
187
|
-
.sort()
|
|
188
|
-
.map((k) => `${encodeURIComponent(k.toLowerCase())}=${encodeURIComponent(params[k])}`)
|
|
189
|
-
.join("&");
|
|
190
|
-
const urlParamList = Object.keys(params)
|
|
191
|
-
.sort()
|
|
192
|
-
.map((k) => encodeURIComponent(k.toLowerCase()))
|
|
193
|
-
.join(";");
|
|
194
|
-
|
|
195
|
-
// 3. 生成 HeaderList 和 HttpHeaders
|
|
196
|
-
const sortedHeaders = Object.keys(headers)
|
|
197
|
-
.sort()
|
|
198
|
-
.map((k) => `${encodeURIComponent(k.toLowerCase())}=${encodeURIComponent(headers[k])}`)
|
|
199
|
-
.join("&");
|
|
200
|
-
const headerList = Object.keys(headers)
|
|
201
|
-
.sort()
|
|
202
|
-
.map((k) => encodeURIComponent(k.toLowerCase()))
|
|
203
|
-
.join(";");
|
|
204
|
-
|
|
205
|
-
// 4. 生成 HttpString
|
|
206
|
-
const httpString = [
|
|
207
|
-
method.toLowerCase(),
|
|
208
|
-
pathname,
|
|
209
|
-
sortedParams,
|
|
210
|
-
sortedHeaders,
|
|
211
|
-
"",
|
|
212
|
-
].join("\n");
|
|
213
|
-
|
|
214
|
-
// 5. 生成 StringToSign
|
|
215
|
-
const httpStringHash = crypto.createHash("sha1").update(httpString).digest("hex");
|
|
216
|
-
const stringToSign = ["sha1", keyTime, httpStringHash, ""].join("\n");
|
|
217
|
-
|
|
218
|
-
// 6. 生成 Signature
|
|
219
|
-
const signature = crypto
|
|
220
|
-
.createHmac("sha1", signKey)
|
|
221
|
-
.update(stringToSign)
|
|
222
|
-
.digest("hex");
|
|
223
|
-
|
|
224
|
-
// 7. 生成最终签名
|
|
225
|
-
return [
|
|
226
|
-
`q-sign-algorithm=sha1`,
|
|
227
|
-
`q-ak=${secretId}`,
|
|
228
|
-
`q-sign-time=${keyTime}`,
|
|
229
|
-
`q-key-time=${keyTime}`,
|
|
230
|
-
`q-header-list=${headerList}`,
|
|
231
|
-
`q-url-param-list=${urlParamList}`,
|
|
232
|
-
`q-signature=${signature}`,
|
|
233
|
-
].join("&");
|
|
234
|
-
}
|
|
235
|
-
|
|
236
157
|
// ==================== 核心功能 ====================
|
|
237
158
|
|
|
238
159
|
/**
|
|
@@ -265,20 +186,20 @@ export async function getStorageCredential(
|
|
|
265
186
|
|
|
266
187
|
const result = await response.json();
|
|
267
188
|
|
|
268
|
-
// 检查响应数据的完整性
|
|
269
|
-
if (!result.
|
|
270
|
-
throw new Error("获取存储凭证失败:
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (!result.bucket || !result.region) {
|
|
274
|
-
throw new Error("获取存储凭证失败: 缺少bucket或region信息");
|
|
189
|
+
// 检查响应数据的完整性 - 现在主要检查 upload_url 和 file_url
|
|
190
|
+
if (!result.upload_url || !result.file_url) {
|
|
191
|
+
throw new Error("获取存储凭证失败: 缺少upload_url或file_url信息");
|
|
275
192
|
}
|
|
276
193
|
|
|
277
194
|
return result as DescribeRemoteBotStorageCredentialRsp;
|
|
278
195
|
}
|
|
279
196
|
|
|
280
197
|
/**
|
|
281
|
-
* 上传文件到COS
|
|
198
|
+
* 上传文件到COS(使用预签名URL直接PUT上传)
|
|
199
|
+
*
|
|
200
|
+
* @param filePath - 本地文件路径
|
|
201
|
+
* @param credential - 存储凭证(包含预签名的 upload_url 和 file_url)
|
|
202
|
+
* @returns 文件下载URL
|
|
282
203
|
*/
|
|
283
204
|
export async function uploadFileToCos(
|
|
284
205
|
filePath: string,
|
|
@@ -290,54 +211,18 @@ export async function uploadFileToCos(
|
|
|
290
211
|
// 2. 读取文件内容
|
|
291
212
|
const fileContent = await readFile(filePath);
|
|
292
213
|
|
|
293
|
-
// 3.
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
// 判断 file_path 是目录路径还是完整文件路径
|
|
297
|
-
// - 如果以 "/" 结尾,是目录路径,需要拼接文件名
|
|
298
|
-
// - 如果不以 "/" 结尾,已经是完整文件路径(服务端生成的唯一路径)
|
|
299
|
-
// 同时确保 cosKey 不以 "/" 开头(COS key 不应该以 / 开头)
|
|
300
|
-
let cosKey: string;
|
|
301
|
-
if (file_path.endsWith("/")) {
|
|
302
|
-
// 目录路径,拼接文件名
|
|
303
|
-
cosKey = `${file_path}${fileMetadata.fileName}`;
|
|
304
|
-
} else {
|
|
305
|
-
// 完整文件路径,直接使用
|
|
306
|
-
cosKey = file_path;
|
|
307
|
-
}
|
|
308
|
-
// 移除开头的斜杠(COS key 不应该以 / 开头)
|
|
309
|
-
if (cosKey.startsWith("/")) {
|
|
310
|
-
cosKey = cosKey.slice(1);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const host = `${bucket}.cos.${region}.myqcloud.com`;
|
|
314
|
-
const uploadUrl = `https://${host}/${cosKey}`;
|
|
315
|
-
|
|
316
|
-
// 4. 生成上传签名
|
|
317
|
-
const headers: Record<string, string> = {
|
|
318
|
-
"Content-Type": fileMetadata.contentType,
|
|
319
|
-
"Content-Length": String(fileContent.length),
|
|
320
|
-
Host: host,
|
|
321
|
-
};
|
|
214
|
+
// 3. 解码预签名的上传URL和下载URL
|
|
215
|
+
const uploadUrl = decodeURIComponent(credential.upload_url);
|
|
216
|
+
const fileUrl = decodeURIComponent(credential.file_url);
|
|
322
217
|
|
|
323
|
-
|
|
324
|
-
credentials.tmp_secret_id,
|
|
325
|
-
credentials.tmp_secret_key,
|
|
326
|
-
"PUT",
|
|
327
|
-
`/${cosKey}`,
|
|
328
|
-
{ host: host },
|
|
329
|
-
{}
|
|
330
|
-
);
|
|
331
|
-
|
|
332
|
-
// 5. 执行上传
|
|
218
|
+
// 4. 执行上传(直接PUT到预签名URL,不需要额外签名)
|
|
333
219
|
const response = await fetchWithTimeout(
|
|
334
220
|
uploadUrl,
|
|
335
221
|
{
|
|
336
222
|
method: "PUT",
|
|
337
223
|
headers: {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
"x-cos-security-token": credentials.token,
|
|
224
|
+
"Content-Type": fileMetadata.contentType,
|
|
225
|
+
"Content-Length": String(fileContent.length),
|
|
341
226
|
},
|
|
342
227
|
body: fileContent,
|
|
343
228
|
},
|
|
@@ -349,35 +234,8 @@ export async function uploadFileToCos(
|
|
|
349
234
|
throw new Error(`上传文件失败: ${response.status} ${errorText}`);
|
|
350
235
|
}
|
|
351
236
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* 获取文件下载签名链接
|
|
357
|
-
*/
|
|
358
|
-
export function getSignedDownloadUrl(
|
|
359
|
-
cosKey: string,
|
|
360
|
-
credential: DescribeRemoteBotStorageCredentialRsp,
|
|
361
|
-
expireSeconds: number = SIGN_EXPIRE_SECONDS
|
|
362
|
-
): string {
|
|
363
|
-
const { bucket, region, credentials } = credential;
|
|
364
|
-
const host = `${bucket}.cos.${region}.myqcloud.com`;
|
|
365
|
-
|
|
366
|
-
// 生成下载签名
|
|
367
|
-
const signature = generateCosSignature(
|
|
368
|
-
credentials.tmp_secret_id,
|
|
369
|
-
credentials.tmp_secret_key,
|
|
370
|
-
"GET",
|
|
371
|
-
`/${cosKey}`,
|
|
372
|
-
{ host: host },
|
|
373
|
-
{},
|
|
374
|
-
expireSeconds
|
|
375
|
-
);
|
|
376
|
-
|
|
377
|
-
// 构建签名URL
|
|
378
|
-
const signedUrl = `https://${host}/${cosKey}?${signature}&x-cos-security-token=${encodeURIComponent(credentials.token)}`;
|
|
379
|
-
|
|
380
|
-
return signedUrl;
|
|
237
|
+
// 5. 返回下载URL
|
|
238
|
+
return fileUrl;
|
|
381
239
|
}
|
|
382
240
|
|
|
383
241
|
// ==================== 从配置读取 Token 的辅助函数 ====================
|
|
@@ -403,7 +261,7 @@ export function resolveClientToken(channelCfg?: AdpOpenclawChannelConfig): strin
|
|
|
403
261
|
* @param filePath - 要上传的本地文件路径
|
|
404
262
|
* @param botToken - 机器人 token
|
|
405
263
|
* @param fileType - 文件类型(可选)
|
|
406
|
-
* @returns
|
|
264
|
+
* @returns 上传结果,包含下载链接或错误信息
|
|
407
265
|
*/
|
|
408
266
|
export async function adpUploadFile(
|
|
409
267
|
filePath: string,
|
|
@@ -411,18 +269,15 @@ export async function adpUploadFile(
|
|
|
411
269
|
fileType?: string
|
|
412
270
|
): Promise<UploadResult> {
|
|
413
271
|
try {
|
|
414
|
-
// 1.
|
|
272
|
+
// 1. 获取预签名URL
|
|
415
273
|
const credential = await getStorageCredential(botToken, fileType);
|
|
416
274
|
|
|
417
|
-
// 2. 上传文件到 COS
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
// 3. 获取签名下载链接
|
|
421
|
-
const signedUrl = getSignedDownloadUrl(cosKey, credential);
|
|
275
|
+
// 2. 上传文件到 COS(返回下载URL)
|
|
276
|
+
const fileUrl = await uploadFileToCos(filePath, credential);
|
|
422
277
|
|
|
423
278
|
return {
|
|
424
279
|
ok: true,
|
|
425
|
-
fileUrl:
|
|
280
|
+
fileUrl: fileUrl,
|
|
426
281
|
};
|
|
427
282
|
} catch (error) {
|
|
428
283
|
return {
|
|
@@ -460,6 +315,8 @@ export async function adpUploadFileWithConfig(
|
|
|
460
315
|
/**
|
|
461
316
|
* 批量上传文件(使用显式传入的 token)
|
|
462
317
|
*
|
|
318
|
+
* 注意:每个文件需要独立获取预签名URL,因为服务端为每个文件生成唯一路径
|
|
319
|
+
*
|
|
463
320
|
* @param filePaths - 要上传的本地文件路径列表
|
|
464
321
|
* @param botToken - 机器人 token
|
|
465
322
|
* @param fileType - 文件类型(可选)
|
|
@@ -470,36 +327,10 @@ export async function adpUploadFiles(
|
|
|
470
327
|
botToken: string,
|
|
471
328
|
fileType?: string
|
|
472
329
|
): Promise<UploadResult[]> {
|
|
473
|
-
//
|
|
474
|
-
let credential: DescribeRemoteBotStorageCredentialRsp;
|
|
475
|
-
|
|
476
|
-
try {
|
|
477
|
-
credential = await getStorageCredential(botToken, fileType);
|
|
478
|
-
} catch (error) {
|
|
479
|
-
// 如果获取密钥失败,返回所有文件都失败
|
|
480
|
-
const errorMsg = error instanceof Error ? error.message : "获取临时密钥失败";
|
|
481
|
-
return filePaths.map(() => ({
|
|
482
|
-
ok: false,
|
|
483
|
-
error: errorMsg,
|
|
484
|
-
}));
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// 并发上传所有文件
|
|
330
|
+
// 逐个上传文件(每个文件需要独立的预签名URL)
|
|
488
331
|
const results = await Promise.all(
|
|
489
332
|
filePaths.map(async (filePath): Promise<UploadResult> => {
|
|
490
|
-
|
|
491
|
-
const cosKey = await uploadFileToCos(filePath, credential);
|
|
492
|
-
const signedUrl = getSignedDownloadUrl(cosKey, credential);
|
|
493
|
-
return {
|
|
494
|
-
ok: true,
|
|
495
|
-
fileUrl: signedUrl,
|
|
496
|
-
};
|
|
497
|
-
} catch (error) {
|
|
498
|
-
return {
|
|
499
|
-
ok: false,
|
|
500
|
-
error: error instanceof Error ? error.message : "上传失败",
|
|
501
|
-
};
|
|
502
|
-
}
|
|
333
|
+
return adpUploadFile(filePath, botToken, fileType);
|
|
503
334
|
})
|
|
504
335
|
);
|
|
505
336
|
|
|
@@ -812,6 +643,8 @@ export interface AdpUploadOptions {
|
|
|
812
643
|
|
|
813
644
|
/**
|
|
814
645
|
* 上传文件到 ADP 存储(返回兼容 kimi 格式的结果)
|
|
646
|
+
*
|
|
647
|
+
* 注意:每个文件需要独立获取预签名URL,因为服务端为每个文件生成唯一路径
|
|
815
648
|
*/
|
|
816
649
|
export const uploadFilesToAdpEndpoint = async (
|
|
817
650
|
paths: string[],
|
|
@@ -826,23 +659,6 @@ export const uploadFilesToAdpEndpoint = async (
|
|
|
826
659
|
};
|
|
827
660
|
}
|
|
828
661
|
|
|
829
|
-
// 获取临时密钥
|
|
830
|
-
let credential: DescribeRemoteBotStorageCredentialRsp;
|
|
831
|
-
try {
|
|
832
|
-
credential = await getStorageCredential(botToken, fileType);
|
|
833
|
-
} catch (error) {
|
|
834
|
-
return {
|
|
835
|
-
ok: false,
|
|
836
|
-
error: {
|
|
837
|
-
...INVALID_PARAMS,
|
|
838
|
-
data: {
|
|
839
|
-
field: "credential",
|
|
840
|
-
reason: error instanceof Error ? error.message : "failed to get storage credential",
|
|
841
|
-
},
|
|
842
|
-
},
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
|
|
846
662
|
// 验证所有本地文件
|
|
847
663
|
const validationResults = await runWithConcurrency(
|
|
848
664
|
paths,
|
|
@@ -870,19 +686,21 @@ export const uploadFilesToAdpEndpoint = async (
|
|
|
870
686
|
(r) => (r as ValidationSuccess<FileMetadata>).value
|
|
871
687
|
);
|
|
872
688
|
|
|
873
|
-
//
|
|
689
|
+
// 并发上传所有文件(每个文件独立获取预签名URL)
|
|
874
690
|
try {
|
|
875
691
|
const uploadResults = await runWithConcurrency(
|
|
876
692
|
fileMetadatas,
|
|
877
693
|
maxConcurrency,
|
|
878
694
|
async (metadata, _index): Promise<UploadedFileInfo> => {
|
|
879
|
-
|
|
880
|
-
const
|
|
695
|
+
// 为每个文件获取预签名URL
|
|
696
|
+
const credential = await getStorageCredential(botToken, fileType);
|
|
697
|
+
// 上传文件并获取下载URL
|
|
698
|
+
const downloadUrl = await uploadFileToCos(metadata.path, credential);
|
|
881
699
|
return {
|
|
882
|
-
uri:
|
|
700
|
+
uri: downloadUrl,
|
|
883
701
|
name: metadata.fileName,
|
|
884
702
|
mimeType: metadata.contentType,
|
|
885
|
-
downloadUrl:
|
|
703
|
+
downloadUrl: downloadUrl,
|
|
886
704
|
};
|
|
887
705
|
}
|
|
888
706
|
);
|