@dcrays/dcgchat-test 0.4.27 → 0.4.28

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.4.27",
3
+ "version": "0.4.28",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
package/src/channel.ts CHANGED
@@ -222,7 +222,7 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
222
222
  const notMessageId = `${msgCtx?.messageId}`?.length === 13 || !msgCtx?.messageId
223
223
  try {
224
224
  const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
225
- const url = opts.mediaUrl ? await ossUpload(opts.mediaUrl, botToken, 1) : ''
225
+ const url = mediaUrl ? await ossUpload(mediaUrl, botToken, 1) : ''
226
226
  if (!msgCtx.agentId) {
227
227
  msgCtx.agentId = agentId
228
228
  }
@@ -1,32 +1,89 @@
1
- import { createReadStream } from 'node:fs'
1
+ import { fileURLToPath } from 'node:url'
2
2
  // @ts-ignore
3
3
  import OSS from 'ali-oss'
4
4
  import { getStsToken, getUserToken } from './api.js'
5
5
  import { dcgLogger } from '../utils/log.js'
6
6
 
7
- /** File/路径/Buffer 转为 ali-oss put 所需的 Buffer 或 ReadableStream */
8
- async function toUploadContent(
9
- input: File | string | Buffer
10
- ): Promise<{ content: Buffer | ReturnType<typeof createReadStream>; fileName: string }> {
7
+ /** 分片大小:OSS 要求每片 ≥100 KB(最后一片可更小) */
8
+ const MULTIPART_PART_SIZE = 1024 * 1024
9
+
10
+ /** ali-oss 默认 timeout 60s,大文件单 PUT 或慢网易触发 ResponseTimeoutError */
11
+ const OSS_HTTP_TIMEOUT_MS = 10 * 60 * 1000
12
+
13
+ /** 归一化入参,避免 file://、包装对象、TypedArray 等导致 SDK 识别失败 */
14
+ function coerceOssFileInput(input: File | string | Buffer): File | string | Buffer {
15
+ if (typeof input === 'string') {
16
+ const t = input.trim()
17
+ if (t.startsWith('file:')) {
18
+ try {
19
+ return fileURLToPath(t)
20
+ } catch {
21
+ return input
22
+ }
23
+ }
24
+ return input
25
+ }
26
+ if (Buffer.isBuffer(input)) {
27
+ return input
28
+ }
29
+ if (input && typeof input === 'object') {
30
+ if (ArrayBuffer.isView(input) && !(input instanceof DataView) && !Buffer.isBuffer(input)) {
31
+ const v = input as ArrayBufferView
32
+ return Buffer.from(v.buffer, v.byteOffset, v.byteLength)
33
+ }
34
+ const o = input as unknown as Record<string, unknown>
35
+ const p = o.path ?? o.filePath
36
+ if (typeof p === 'string' && p.trim()) return p.trim()
37
+ }
38
+ return input
39
+ }
40
+
41
+ function resolveMime(input: File | string | Buffer): string {
42
+ if (typeof input !== 'string' && !Buffer.isBuffer(input) && input.type) {
43
+ return input.type
44
+ }
45
+ return 'application/octet-stream'
46
+ }
47
+
48
+ /**
49
+ * 将 File/路径/Buffer 转为 ali-oss 接受的类型。
50
+ * 本地路径保持为字符串:put 内部用 contentLength + ReadStream,大文件也稳定。
51
+ */
52
+ async function toUploadContent(input: File | string | Buffer): Promise<{ content: Buffer | string; fileName: string }> {
11
53
  if (Buffer.isBuffer(input)) {
12
54
  return { content: input, fileName: 'file' }
13
55
  }
14
56
  if (typeof input === 'string') {
15
57
  return {
16
- content: createReadStream(input),
17
- fileName: input.split('/').pop() ?? 'file'
58
+ content: input,
59
+ fileName: input.split(/[/\\]/).pop() ?? 'file'
18
60
  }
19
61
  }
20
- // File: ali-oss 需要 Buffer/Stream,用 arrayBuffer 转 Buffer
21
62
  const buf = Buffer.from(await input.arrayBuffer())
22
- return { content: buf, fileName: input.name }
63
+ const n = (input as { name?: string }).name
64
+ return { content: buf, fileName: typeof n === 'string' && n ? n : 'file' }
23
65
  }
24
66
 
25
- export const ossUpload = async (file: File | string | Buffer, botToken: string, isPrivate: 0 | 1 = 1) => {
67
+ export type OssUploadOptions = {
68
+ /** 分片上传进度,p 为 0~1(仅大 Buffer 分片时触发) */
69
+ onProgress?: (p: number) => void
70
+ /** HTTP 超时(毫秒),覆盖默认 15 分钟;可传 `30 * 60 * 1000` 等 */
71
+ timeoutMs?: number
72
+ }
73
+
74
+ export const ossUpload = async (
75
+ rawFile: File | string | Buffer,
76
+ botToken: string,
77
+ isPrivate: 0 | 1 = 1,
78
+ uploadOptions?: OssUploadOptions
79
+ ) => {
26
80
  await getUserToken(botToken)
27
81
 
82
+ const file = coerceOssFileInput(rawFile)
28
83
  const { content, fileName } = await toUploadContent(file)
29
84
  const data = await getStsToken(fileName, botToken, isPrivate)
85
+ const mime = resolveMime(file)
86
+ const onProgress = uploadOptions?.onProgress
30
87
 
31
88
  const options: OSS.Options = {
32
89
  // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
@@ -40,7 +97,8 @@ export const ossUpload = async (file: File | string | Buffer, botToken: string,
40
97
  region: data.region,
41
98
  secure: true,
42
99
  cname: true,
43
- authorizationV4: true
100
+ authorizationV4: true,
101
+ timeout: uploadOptions?.timeoutMs ?? OSS_HTTP_TIMEOUT_MS
44
102
  }
45
103
 
46
104
  const client = new OSS(options)
@@ -48,12 +106,22 @@ export const ossUpload = async (file: File | string | Buffer, botToken: string,
48
106
  const name = `${data.uploadDir}${data.ossFileKey}`
49
107
 
50
108
  try {
51
- const objectResult = await client.put(name, content)
109
+ let objectResult: OSS.PutObjectResult | OSS.CompleteMultipartUploadResult
110
+
111
+ const multipartUploadOptions: OSS.MultipartUploadOptions = {
112
+ progress: (p: number) => {
113
+ onProgress?.(p)
114
+ },
115
+ parallel: 4,
116
+ partSize: MULTIPART_PART_SIZE,
117
+ mime
118
+ }
119
+ objectResult = await client.multipartUpload(name, content, multipartUploadOptions)
120
+
52
121
  if (objectResult?.res?.status !== 200) {
53
122
  dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`)
54
123
  }
55
124
  dcgLogger(`OSS 上传成功, ${objectResult.name || objectResult.url}`)
56
- // const url = `${data.protocol || 'http'}://${data.bucket}.${data.endPoint}/${data.uploadDir}${data.ossFileKey}`
57
125
  return isPrivate === 1 ? objectResult.name || objectResult.url : objectResult.url
58
126
  } catch (error) {
59
127
  dcgLogger(`OSS 上传失败: ${error}`, 'error')