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 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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adp-openclaw",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "ADP-OpenClaw demo channel plugin (Go WebSocket backend)",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -2,9 +2,9 @@
2
2
  * ADP Upload Tool
3
3
  *
4
4
  * 功能:
5
- * 1. 通过固定接口获取腾讯云COS临时密钥
6
- * 2. 使用密钥上传文件到COS
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; // 上传url
42
- file_url: string; // 下载url
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.credentials?.tmp_secret_id || !result.credentials?.tmp_secret_key) {
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. 构建COS上传URL
294
- const { bucket, region, file_path, credentials } = credential;
295
- const cosKey = `${file_path}${fileMetadata.fileName}`;
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
- // 5. 执行上传
218
+ // 4. 执行上传(直接PUT到预签名URL,不需要额外签名)
316
219
  const response = await fetchWithTimeout(
317
220
  uploadUrl,
318
221
  {
319
222
  method: "PUT",
320
223
  headers: {
321
- ...headers,
322
- Authorization: signature,
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
- return cosKey;
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 cosKey = await uploadFileToCos(filePath, credential);
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: signedUrl,
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
- try {
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
- const cosKey = await uploadFileToCos(metadata.path, credential);
863
- const signedUrl = getSignedDownloadUrl(cosKey, credential);
695
+ // 为每个文件获取预签名URL
696
+ const credential = await getStorageCredential(botToken, fileType);
697
+ // 上传文件并获取下载URL
698
+ const downloadUrl = await uploadFileToCos(metadata.path, credential);
864
699
  return {
865
- uri: `adp-file://${cosKey}`,
700
+ uri: downloadUrl,
866
701
  name: metadata.fileName,
867
702
  mimeType: metadata.contentType,
868
- downloadUrl: signedUrl,
703
+ downloadUrl: downloadUrl,
869
704
  };
870
705
  }
871
706
  );