koishi-plugin-chat-patch 5.6.1 → 6.1.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-BLcuHxzh.js +28 -0
- package/client/web/dist/assets/{Chat-BXVWj5Bd.css → Chat-Cee07eDL.css} +1 -1
- package/client/web/dist/assets/{MsgBody-CzrxyiFO.js → MsgBody-krZlLzRL.js} +1 -1
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CLmUjKws.js +67 -0
- package/client/web/dist/assets/{index-EJ2CcODr.js → index-DOJoilEy.js} +57 -60
- package/client/web/dist/index.html +1 -1
- package/client/web/src/assets/l10n/zh-CN.po +0 -3
- package/client/web/src/components/MsgBody.vue +29 -20
- package/client/web/src/function/connect.ts +17 -18
- package/client/web/src/function/msg.ts +2 -0
- package/client/web/src/function/satori-model.ts +2 -0
- package/client/web/src/function/satori.ts +18 -12
- package/client/web/src/function/utils/appUtil.ts +27 -6
- package/client/web/src/function/utils/msgUtil.ts +27 -21
- package/client/web/src/pages/Chat.vue +35 -32
- 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 +5409 -473
- package/lib/media.d.ts +1 -1
- package/lib/recorder.d.ts +1 -4
- package/lib/self-message.d.ts +1 -4
- package/lib/types.d.ts +4 -0
- package/package.json +25 -10
- package/src/bootstrap.ts +3 -2
- package/src/config.ts +1 -5
- package/src/database.ts +313 -139
- package/src/gateway.ts +21 -13
- package/src/index.ts +59 -54
- package/src/media.ts +43 -12
- package/src/recorder.ts +2 -12
- package/src/self-message.ts +7 -14
- package/src/server.d.ts +17 -3
- package/src/types.ts +4 -0
- package/src/web.ts +245 -151
- package/client/web/dist/assets/Chat-m4-U5uHa.js +0 -28
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-BBF1uxm6.js +0 -67
- package/lib/bootstrap.d.ts +0 -6
- package/lib/satori.d.ts +0 -3
- 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?: {
|
|
@@ -39,10 +40,11 @@ export function registerWeb(
|
|
|
39
40
|
const webRoot = path.resolve(__dirname, '..', 'client', 'web', 'dist')
|
|
40
41
|
const webSource = path.resolve(__dirname, '..', 'client', 'web', 'src')
|
|
41
42
|
const webPublic = path.resolve(__dirname, '..', 'client', 'web', 'public')
|
|
42
|
-
const webIndex = path.resolve(__dirname, '..', 'client', 'web', 'index.html')
|
|
43
|
-
const uploadDir = path.resolve(ctx.baseDir, 'data', 'chat-patch', 'upload-media')
|
|
43
|
+
const webIndex = path.resolve(__dirname, '..', 'client', 'web', 'index.html')
|
|
44
|
+
const uploadDir = path.resolve(ctx.baseDir, 'data', 'chat-patch', 'upload-media')
|
|
45
|
+
const mediaDir = path.resolve(ctx.baseDir, 'data', 'chat-patch', 'media')
|
|
44
46
|
|
|
45
|
-
type WebContext =
|
|
47
|
+
type WebContext = Router.RouterContext<DefaultState, DefaultContext>
|
|
46
48
|
const cacheType = (type: string) => type === 'user' ? 'friend' : type
|
|
47
49
|
|
|
48
50
|
const getVite = (): ViteConsoleServer | undefined => {
|
|
@@ -105,6 +107,22 @@ export function registerWeb(
|
|
|
105
107
|
const historyChannelCandidates = (channelId: string): string[] => {
|
|
106
108
|
return channelId ? [channelId] : []
|
|
107
109
|
}
|
|
110
|
+
|
|
111
|
+
const messageSortTime = (item: MessageRecord): number => {
|
|
112
|
+
const timestampMs = Number(item.timestampMs ?? 0)
|
|
113
|
+
if (timestampMs > 0) return timestampMs > 1e12 ? timestampMs : timestampMs * 1000
|
|
114
|
+
const timestamp = Number(item.timestamp ?? 0)
|
|
115
|
+
if (timestamp > 0) return timestamp * 1000
|
|
116
|
+
return Number(item.receivedAt ?? 0)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const selfMessageSortTime = (item: SelfMessageRecord): number => {
|
|
120
|
+
const timestampMs = Number(item.timestampMs ?? item.sentAt ?? 0)
|
|
121
|
+
if (timestampMs > 0) return timestampMs > 1e12 ? timestampMs : timestampMs * 1000
|
|
122
|
+
const timestamp = Number(item.timestamp ?? 0)
|
|
123
|
+
if (timestamp > 0) return timestamp * 1000
|
|
124
|
+
return item.sentAt
|
|
125
|
+
}
|
|
108
126
|
|
|
109
127
|
const mimeToExt: Record<string, string> = {
|
|
110
128
|
'audio/mpeg': '.mp3',
|
|
@@ -133,16 +151,21 @@ export function registerWeb(
|
|
|
133
151
|
}
|
|
134
152
|
const MAX_UPLOAD_BYTES = 500 * 1024 * 1024
|
|
135
153
|
|
|
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
|
-
|
|
154
|
+
ctx.server.post(`${config.basePath}/api/upload-media`, async (koa) => {
|
|
155
|
+
const requestBody = (koa.request as unknown as { body?: unknown }).body
|
|
156
|
+
const body = (requestBody ?? {}) as Record<string, unknown>
|
|
157
|
+
let source = String(body.dataUrl ?? body.data ?? '')
|
|
158
|
+
let name = String(body.name ?? '')
|
|
159
|
+
let mime = ''
|
|
160
|
+
let buffer: Buffer | null = null
|
|
161
|
+
let tempPath = ''
|
|
162
|
+
const rawHash = createHash('md5')
|
|
163
|
+
const cleanupTemp = async () => {
|
|
164
|
+
if (tempPath) await fs.unlink(tempPath).catch(() => undefined)
|
|
165
|
+
}
|
|
166
|
+
if (source) {
|
|
167
|
+
if (source.startsWith('base64://')) source = source.slice(9)
|
|
168
|
+
const comma = source.indexOf('base64,')
|
|
146
169
|
const base64 = comma >= 0 ? source.slice(comma + 7) : source
|
|
147
170
|
const normalizedBase64 = base64.replace(/-/g, '+').replace(/_/g, '/')
|
|
148
171
|
try {
|
|
@@ -152,124 +175,152 @@ export function registerWeb(
|
|
|
152
175
|
koa.body = { error: 'invalid media data' }
|
|
153
176
|
return
|
|
154
177
|
}
|
|
155
|
-
mime = source.startsWith('data:')
|
|
156
|
-
? source.slice(5, source.indexOf(';')).toLowerCase()
|
|
157
|
-
: ''
|
|
158
|
-
} else {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
178
|
+
mime = source.startsWith('data:')
|
|
179
|
+
? source.slice(5, source.indexOf(';')).toLowerCase()
|
|
180
|
+
: ''
|
|
181
|
+
} else {
|
|
182
|
+
tempPath = path.join(uploadDir, `.upload-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`)
|
|
183
|
+
const write = createWriteStream(tempPath)
|
|
184
|
+
try {
|
|
185
|
+
for await (const chunk of koa.req as AsyncIterable<Buffer | string>) {
|
|
186
|
+
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
187
|
+
rawHash.update(part)
|
|
188
|
+
if (!write.write(part)) {
|
|
189
|
+
await new Promise<void>((resolve) => write.once('drain', resolve))
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
await new Promise<void>((resolve, reject) => {
|
|
193
|
+
write.once('error', reject)
|
|
194
|
+
write.end(() => resolve())
|
|
195
|
+
})
|
|
196
|
+
} catch (error) {
|
|
197
|
+
write.destroy()
|
|
198
|
+
await cleanupTemp()
|
|
199
|
+
throw error
|
|
200
|
+
}
|
|
201
|
+
name = String(koa.query.name ?? koa.get('x-file-name') ?? '')
|
|
202
|
+
try {
|
|
203
|
+
name = decodeURIComponent(name)
|
|
167
204
|
} catch {
|
|
168
205
|
// 保持原始名称即可
|
|
169
206
|
}
|
|
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
|
|
207
|
+
mime = String(koa.get('content-type') ?? '').split(';')[0].trim().toLowerCase()
|
|
208
|
+
}
|
|
209
|
+
const fileSize = buffer ? buffer.length : existsSync(tempPath) ? statSync(tempPath).size : 0
|
|
210
|
+
if (!fileSize) {
|
|
211
|
+
await cleanupTemp()
|
|
212
|
+
koa.status = 400
|
|
213
|
+
koa.body = { error: 'missing media data' }
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
if (fileSize > MAX_UPLOAD_BYTES) {
|
|
217
|
+
await cleanupTemp()
|
|
218
|
+
koa.status = 413
|
|
219
|
+
koa.body = { error: 'media file too large' }
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await fs.mkdir(uploadDir, { recursive: true })
|
|
224
|
+
let detected: { ext?: string } | undefined
|
|
225
|
+
try {
|
|
226
|
+
const result = buffer
|
|
227
|
+
? await FileType.fromBuffer(buffer)
|
|
228
|
+
: await FileType.fromStream(createReadStream(tempPath))
|
|
229
|
+
detected = result ?? undefined
|
|
230
|
+
} catch {
|
|
231
|
+
// 部分音频/文件无法识别时继续用 MIME 或文件名兜底
|
|
232
|
+
detected = undefined
|
|
196
233
|
}
|
|
197
234
|
const mimeExt = mimeToExt[mime] || ''
|
|
198
235
|
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
|
-
|
|
236
|
+
const ext = detected?.ext
|
|
237
|
+
? toExtension(detected.ext)
|
|
238
|
+
: mimeExt || nameExt || '.bin'
|
|
239
|
+
const digest = buffer ? createHash('md5').update(buffer).digest('hex') : rawHash.digest('hex')
|
|
240
|
+
const filename = `${digest}${ext}`
|
|
241
|
+
const filePath = path.join(uploadDir, filename)
|
|
242
|
+
try {
|
|
243
|
+
if (!existsSync(filePath)) {
|
|
244
|
+
if (buffer) {
|
|
245
|
+
await fs.writeFile(filePath, buffer)
|
|
246
|
+
} else {
|
|
247
|
+
await fs.rename(tempPath, filePath)
|
|
248
|
+
tempPath = ''
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
await cleanupTemp()
|
|
253
|
+
throw error
|
|
254
|
+
}
|
|
255
|
+
await cleanupTemp()
|
|
256
|
+
koa.body = {
|
|
257
|
+
path: pathToFileURL(filePath).href,
|
|
209
258
|
localPath: filePath,
|
|
210
259
|
}
|
|
211
260
|
})
|
|
212
261
|
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
262
|
+
const sendLocalFile = async (koa: WebContext, filePath: string) => {
|
|
263
|
+
const size = statSync(filePath).size
|
|
264
|
+
const fileName = path.basename(filePath)
|
|
265
|
+
koa.set('Accept-Ranges', 'bytes')
|
|
266
|
+
if (fileName.toLowerCase().endsWith('.webm')) {
|
|
267
|
+
koa.type = 'audio/webm'
|
|
268
|
+
} else {
|
|
269
|
+
koa.type = fileName
|
|
270
|
+
}
|
|
271
|
+
const range = koa.headers.range
|
|
272
|
+
if (range) {
|
|
273
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(String(range))
|
|
274
|
+
if (!match) {
|
|
275
|
+
koa.status = 416
|
|
276
|
+
koa.set('Content-Range', `bytes */${size}`)
|
|
277
|
+
koa.body = ''
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
let start = 0
|
|
281
|
+
let end = size - 1
|
|
282
|
+
if (match[1] === '' && match[2]) {
|
|
283
|
+
start = Math.max(0, size - Number(match[2]))
|
|
284
|
+
} else if (match[1]) {
|
|
285
|
+
start = Number(match[1])
|
|
286
|
+
end = match[2] ? Number(match[2]) : end
|
|
287
|
+
}
|
|
288
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || start >= size) {
|
|
289
|
+
koa.status = 416
|
|
290
|
+
koa.set('Content-Range', `bytes */${size}`)
|
|
291
|
+
koa.body = ''
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
if (end >= size) end = size - 1
|
|
295
|
+
if (end < start) end = start
|
|
296
|
+
koa.status = 206
|
|
297
|
+
koa.set('Content-Range', `bytes ${start}-${end}/${size}`)
|
|
298
|
+
koa.set('Content-Length', String(end - start + 1))
|
|
299
|
+
koa.body = createReadStream(filePath, { start, end })
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
koa.set('Content-Length', String(size))
|
|
303
|
+
koa.body = createReadStream(filePath)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
ctx.server.get(`${config.basePath}/api/media`, async (koa) => {
|
|
307
|
+
const fileName = path.basename(String(koa.query.file ?? koa.query.name ?? ''))
|
|
308
|
+
if (!fileName || fileName === '.' || fileName === '..') {
|
|
309
|
+
koa.status = 400
|
|
310
|
+
koa.body = { error: 'missing media file' }
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
for (const root of [uploadDir, mediaDir]) {
|
|
314
|
+
const filePath = path.resolve(root, fileName)
|
|
315
|
+
if (filePath !== root && filePath.startsWith(`${root}${path.sep}`)
|
|
316
|
+
&& existsSync(filePath) && statSync(filePath).isFile()) {
|
|
317
|
+
await sendLocalFile(koa, filePath)
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
koa.status = 404
|
|
322
|
+
koa.body = { error: 'media file not found' }
|
|
323
|
+
})
|
|
273
324
|
|
|
274
325
|
ctx.server.get(`${config.basePath}/api/cache/all`, async (koa) => {
|
|
275
326
|
const entries = await database.getAllContacts()
|
|
@@ -303,8 +354,12 @@ export function registerWeb(
|
|
|
303
354
|
koa.body = { error: 'missing history params' }
|
|
304
355
|
return
|
|
305
356
|
}
|
|
306
|
-
const
|
|
307
|
-
const
|
|
357
|
+
const defaultLimit = Math.min(config.maxMessagesPerChannel, 100)
|
|
358
|
+
const parsedLimit = Number(koa.query.limit ?? defaultLimit)
|
|
359
|
+
const limit = Math.min(
|
|
360
|
+
Math.max(1, Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.floor(parsedLimit) : defaultLimit),
|
|
361
|
+
config.maxMessagesPerChannel,
|
|
362
|
+
)
|
|
308
363
|
const beforeTime = Number(koa.query.beforeTime ?? 0)
|
|
309
364
|
const queryMessages = async (cid: string) => {
|
|
310
365
|
return Number.isFinite(beforeTime) && beforeTime > 0
|
|
@@ -326,15 +381,40 @@ export function registerWeb(
|
|
|
326
381
|
selfMessages = await querySelfMessages(candidate)
|
|
327
382
|
if (selfMessages.length) break
|
|
328
383
|
}
|
|
329
|
-
// 统一按本地收到时间排序,避免平台时间不准导致顺序颠倒
|
|
330
|
-
messages.sort((a, b) => {
|
|
331
|
-
const timeA =
|
|
332
|
-
const timeB =
|
|
333
|
-
return timeA - timeB
|
|
334
|
-
})
|
|
335
|
-
selfMessages.sort((a, b) => a
|
|
336
|
-
|
|
337
|
-
|
|
384
|
+
// 统一按本地收到时间排序,避免平台时间不准导致顺序颠倒
|
|
385
|
+
messages.sort((a, b) => {
|
|
386
|
+
const timeA = messageSortTime(a)
|
|
387
|
+
const timeB = messageSortTime(b)
|
|
388
|
+
return timeA - timeB
|
|
389
|
+
})
|
|
390
|
+
selfMessages.sort((a, b) => selfMessageSortTime(a) - selfMessageSortTime(b))
|
|
391
|
+
// 合并两条记录流后只返回 limit 条,保证前端分页/结束判断不会因自消息被放大
|
|
392
|
+
const combined: Array<{
|
|
393
|
+
kind: 'message' | 'self'
|
|
394
|
+
time: number
|
|
395
|
+
record: MessageRecord | SelfMessageRecord
|
|
396
|
+
}> = []
|
|
397
|
+
for (const item of messages) {
|
|
398
|
+
combined.push({
|
|
399
|
+
kind: 'message',
|
|
400
|
+
time: messageSortTime(item),
|
|
401
|
+
record: item,
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
for (const item of selfMessages) {
|
|
405
|
+
combined.push({ kind: 'self', time: selfMessageSortTime(item), record: item })
|
|
406
|
+
}
|
|
407
|
+
combined.sort((a, b) => a.time - b.time)
|
|
408
|
+
const capped = combined.slice(-limit)
|
|
409
|
+
koa.body = {
|
|
410
|
+
messages: capped
|
|
411
|
+
.filter((item) => item.kind === 'message')
|
|
412
|
+
.map((item) => item.record as MessageRecord),
|
|
413
|
+
selfMessages: capped
|
|
414
|
+
.filter((item) => item.kind === 'self')
|
|
415
|
+
.map((item) => item.record as SelfMessageRecord),
|
|
416
|
+
}
|
|
417
|
+
})
|
|
338
418
|
|
|
339
419
|
ctx.server.get(`${config.basePath}/api/self-messages`, async (koa) => {
|
|
340
420
|
const platform = String(koa.query.platform ?? '')
|
|
@@ -355,7 +435,7 @@ export function registerWeb(
|
|
|
355
435
|
: await database.listSelfMessages(platform, selfId, candidate, limit)
|
|
356
436
|
if (messages.length) break
|
|
357
437
|
}
|
|
358
|
-
messages.sort((a, b) => a
|
|
438
|
+
messages.sort((a, b) => selfMessageSortTime(a) - selfMessageSortTime(b))
|
|
359
439
|
koa.body = { messages }
|
|
360
440
|
})
|
|
361
441
|
|
|
@@ -377,9 +457,15 @@ export function registerWeb(
|
|
|
377
457
|
message: Array.isArray(body.message) ? body.message as unknown[] : undefined,
|
|
378
458
|
forwardId: typeof body.forwardId === 'string' ? body.forwardId : undefined,
|
|
379
459
|
forwardContent: Array.isArray(body.forwardContent) ? body.forwardContent as unknown[] : undefined,
|
|
380
|
-
sentAt: typeof body.sentAt === 'number' && Number.isFinite(body.sentAt)
|
|
381
|
-
? body.sentAt
|
|
382
|
-
: Date.now(),
|
|
460
|
+
sentAt: typeof body.sentAt === 'number' && Number.isFinite(body.sentAt)
|
|
461
|
+
? body.sentAt
|
|
462
|
+
: Date.now(),
|
|
463
|
+
timestamp: typeof body.timestamp === 'number' && Number.isFinite(body.timestamp)
|
|
464
|
+
? body.timestamp
|
|
465
|
+
: undefined,
|
|
466
|
+
timestampMs: typeof body.timestampMs === 'number' && Number.isFinite(body.timestampMs)
|
|
467
|
+
? body.timestampMs
|
|
468
|
+
: undefined,
|
|
383
469
|
sequence: typeof body.sequence === 'number' && Number.isFinite(body.sequence)
|
|
384
470
|
? body.sequence
|
|
385
471
|
: 0,
|
|
@@ -395,7 +481,8 @@ export function registerWeb(
|
|
|
395
481
|
koa.body = { error: 'missing self-message params' }
|
|
396
482
|
return
|
|
397
483
|
}
|
|
398
|
-
const
|
|
484
|
+
const sentAt = payload.sentAt ?? Date.now()
|
|
485
|
+
const record: SelfMessageRecord = {
|
|
399
486
|
id: payload.id || `web-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
400
487
|
platform: payload.platform,
|
|
401
488
|
selfId: payload.selfId,
|
|
@@ -408,7 +495,9 @@ export function registerWeb(
|
|
|
408
495
|
message: payload.message,
|
|
409
496
|
forwardId: payload.forwardId,
|
|
410
497
|
forwardContent: payload.forwardContent,
|
|
411
|
-
sentAt
|
|
498
|
+
sentAt,
|
|
499
|
+
timestamp: payload.timestamp ?? Math.floor(sentAt / 1000),
|
|
500
|
+
timestampMs: payload.timestampMs ?? sentAt,
|
|
412
501
|
sequence: payload.sequence ?? 0,
|
|
413
502
|
source: payload.source ?? 'webui',
|
|
414
503
|
kind: payload.kind ?? 'text',
|
|
@@ -685,15 +774,20 @@ export function registerWeb(
|
|
|
685
774
|
})
|
|
686
775
|
|
|
687
776
|
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
|
-
|
|
777
|
+
const fullPath = path.join(webRoot, fileName)
|
|
778
|
+
if (existsSync(fullPath) && statSync(fullPath).isFile()) {
|
|
779
|
+
const rootContext = koa as unknown as ParameterizedContext<DefaultState, DefaultContext>
|
|
780
|
+
await send(rootContext, path.relative(webRoot, fullPath), { root: webRoot })
|
|
781
|
+
return
|
|
782
|
+
}
|
|
783
|
+
koa.status = 200
|
|
784
|
+
koa.body = ''
|
|
785
|
+
await send(
|
|
786
|
+
koa as unknown as ParameterizedContext<DefaultState, DefaultContext>,
|
|
787
|
+
'index.html',
|
|
788
|
+
{ root: webRoot },
|
|
789
|
+
)
|
|
790
|
+
}
|
|
697
791
|
|
|
698
792
|
const serveDevWeb = async (koa: WebContext): Promise<boolean> => {
|
|
699
793
|
const vite = getVite()
|