@dcrays/dcgchat-test 0.4.27 → 0.4.29
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 +1 -1
- package/src/channel.ts +9 -9
- package/src/request/oss.ts +166 -15
- package/src/tools/messageTool.ts +1 -1
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -192,6 +192,14 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
192
192
|
return
|
|
193
193
|
}
|
|
194
194
|
const mediaUrl = expanded[0]
|
|
195
|
+
|
|
196
|
+
const { sessionId, agentId } = getInfoBySessionKey(sessionKey)
|
|
197
|
+
if (!msgCtx.sessionId) {
|
|
198
|
+
msgCtx.sessionId = sessionId
|
|
199
|
+
}
|
|
200
|
+
if (!msgCtx.agentId) {
|
|
201
|
+
msgCtx.agentId = agentId
|
|
202
|
+
}
|
|
195
203
|
if (!mediaUrl || !msgCtx.sessionId) {
|
|
196
204
|
dcgLogger(`dcgchat: sendMedia skipped (duplicate in session): ${mediaUrl} sessionId=${msgCtx.sessionId} sessionKey=${sessionKey}`)
|
|
197
205
|
return
|
|
@@ -213,19 +221,11 @@ export async function sendDcgchatMedia(opts: DcgchatMediaSendOptions): Promise<v
|
|
|
213
221
|
}
|
|
214
222
|
addSentMediaKey(msgCtx.sessionId, mediaUrl)
|
|
215
223
|
}
|
|
216
|
-
|
|
217
|
-
const { sessionId, agentId } = getInfoBySessionKey(sessionKey)
|
|
218
|
-
if (!msgCtx.sessionId) {
|
|
219
|
-
msgCtx.sessionId = sessionId
|
|
220
|
-
}
|
|
221
224
|
const fileName = mediaUrl?.split(/[\\/]/).pop() || ''
|
|
222
225
|
const notMessageId = `${msgCtx?.messageId}`?.length === 13 || !msgCtx?.messageId
|
|
223
226
|
try {
|
|
224
227
|
const botToken = msgCtx.botToken ?? getOpenClawConfig()?.channels?.["dcgchat-test"]?.botToken ?? ''
|
|
225
|
-
const url =
|
|
226
|
-
if (!msgCtx.agentId) {
|
|
227
|
-
msgCtx.agentId = agentId
|
|
228
|
-
}
|
|
228
|
+
const url = mediaUrl ? await ossUpload(mediaUrl, botToken, 1) : ''
|
|
229
229
|
wsSendRaw(msgCtx, {
|
|
230
230
|
response: opts.text ?? '',
|
|
231
231
|
is_finish: notMessageId ? -1 : 0,
|
package/src/request/oss.ts
CHANGED
|
@@ -1,32 +1,166 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from 'node:fs/promises'
|
|
2
|
+
import { extname } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
2
4
|
// @ts-ignore
|
|
3
5
|
import OSS from 'ali-oss'
|
|
4
6
|
import { getStsToken, getUserToken } from './api.js'
|
|
5
7
|
import { dcgLogger } from '../utils/log.js'
|
|
6
8
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/** 仅对内存 Buffer 超过此大小使用分片(本地路径一律走 put,避免 multipart 对类型的限制) */
|
|
10
|
+
const MULTIPART_THRESHOLD_BYTES = 1024 * 1024
|
|
11
|
+
|
|
12
|
+
/** 分片大小:OSS 要求每片 ≥100 KB(最后一片可更小) */
|
|
13
|
+
const MULTIPART_PART_SIZE = 1024 * 1024
|
|
14
|
+
|
|
15
|
+
/** ali-oss 默认 timeout 为 60s,大文件单 PUT 或慢网易触发 ResponseTimeoutError */
|
|
16
|
+
const OSS_HTTP_TIMEOUT_MS = 15 * 60 * 1000
|
|
17
|
+
|
|
18
|
+
/** 归一化入参,避免 file://、包装对象、TypedArray 等导致 SDK 识别失败 */
|
|
19
|
+
function coerceOssFileInput(input: File | string | Buffer): File | string | Buffer {
|
|
20
|
+
if (typeof input === 'string') {
|
|
21
|
+
const t = input.trim()
|
|
22
|
+
if (t.startsWith('file:')) {
|
|
23
|
+
try {
|
|
24
|
+
return fileURLToPath(t)
|
|
25
|
+
} catch {
|
|
26
|
+
return input
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return input
|
|
30
|
+
}
|
|
31
|
+
if (Buffer.isBuffer(input)) {
|
|
32
|
+
return input
|
|
33
|
+
}
|
|
34
|
+
if (input && typeof input === 'object') {
|
|
35
|
+
if (ArrayBuffer.isView(input) && !(input instanceof DataView) && !Buffer.isBuffer(input)) {
|
|
36
|
+
const v = input as ArrayBufferView
|
|
37
|
+
return Buffer.from(v.buffer, v.byteOffset, v.byteLength)
|
|
38
|
+
}
|
|
39
|
+
const o = input as unknown as Record<string, unknown>
|
|
40
|
+
const p = o.path ?? o.filePath
|
|
41
|
+
if (typeof p === 'string' && p.trim()) return p.trim()
|
|
42
|
+
}
|
|
43
|
+
return input
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getUploadByteLength(input: File | string | Buffer): Promise<number> {
|
|
47
|
+
if (Buffer.isBuffer(input)) return input.length
|
|
48
|
+
if (typeof input === 'string') {
|
|
49
|
+
const s = await stat(input)
|
|
50
|
+
return s.size
|
|
51
|
+
}
|
|
52
|
+
return input.size
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 常见可在浏览器内联预览的类型(避免一律 application/octet-stream 触发下载) */
|
|
56
|
+
const PREVIEW_EXT_MIME: Record<string, string> = {
|
|
57
|
+
'.jpg': 'image/jpeg',
|
|
58
|
+
'.jpeg': 'image/jpeg',
|
|
59
|
+
'.png': 'image/png',
|
|
60
|
+
'.gif': 'image/gif',
|
|
61
|
+
'.webp': 'image/webp',
|
|
62
|
+
'.bmp': 'image/bmp',
|
|
63
|
+
'.svg': 'image/svg+xml',
|
|
64
|
+
'.ico': 'image/x-icon',
|
|
65
|
+
'.avif': 'image/avif',
|
|
66
|
+
'.heic': 'image/heic',
|
|
67
|
+
'.heif': 'image/heif',
|
|
68
|
+
'.pdf': 'application/pdf',
|
|
69
|
+
'.mp4': 'video/mp4',
|
|
70
|
+
'.webm': 'video/webm',
|
|
71
|
+
'.mov': 'video/quicktime',
|
|
72
|
+
'.mp3': 'audio/mpeg',
|
|
73
|
+
'.wav': 'audio/wav',
|
|
74
|
+
'.ogg': 'audio/ogg',
|
|
75
|
+
'.opus': 'audio/opus',
|
|
76
|
+
'.m4a': 'audio/mp4',
|
|
77
|
+
'.aac': 'audio/aac',
|
|
78
|
+
'.flac': 'audio/flac',
|
|
79
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
80
|
+
'.log': 'text/plain; charset=utf-8',
|
|
81
|
+
'.csv': 'text/csv; charset=utf-8',
|
|
82
|
+
'.html': 'text/html; charset=utf-8',
|
|
83
|
+
'.htm': 'text/html; charset=utf-8',
|
|
84
|
+
'.css': 'text/css; charset=utf-8',
|
|
85
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
86
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
87
|
+
'.json': 'application/json; charset=utf-8',
|
|
88
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
89
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
90
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
91
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
92
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function mimeFromPathOrName(pathOrName: string): string | undefined {
|
|
96
|
+
const base = pathOrName.split(/[/\\]/).pop() ?? pathOrName
|
|
97
|
+
const ext = extname(base).toLowerCase()
|
|
98
|
+
return ext ? PREVIEW_EXT_MIME[ext] : undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 解析上传 Content-Type,并配合 Content-Disposition: inline 便于直链预览 */
|
|
102
|
+
function resolveMime(input: File | string | Buffer, fileNameHint?: string): string {
|
|
103
|
+
if (typeof input === 'string') {
|
|
104
|
+
return mimeFromPathOrName(input) ?? 'application/octet-stream'
|
|
105
|
+
}
|
|
106
|
+
if (Buffer.isBuffer(input)) {
|
|
107
|
+
if (fileNameHint) {
|
|
108
|
+
const fromName = mimeFromPathOrName(fileNameHint)
|
|
109
|
+
if (fromName) return fromName
|
|
110
|
+
}
|
|
111
|
+
return 'application/octet-stream'
|
|
112
|
+
}
|
|
113
|
+
const declared = input.type?.trim()
|
|
114
|
+
if (declared && declared !== 'application/octet-stream') {
|
|
115
|
+
return declared
|
|
116
|
+
}
|
|
117
|
+
const name = typeof input.name === 'string' && input.name ? input.name : ''
|
|
118
|
+
if (name) {
|
|
119
|
+
const fromName = mimeFromPathOrName(name)
|
|
120
|
+
if (fromName) return fromName
|
|
121
|
+
}
|
|
122
|
+
return declared || 'application/octet-stream'
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 将 File/路径/Buffer 转为 ali-oss 接受的类型。
|
|
127
|
+
* 本地路径保持为字符串:put 内部用 contentLength + ReadStream,大文件也稳定。
|
|
128
|
+
*/
|
|
129
|
+
async function toUploadContent(input: File | string | Buffer): Promise<{ content: Buffer | string; fileName: string }> {
|
|
11
130
|
if (Buffer.isBuffer(input)) {
|
|
12
131
|
return { content: input, fileName: 'file' }
|
|
13
132
|
}
|
|
14
133
|
if (typeof input === 'string') {
|
|
15
134
|
return {
|
|
16
|
-
content:
|
|
17
|
-
fileName: input.split(
|
|
135
|
+
content: input,
|
|
136
|
+
fileName: input.split(/[/\\]/).pop() ?? 'file'
|
|
18
137
|
}
|
|
19
138
|
}
|
|
20
|
-
// File: ali-oss 需要 Buffer/Stream,用 arrayBuffer 转 Buffer
|
|
21
139
|
const buf = Buffer.from(await input.arrayBuffer())
|
|
22
|
-
|
|
140
|
+
const n = (input as { name?: string }).name
|
|
141
|
+
return { content: buf, fileName: typeof n === 'string' && n ? n : 'file' }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type OssUploadOptions = {
|
|
145
|
+
/** 分片上传进度,p 为 0~1(仅大 Buffer 分片时触发) */
|
|
146
|
+
onProgress?: (p: number) => void
|
|
147
|
+
/** HTTP 超时(毫秒),覆盖默认 15 分钟;可传 `30 * 60 * 1000` 等 */
|
|
148
|
+
timeoutMs?: number
|
|
23
149
|
}
|
|
24
150
|
|
|
25
|
-
export const ossUpload = async (
|
|
151
|
+
export const ossUpload = async (
|
|
152
|
+
rawFile: File | string | Buffer,
|
|
153
|
+
botToken: string,
|
|
154
|
+
isPrivate: 0 | 1 = 1,
|
|
155
|
+
uploadOptions?: OssUploadOptions
|
|
156
|
+
) => {
|
|
26
157
|
await getUserToken(botToken)
|
|
27
158
|
|
|
159
|
+
const file = coerceOssFileInput(rawFile)
|
|
28
160
|
const { content, fileName } = await toUploadContent(file)
|
|
29
161
|
const data = await getStsToken(fileName, botToken, isPrivate)
|
|
162
|
+
const mime = resolveMime(file, fileName)
|
|
163
|
+
const onProgress = uploadOptions?.onProgress
|
|
30
164
|
|
|
31
165
|
const options: OSS.Options = {
|
|
32
166
|
// 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。
|
|
@@ -40,7 +174,8 @@ export const ossUpload = async (file: File | string | Buffer, botToken: string,
|
|
|
40
174
|
region: data.region,
|
|
41
175
|
secure: true,
|
|
42
176
|
cname: true,
|
|
43
|
-
authorizationV4: true
|
|
177
|
+
authorizationV4: true,
|
|
178
|
+
timeout: uploadOptions?.timeoutMs ?? OSS_HTTP_TIMEOUT_MS
|
|
44
179
|
}
|
|
45
180
|
|
|
46
181
|
const client = new OSS(options)
|
|
@@ -48,13 +183,29 @@ export const ossUpload = async (file: File | string | Buffer, botToken: string,
|
|
|
48
183
|
const name = `${data.uploadDir}${data.ossFileKey}`
|
|
49
184
|
|
|
50
185
|
try {
|
|
51
|
-
|
|
186
|
+
let objectResult: OSS.PutObjectResult | OSS.CompleteMultipartUploadResult
|
|
187
|
+
|
|
188
|
+
const multipartUploadOptions: OSS.MultipartUploadOptions = {
|
|
189
|
+
progress: (p: number) => {
|
|
190
|
+
onProgress?.(p)
|
|
191
|
+
},
|
|
192
|
+
parallel: 4,
|
|
193
|
+
partSize: MULTIPART_PART_SIZE,
|
|
194
|
+
mime,
|
|
195
|
+
/** 直链打开时优先内联展示,而非附件下载 */
|
|
196
|
+
headers: {
|
|
197
|
+
'Content-Disposition': 'inline'
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
objectResult = await client.multipartUpload(name, content, multipartUploadOptions)
|
|
201
|
+
|
|
52
202
|
if (objectResult?.res?.status !== 200) {
|
|
53
203
|
dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`)
|
|
54
204
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
205
|
+
const requestUrls = objectResult?.res?.requestUrls || []
|
|
206
|
+
const url = requestUrls[0] || ''
|
|
207
|
+
dcgLogger(`OSS 上传成功, ${isPrivate === 1 ? objectResult.name || url : url}`)
|
|
208
|
+
return isPrivate === 1 ? objectResult.name || url : url
|
|
58
209
|
} catch (error) {
|
|
59
210
|
dcgLogger(`OSS 上传失败: ${error}`, 'error')
|
|
60
211
|
}
|
package/src/tools/messageTool.ts
CHANGED