adp-openclaw 0.0.32 → 0.0.34
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 +0 -1
- package/package.json +1 -1
- package/src/adp-upload-tool.ts +40 -205
package/index.ts
CHANGED
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,37 +211,18 @@ export async function uploadFileToCos(
|
|
|
290
211
|
// 2. 读取文件内容
|
|
291
212
|
const fileContent = await readFile(filePath);
|
|
292
213
|
|
|
293
|
-
// 3.
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const host = `${bucket}.cos.${region}.myqcloud.com`;
|
|
297
|
-
const uploadUrl = `https://${host}/${cosKey}`;
|
|
298
|
-
|
|
299
|
-
// 4. 生成上传签名
|
|
300
|
-
const headers: Record<string, string> = {
|
|
301
|
-
"Content-Type": fileMetadata.contentType,
|
|
302
|
-
"Content-Length": String(fileContent.length),
|
|
303
|
-
Host: host,
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const signature = generateCosSignature(
|
|
307
|
-
credentials.tmp_secret_id,
|
|
308
|
-
credentials.tmp_secret_key,
|
|
309
|
-
"PUT",
|
|
310
|
-
`/${cosKey}`,
|
|
311
|
-
{ host: host },
|
|
312
|
-
{}
|
|
313
|
-
);
|
|
214
|
+
// 3. 解码预签名的上传URL和下载URL
|
|
215
|
+
const uploadUrl = decodeURIComponent(credential.upload_url);
|
|
216
|
+
const fileUrl = decodeURIComponent(credential.file_url);
|
|
314
217
|
|
|
315
|
-
//
|
|
218
|
+
// 4. 执行上传(直接PUT到预签名URL,不需要额外签名)
|
|
316
219
|
const response = await fetchWithTimeout(
|
|
317
220
|
uploadUrl,
|
|
318
221
|
{
|
|
319
222
|
method: "PUT",
|
|
320
223
|
headers: {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
"x-cos-security-token": credentials.token,
|
|
224
|
+
"Content-Type": fileMetadata.contentType,
|
|
225
|
+
"Content-Length": String(fileContent.length),
|
|
324
226
|
},
|
|
325
227
|
body: fileContent,
|
|
326
228
|
},
|
|
@@ -332,35 +234,8 @@ export async function uploadFileToCos(
|
|
|
332
234
|
throw new Error(`上传文件失败: ${response.status} ${errorText}`);
|
|
333
235
|
}
|
|
334
236
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* 获取文件下载签名链接
|
|
340
|
-
*/
|
|
341
|
-
export function getSignedDownloadUrl(
|
|
342
|
-
cosKey: string,
|
|
343
|
-
credential: DescribeRemoteBotStorageCredentialRsp,
|
|
344
|
-
expireSeconds: number = SIGN_EXPIRE_SECONDS
|
|
345
|
-
): string {
|
|
346
|
-
const { bucket, region, credentials } = credential;
|
|
347
|
-
const host = `${bucket}.cos.${region}.myqcloud.com`;
|
|
348
|
-
|
|
349
|
-
// 生成下载签名
|
|
350
|
-
const signature = generateCosSignature(
|
|
351
|
-
credentials.tmp_secret_id,
|
|
352
|
-
credentials.tmp_secret_key,
|
|
353
|
-
"GET",
|
|
354
|
-
`/${cosKey}`,
|
|
355
|
-
{ host: host },
|
|
356
|
-
{},
|
|
357
|
-
expireSeconds
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
// 构建签名URL
|
|
361
|
-
const signedUrl = `https://${host}/${cosKey}?${signature}&x-cos-security-token=${encodeURIComponent(credentials.token)}`;
|
|
362
|
-
|
|
363
|
-
return signedUrl;
|
|
237
|
+
// 5. 返回下载URL
|
|
238
|
+
return fileUrl;
|
|
364
239
|
}
|
|
365
240
|
|
|
366
241
|
// ==================== 从配置读取 Token 的辅助函数 ====================
|
|
@@ -386,7 +261,7 @@ export function resolveClientToken(channelCfg?: AdpOpenclawChannelConfig): strin
|
|
|
386
261
|
* @param filePath - 要上传的本地文件路径
|
|
387
262
|
* @param botToken - 机器人 token
|
|
388
263
|
* @param fileType - 文件类型(可选)
|
|
389
|
-
* @returns
|
|
264
|
+
* @returns 上传结果,包含下载链接或错误信息
|
|
390
265
|
*/
|
|
391
266
|
export async function adpUploadFile(
|
|
392
267
|
filePath: string,
|
|
@@ -394,18 +269,15 @@ export async function adpUploadFile(
|
|
|
394
269
|
fileType?: string
|
|
395
270
|
): Promise<UploadResult> {
|
|
396
271
|
try {
|
|
397
|
-
// 1.
|
|
272
|
+
// 1. 获取预签名URL
|
|
398
273
|
const credential = await getStorageCredential(botToken, fileType);
|
|
399
274
|
|
|
400
|
-
// 2. 上传文件到 COS
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
// 3. 获取签名下载链接
|
|
404
|
-
const signedUrl = getSignedDownloadUrl(cosKey, credential);
|
|
275
|
+
// 2. 上传文件到 COS(返回下载URL)
|
|
276
|
+
const fileUrl = await uploadFileToCos(filePath, credential);
|
|
405
277
|
|
|
406
278
|
return {
|
|
407
279
|
ok: true,
|
|
408
|
-
fileUrl:
|
|
280
|
+
fileUrl: fileUrl,
|
|
409
281
|
};
|
|
410
282
|
} catch (error) {
|
|
411
283
|
return {
|
|
@@ -443,6 +315,8 @@ export async function adpUploadFileWithConfig(
|
|
|
443
315
|
/**
|
|
444
316
|
* 批量上传文件(使用显式传入的 token)
|
|
445
317
|
*
|
|
318
|
+
* 注意:每个文件需要独立获取预签名URL,因为服务端为每个文件生成唯一路径
|
|
319
|
+
*
|
|
446
320
|
* @param filePaths - 要上传的本地文件路径列表
|
|
447
321
|
* @param botToken - 机器人 token
|
|
448
322
|
* @param fileType - 文件类型(可选)
|
|
@@ -453,36 +327,10 @@ export async function adpUploadFiles(
|
|
|
453
327
|
botToken: string,
|
|
454
328
|
fileType?: string
|
|
455
329
|
): Promise<UploadResult[]> {
|
|
456
|
-
//
|
|
457
|
-
let credential: DescribeRemoteBotStorageCredentialRsp;
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
credential = await getStorageCredential(botToken, fileType);
|
|
461
|
-
} catch (error) {
|
|
462
|
-
// 如果获取密钥失败,返回所有文件都失败
|
|
463
|
-
const errorMsg = error instanceof Error ? error.message : "获取临时密钥失败";
|
|
464
|
-
return filePaths.map(() => ({
|
|
465
|
-
ok: false,
|
|
466
|
-
error: errorMsg,
|
|
467
|
-
}));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// 并发上传所有文件
|
|
330
|
+
// 逐个上传文件(每个文件需要独立的预签名URL)
|
|
471
331
|
const results = await Promise.all(
|
|
472
332
|
filePaths.map(async (filePath): Promise<UploadResult> => {
|
|
473
|
-
|
|
474
|
-
const cosKey = await uploadFileToCos(filePath, credential);
|
|
475
|
-
const signedUrl = getSignedDownloadUrl(cosKey, credential);
|
|
476
|
-
return {
|
|
477
|
-
ok: true,
|
|
478
|
-
fileUrl: signedUrl,
|
|
479
|
-
};
|
|
480
|
-
} catch (error) {
|
|
481
|
-
return {
|
|
482
|
-
ok: false,
|
|
483
|
-
error: error instanceof Error ? error.message : "上传失败",
|
|
484
|
-
};
|
|
485
|
-
}
|
|
333
|
+
return adpUploadFile(filePath, botToken, fileType);
|
|
486
334
|
})
|
|
487
335
|
);
|
|
488
336
|
|
|
@@ -795,6 +643,8 @@ export interface AdpUploadOptions {
|
|
|
795
643
|
|
|
796
644
|
/**
|
|
797
645
|
* 上传文件到 ADP 存储(返回兼容 kimi 格式的结果)
|
|
646
|
+
*
|
|
647
|
+
* 注意:每个文件需要独立获取预签名URL,因为服务端为每个文件生成唯一路径
|
|
798
648
|
*/
|
|
799
649
|
export const uploadFilesToAdpEndpoint = async (
|
|
800
650
|
paths: string[],
|
|
@@ -809,23 +659,6 @@ export const uploadFilesToAdpEndpoint = async (
|
|
|
809
659
|
};
|
|
810
660
|
}
|
|
811
661
|
|
|
812
|
-
// 获取临时密钥
|
|
813
|
-
let credential: DescribeRemoteBotStorageCredentialRsp;
|
|
814
|
-
try {
|
|
815
|
-
credential = await getStorageCredential(botToken, fileType);
|
|
816
|
-
} catch (error) {
|
|
817
|
-
return {
|
|
818
|
-
ok: false,
|
|
819
|
-
error: {
|
|
820
|
-
...INVALID_PARAMS,
|
|
821
|
-
data: {
|
|
822
|
-
field: "credential",
|
|
823
|
-
reason: error instanceof Error ? error.message : "failed to get storage credential",
|
|
824
|
-
},
|
|
825
|
-
},
|
|
826
|
-
};
|
|
827
|
-
}
|
|
828
|
-
|
|
829
662
|
// 验证所有本地文件
|
|
830
663
|
const validationResults = await runWithConcurrency(
|
|
831
664
|
paths,
|
|
@@ -853,19 +686,21 @@ export const uploadFilesToAdpEndpoint = async (
|
|
|
853
686
|
(r) => (r as ValidationSuccess<FileMetadata>).value
|
|
854
687
|
);
|
|
855
688
|
|
|
856
|
-
//
|
|
689
|
+
// 并发上传所有文件(每个文件独立获取预签名URL)
|
|
857
690
|
try {
|
|
858
691
|
const uploadResults = await runWithConcurrency(
|
|
859
692
|
fileMetadatas,
|
|
860
693
|
maxConcurrency,
|
|
861
694
|
async (metadata, _index): Promise<UploadedFileInfo> => {
|
|
862
|
-
|
|
863
|
-
const
|
|
695
|
+
// 为每个文件获取预签名URL
|
|
696
|
+
const credential = await getStorageCredential(botToken, fileType);
|
|
697
|
+
// 上传文件并获取下载URL
|
|
698
|
+
const downloadUrl = await uploadFileToCos(metadata.path, credential);
|
|
864
699
|
return {
|
|
865
|
-
uri:
|
|
700
|
+
uri: downloadUrl,
|
|
866
701
|
name: metadata.fileName,
|
|
867
702
|
mimeType: metadata.contentType,
|
|
868
|
-
downloadUrl:
|
|
703
|
+
downloadUrl: downloadUrl,
|
|
869
704
|
};
|
|
870
705
|
}
|
|
871
706
|
);
|