koishi-plugin-chat-patch 5.6.0 → 6.0.0
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/client/web/dist/assets/Chat-BkRnAj_r.js +28 -0
- package/client/web/dist/assets/{Chat-BXVWj5Bd.css → Chat-D4tCqRrO.css} +1 -1
- package/client/web/dist/assets/{MsgBody-CzrxyiFO.js → MsgBody-BaoDyXUz.js} +1 -1
- package/client/web/dist/assets/{MsgBody.vue_vue_type_script_setup_true_lang-BBF1uxm6.js → MsgBody.vue_vue_type_script_setup_true_lang-CgpEB8mj.js} +1 -1
- package/client/web/dist/assets/{index-EJ2CcODr.js → index--7-eCx0r.js} +57 -57
- package/client/web/dist/index.html +1 -1
- package/client/web/src/function/connect.ts +5 -12
- package/client/web/src/function/satori.ts +18 -12
- package/client/web/src/function/utils/appUtil.ts +27 -6
- package/client/web/src/pages/Chat.vue +32 -24
- package/lib/database.d.ts +5 -3
- package/lib/gateway.d.ts +2 -4
- package/lib/index.d.ts +17 -0
- package/lib/index.js +5359 -452
- package/lib/media.d.ts +1 -1
- package/lib/recorder.d.ts +1 -4
- package/lib/self-message.d.ts +1 -4
- package/package.json +25 -10
- package/src/bootstrap.ts +3 -2
- package/src/config.ts +1 -5
- package/src/database.ts +297 -131
- package/src/gateway.ts +21 -13
- package/src/index.ts +59 -54
- package/src/media.ts +43 -12
- package/src/recorder.ts +0 -11
- package/src/satori.ts +8 -5
- package/src/self-message.ts +3 -13
- package/src/server.d.ts +24 -9
- package/src/web.ts +144 -78
- package/client/web/dist/assets/Chat-m4-U5uHa.js +0 -28
- package/lib/bootstrap.d.ts +0 -6
- package/lib/satori.d.ts +0 -3
- package/lib/types.d.ts +0 -138
- package/lib/web.d.ts +0 -7
package/src/web.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Context } from 'koishi'
|
|
2
2
|
import {} from '@koishijs/plugin-server'
|
|
3
|
-
import { createReadStream, existsSync, promises as fs, statSync } from 'node:fs'
|
|
3
|
+
import { createReadStream, createWriteStream, existsSync, promises as fs, statSync } from 'node:fs'
|
|
4
4
|
import { createHash } from 'node:crypto'
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import { pathToFileURL } from 'node:url'
|
|
7
|
-
import send from 'koa-send'
|
|
8
|
-
import type
|
|
7
|
+
import send from 'koa-send'
|
|
8
|
+
import type Router from '@koa/router'
|
|
9
|
+
import type { DefaultContext, DefaultState, ParameterizedContext } from 'koa'
|
|
9
10
|
import FileType from 'file-type'
|
|
10
11
|
|
|
11
12
|
import { Config } from './config'
|
|
@@ -13,7 +14,7 @@ import { ContactCacheService } from './cache'
|
|
|
13
14
|
import { ChatDatabase } from './database'
|
|
14
15
|
import { MediaManager } from './media'
|
|
15
16
|
import { PluginLogger } from './logger'
|
|
16
|
-
import { ContactCacheItem, SelfMessagePayload, SelfMessageRecord } from './types'
|
|
17
|
+
import { ContactCacheItem, MessageRecord, SelfMessagePayload, SelfMessageRecord } from './types'
|
|
17
18
|
|
|
18
19
|
interface ViteConsoleServer {
|
|
19
20
|
config?: {
|
|
@@ -42,7 +43,7 @@ export function registerWeb(
|
|
|
42
43
|
const webIndex = path.resolve(__dirname, '..', 'client', 'web', 'index.html')
|
|
43
44
|
const uploadDir = path.resolve(ctx.baseDir, 'data', 'chat-patch', 'upload-media')
|
|
44
45
|
|
|
45
|
-
type WebContext =
|
|
46
|
+
type WebContext = Router.RouterContext<DefaultState, DefaultContext>
|
|
46
47
|
const cacheType = (type: string) => type === 'user' ? 'friend' : type
|
|
47
48
|
|
|
48
49
|
const getVite = (): ViteConsoleServer | undefined => {
|
|
@@ -133,16 +134,21 @@ export function registerWeb(
|
|
|
133
134
|
}
|
|
134
135
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024
|
|
135
136
|
|
|
136
|
-
ctx.server.post(`${config.basePath}/api/upload-media`, async (koa) => {
|
|
137
|
-
const requestBody = (koa.request as unknown as { body?: unknown }).body
|
|
138
|
-
const body = (requestBody ?? {}) as Record<string, unknown>
|
|
139
|
-
let source = String(body.dataUrl ?? body.data ?? '')
|
|
140
|
-
let name = String(body.name ?? '')
|
|
141
|
-
let mime = ''
|
|
142
|
-
let buffer: Buffer | null = null
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
ctx.server.post(`${config.basePath}/api/upload-media`, async (koa) => {
|
|
138
|
+
const requestBody = (koa.request as unknown as { body?: unknown }).body
|
|
139
|
+
const body = (requestBody ?? {}) as Record<string, unknown>
|
|
140
|
+
let source = String(body.dataUrl ?? body.data ?? '')
|
|
141
|
+
let name = String(body.name ?? '')
|
|
142
|
+
let mime = ''
|
|
143
|
+
let buffer: Buffer | null = null
|
|
144
|
+
let tempPath = ''
|
|
145
|
+
const rawHash = createHash('md5')
|
|
146
|
+
const cleanupTemp = async () => {
|
|
147
|
+
if (tempPath) await fs.unlink(tempPath).catch(() => undefined)
|
|
148
|
+
}
|
|
149
|
+
if (source) {
|
|
150
|
+
if (source.startsWith('base64://')) source = source.slice(9)
|
|
151
|
+
const comma = source.indexOf('base64,')
|
|
146
152
|
const base64 = comma >= 0 ? source.slice(comma + 7) : source
|
|
147
153
|
const normalizedBase64 = base64.replace(/-/g, '+').replace(/_/g, '/')
|
|
148
154
|
try {
|
|
@@ -152,60 +158,86 @@ export function registerWeb(
|
|
|
152
158
|
koa.body = { error: 'invalid media data' }
|
|
153
159
|
return
|
|
154
160
|
}
|
|
155
|
-
mime = source.startsWith('data:')
|
|
156
|
-
? source.slice(5, source.indexOf(';')).toLowerCase()
|
|
157
|
-
: ''
|
|
158
|
-
} else {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
161
|
+
mime = source.startsWith('data:')
|
|
162
|
+
? source.slice(5, source.indexOf(';')).toLowerCase()
|
|
163
|
+
: ''
|
|
164
|
+
} else {
|
|
165
|
+
tempPath = path.join(uploadDir, `.upload-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`)
|
|
166
|
+
const write = createWriteStream(tempPath)
|
|
167
|
+
try {
|
|
168
|
+
for await (const chunk of koa.req as AsyncIterable<Buffer | string>) {
|
|
169
|
+
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
170
|
+
rawHash.update(part)
|
|
171
|
+
if (!write.write(part)) {
|
|
172
|
+
await new Promise<void>((resolve) => write.once('drain', resolve))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await new Promise<void>((resolve, reject) => {
|
|
176
|
+
write.once('error', reject)
|
|
177
|
+
write.end(() => resolve())
|
|
178
|
+
})
|
|
179
|
+
} catch (error) {
|
|
180
|
+
write.destroy()
|
|
181
|
+
await cleanupTemp()
|
|
182
|
+
throw error
|
|
183
|
+
}
|
|
184
|
+
name = String(koa.query.name ?? koa.get('x-file-name') ?? '')
|
|
185
|
+
try {
|
|
186
|
+
name = decodeURIComponent(name)
|
|
167
187
|
} catch {
|
|
168
188
|
// 保持原始名称即可
|
|
169
189
|
}
|
|
170
|
-
mime = String(koa.get('content-type') ?? '').split(';')[0].trim().toLowerCase()
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
detected = result ?? undefined
|
|
193
|
-
} catch {
|
|
194
|
-
// 部分音频/文件无法识别时继续用 MIME 或文件名兜底
|
|
195
|
-
detected = undefined
|
|
190
|
+
mime = String(koa.get('content-type') ?? '').split(';')[0].trim().toLowerCase()
|
|
191
|
+
}
|
|
192
|
+
const fileSize = buffer ? buffer.length : existsSync(tempPath) ? statSync(tempPath).size : 0
|
|
193
|
+
if (!fileSize) {
|
|
194
|
+
await cleanupTemp()
|
|
195
|
+
koa.status = 400
|
|
196
|
+
koa.body = { error: 'missing media data' }
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
if (fileSize > MAX_UPLOAD_BYTES) {
|
|
200
|
+
await cleanupTemp()
|
|
201
|
+
koa.status = 413
|
|
202
|
+
koa.body = { error: 'media file too large' }
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await fs.mkdir(uploadDir, { recursive: true })
|
|
207
|
+
let detected: { ext?: string } | undefined
|
|
208
|
+
try {
|
|
209
|
+
const result = buffer
|
|
210
|
+
? await FileType.fromBuffer(buffer)
|
|
211
|
+
: await FileType.fromStream(createReadStream(tempPath))
|
|
212
|
+
detected = result ?? undefined
|
|
213
|
+
} catch {
|
|
214
|
+
// 部分音频/文件无法识别时继续用 MIME 或文件名兜底
|
|
215
|
+
detected = undefined
|
|
196
216
|
}
|
|
197
217
|
const mimeExt = mimeToExt[mime] || ''
|
|
198
218
|
const nameExt = path.extname(name).toLowerCase()
|
|
199
|
-
const ext = detected?.ext
|
|
200
|
-
? toExtension(detected.ext)
|
|
201
|
-
: mimeExt || nameExt || '.bin'
|
|
202
|
-
const
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
219
|
+
const ext = detected?.ext
|
|
220
|
+
? toExtension(detected.ext)
|
|
221
|
+
: mimeExt || nameExt || '.bin'
|
|
222
|
+
const digest = buffer ? createHash('md5').update(buffer).digest('hex') : rawHash.digest('hex')
|
|
223
|
+
const filename = `${digest}${ext}`
|
|
224
|
+
const filePath = path.join(uploadDir, filename)
|
|
225
|
+
try {
|
|
226
|
+
if (!existsSync(filePath)) {
|
|
227
|
+
if (buffer) {
|
|
228
|
+
await fs.writeFile(filePath, buffer)
|
|
229
|
+
} else {
|
|
230
|
+
await fs.rename(tempPath, filePath)
|
|
231
|
+
tempPath = ''
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
await cleanupTemp()
|
|
236
|
+
throw error
|
|
237
|
+
}
|
|
238
|
+
await cleanupTemp()
|
|
239
|
+
koa.body = {
|
|
240
|
+
path: pathToFileURL(filePath).href,
|
|
209
241
|
localPath: filePath,
|
|
210
242
|
}
|
|
211
243
|
})
|
|
@@ -303,8 +335,12 @@ export function registerWeb(
|
|
|
303
335
|
koa.body = { error: 'missing history params' }
|
|
304
336
|
return
|
|
305
337
|
}
|
|
306
|
-
const
|
|
307
|
-
const
|
|
338
|
+
const defaultLimit = Math.min(config.maxMessagesPerChannel, 100)
|
|
339
|
+
const parsedLimit = Number(koa.query.limit ?? defaultLimit)
|
|
340
|
+
const limit = Math.min(
|
|
341
|
+
Math.max(1, Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.floor(parsedLimit) : defaultLimit),
|
|
342
|
+
config.maxMessagesPerChannel,
|
|
343
|
+
)
|
|
308
344
|
const beforeTime = Number(koa.query.beforeTime ?? 0)
|
|
309
345
|
const queryMessages = async (cid: string) => {
|
|
310
346
|
return Number.isFinite(beforeTime) && beforeTime > 0
|
|
@@ -331,10 +367,35 @@ export function registerWeb(
|
|
|
331
367
|
const timeA = Number(a?.receivedAt ?? a?.timestampMs ?? a?.timestamp ?? 0)
|
|
332
368
|
const timeB = Number(b?.receivedAt ?? b?.timestampMs ?? b?.timestamp ?? 0)
|
|
333
369
|
return timeA - timeB
|
|
334
|
-
})
|
|
335
|
-
selfMessages.sort((a, b) => a.sentAt - b.sentAt)
|
|
336
|
-
|
|
337
|
-
|
|
370
|
+
})
|
|
371
|
+
selfMessages.sort((a, b) => a.sentAt - b.sentAt)
|
|
372
|
+
// 合并两条记录流后只返回 limit 条,保证前端分页/结束判断不会因自消息被放大
|
|
373
|
+
const combined: Array<{
|
|
374
|
+
kind: 'message' | 'self'
|
|
375
|
+
time: number
|
|
376
|
+
record: MessageRecord | SelfMessageRecord
|
|
377
|
+
}> = []
|
|
378
|
+
for (const item of messages) {
|
|
379
|
+
combined.push({
|
|
380
|
+
kind: 'message',
|
|
381
|
+
time: Number(item?.receivedAt ?? item?.timestampMs ?? item?.timestamp ?? 0),
|
|
382
|
+
record: item,
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
for (const item of selfMessages) {
|
|
386
|
+
combined.push({ kind: 'self', time: item.sentAt, record: item })
|
|
387
|
+
}
|
|
388
|
+
combined.sort((a, b) => a.time - b.time)
|
|
389
|
+
const capped = combined.slice(-limit)
|
|
390
|
+
koa.body = {
|
|
391
|
+
messages: capped
|
|
392
|
+
.filter((item) => item.kind === 'message')
|
|
393
|
+
.map((item) => item.record as MessageRecord),
|
|
394
|
+
selfMessages: capped
|
|
395
|
+
.filter((item) => item.kind === 'self')
|
|
396
|
+
.map((item) => item.record as SelfMessageRecord),
|
|
397
|
+
}
|
|
398
|
+
})
|
|
338
399
|
|
|
339
400
|
ctx.server.get(`${config.basePath}/api/self-messages`, async (koa) => {
|
|
340
401
|
const platform = String(koa.query.platform ?? '')
|
|
@@ -685,15 +746,20 @@ export function registerWeb(
|
|
|
685
746
|
})
|
|
686
747
|
|
|
687
748
|
const serveFile = async (koa: WebContext, fileName: string) => {
|
|
688
|
-
const fullPath = path.join(webRoot, fileName)
|
|
689
|
-
if (existsSync(fullPath) && statSync(fullPath).isFile()) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
koa.
|
|
695
|
-
|
|
696
|
-
|
|
749
|
+
const fullPath = path.join(webRoot, fileName)
|
|
750
|
+
if (existsSync(fullPath) && statSync(fullPath).isFile()) {
|
|
751
|
+
const rootContext = koa as unknown as ParameterizedContext<DefaultState, DefaultContext>
|
|
752
|
+
await send(rootContext, path.relative(webRoot, fullPath), { root: webRoot })
|
|
753
|
+
return
|
|
754
|
+
}
|
|
755
|
+
koa.status = 200
|
|
756
|
+
koa.body = ''
|
|
757
|
+
await send(
|
|
758
|
+
koa as unknown as ParameterizedContext<DefaultState, DefaultContext>,
|
|
759
|
+
'index.html',
|
|
760
|
+
{ root: webRoot },
|
|
761
|
+
)
|
|
762
|
+
}
|
|
697
763
|
|
|
698
764
|
const serveDevWeb = async (koa: WebContext): Promise<boolean> => {
|
|
699
765
|
const vite = getVite()
|