@dcrays/dcgchat-test 0.4.28 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcrays/dcgchat-test",
3
- "version": "0.4.28",
3
+ "version": "0.4.29",
4
4
  "type": "module",
5
5
  "description": "OpenClaw channel plugin for 书灵墨宝 (WebSocket)",
6
6
  "main": "index.ts",
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
228
  const url = mediaUrl ? await ossUpload(mediaUrl, botToken, 1) : ''
226
- if (!msgCtx.agentId) {
227
- msgCtx.agentId = agentId
228
- }
229
229
  wsSendRaw(msgCtx, {
230
230
  response: opts.text ?? '',
231
231
  is_finish: notMessageId ? -1 : 0,
@@ -1,14 +1,19 @@
1
+ import { stat } from 'node:fs/promises'
2
+ import { extname } from 'node:path'
1
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
 
9
+ /** 仅对内存 Buffer 超过此大小使用分片(本地路径一律走 put,避免 multipart 对类型的限制) */
10
+ const MULTIPART_THRESHOLD_BYTES = 1024 * 1024
11
+
7
12
  /** 分片大小:OSS 要求每片 ≥100 KB(最后一片可更小) */
8
13
  const MULTIPART_PART_SIZE = 1024 * 1024
9
14
 
10
15
  /** ali-oss 默认 timeout 为 60s,大文件单 PUT 或慢网易触发 ResponseTimeoutError */
11
- const OSS_HTTP_TIMEOUT_MS = 10 * 60 * 1000
16
+ const OSS_HTTP_TIMEOUT_MS = 15 * 60 * 1000
12
17
 
13
18
  /** 归一化入参,避免 file://、包装对象、TypedArray 等导致 SDK 识别失败 */
14
19
  function coerceOssFileInput(input: File | string | Buffer): File | string | Buffer {
@@ -38,11 +43,83 @@ function coerceOssFileInput(input: File | string | Buffer): File | string | Buff
38
43
  return input
39
44
  }
40
45
 
41
- function resolveMime(input: File | string | Buffer): string {
42
- if (typeof input !== 'string' && !Buffer.isBuffer(input) && input.type) {
43
- return input.type
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
44
116
  }
45
- return 'application/octet-stream'
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'
46
123
  }
47
124
 
48
125
  /**
@@ -82,7 +159,7 @@ export const ossUpload = async (
82
159
  const file = coerceOssFileInput(rawFile)
83
160
  const { content, fileName } = await toUploadContent(file)
84
161
  const data = await getStsToken(fileName, botToken, isPrivate)
85
- const mime = resolveMime(file)
162
+ const mime = resolveMime(file, fileName)
86
163
  const onProgress = uploadOptions?.onProgress
87
164
 
88
165
  const options: OSS.Options = {
@@ -114,15 +191,21 @@ export const ossUpload = async (
114
191
  },
115
192
  parallel: 4,
116
193
  partSize: MULTIPART_PART_SIZE,
117
- mime
194
+ mime,
195
+ /** 直链打开时优先内联展示,而非附件下载 */
196
+ headers: {
197
+ 'Content-Disposition': 'inline'
198
+ }
118
199
  }
119
200
  objectResult = await client.multipartUpload(name, content, multipartUploadOptions)
120
201
 
121
202
  if (objectResult?.res?.status !== 200) {
122
203
  dcgLogger(`OSS 上传失败, ${objectResult?.res?.status}`)
123
204
  }
124
- dcgLogger(`OSS 上传成功, ${objectResult.name || objectResult.url}`)
125
- return isPrivate === 1 ? objectResult.name || objectResult.url : objectResult.url
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
126
209
  } catch (error) {
127
210
  dcgLogger(`OSS 上传失败: ${error}`, 'error')
128
211
  }
@@ -82,7 +82,7 @@ const messageToolParameters = {
82
82
  }
83
83
  }
84
84
  },
85
- oneOf: [{ required: ['content'] }, { required: ['media'] }]
85
+ anyOf: [{ required: ['content'] }, { required: ['media'] }]
86
86
  }
87
87
 
88
88
  /** 从正文提取可发送的文件路径(固定挂载 + 当前工作区前缀)。 */