koishi-plugin-chat-patch 5.5.0 → 5.6.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/vue/index.vue +8 -1
- package/client/web/dist/assets/{Chat-DTEsebnY.css → Chat-BXVWj5Bd.css} +1 -1
- package/client/web/dist/assets/{Chat-Clqxn7CB.js → Chat-m4-U5uHa.js} +4 -4
- package/client/web/dist/assets/{MsgBody-C0TKsXhC.js → MsgBody-CzrxyiFO.js} +1 -1
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-BBF1uxm6.js +67 -0
- package/client/web/dist/assets/{index-BA37EQGa.js → index-EJ2CcODr.js} +72 -78
- package/client/web/dist/assets/index-X9hFOAza.css +1 -0
- package/client/web/dist/index.html +2 -2
- package/client/web/src/App.vue +7 -0
- package/client/web/src/assets/css/chat.css +6 -4
- package/client/web/src/assets/l10n/zh-CN.po +0 -6
- package/client/web/src/components/History.vue +27 -0
- package/client/web/src/components/MsgBody.vue +18 -9
- package/client/web/src/function/connect.ts +208 -46
- package/client/web/src/function/msg.ts +8 -6
- package/client/web/src/function/option.ts +1 -1
- package/client/web/src/function/satori-model.ts +20 -2
- package/client/web/src/function/satori.ts +64 -0
- package/client/web/src/pages/Chat.vue +0 -4
- package/client/web/src/pages/Friends.vue +7 -9
- package/client/web/src/pages/Messages.vue +6 -7
- package/client/web/src/pages/options/OptDev.vue +1 -25
- package/client/web/src/pages/options/OptFunction.vue +23 -0
- package/client/web/tsconfig.tsbuildinfo +1 -1
- package/dist/index.js +1 -1
- package/dist/style.css +1 -1
- package/lib/bootstrap.d.ts +6 -0
- package/lib/database.d.ts +5 -2
- package/lib/gateway.d.ts +46 -0
- package/lib/index.js +1468 -131
- package/lib/recorder.d.ts +3 -5
- package/lib/satori.d.ts +3 -0
- package/lib/types.d.ts +17 -0
- package/package.json +4 -1
- package/src/bootstrap.ts +5 -10
- package/src/database.ts +745 -662
- package/src/gateway.ts +259 -0
- package/src/index.ts +63 -53
- package/src/recorder.ts +97 -77
- package/src/satori.ts +19 -0
- package/src/server.d.ts +26 -26
- package/src/types.ts +19 -0
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CCdBwlN_.js +0 -67
- package/client/web/dist/assets/index-B71pPwae.css +0 -1
package/src/gateway.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { Context } from 'koishi'
|
|
2
|
+
import { Opcode } from '@satorijs/protocol'
|
|
3
|
+
import {} from '@koishijs/plugin-http'
|
|
4
|
+
import {} from '@koishijs/plugin-server'
|
|
5
|
+
import {} from '@satorijs/plugin-server'
|
|
6
|
+
|
|
7
|
+
import { Config } from './config'
|
|
8
|
+
import { ChatDatabase } from './database'
|
|
9
|
+
import { PluginLogger } from './logger'
|
|
10
|
+
import { Recorder } from './recorder'
|
|
11
|
+
import { resolveSatoriEndpoint, toSatoriEventUrl } from './satori'
|
|
12
|
+
import { SatoriEventPayload, SatoriLoginInfo } from './types'
|
|
13
|
+
|
|
14
|
+
const WS_OPEN = 1
|
|
15
|
+
const RECONNECT_MAX_DELAY = 30000
|
|
16
|
+
|
|
17
|
+
export type SatoriGatewayPayload =
|
|
18
|
+
| { kind: 'ready'; logins: SatoriLoginInfo[] }
|
|
19
|
+
| { kind: 'event'; event: SatoriEventPayload }
|
|
20
|
+
| { kind: 'status'; online: boolean }
|
|
21
|
+
|
|
22
|
+
type SatoriGatewayHandler = (payload: SatoriGatewayPayload) => void
|
|
23
|
+
|
|
24
|
+
function getObject(value: unknown): Record<string, unknown> {
|
|
25
|
+
return typeof value === 'object' && value !== null
|
|
26
|
+
? value as Record<string, unknown>
|
|
27
|
+
: {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getString(value: unknown): string {
|
|
31
|
+
return typeof value === 'string' ? value : ''
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getNumber(value: unknown): number {
|
|
35
|
+
const num = Number(value)
|
|
36
|
+
return Number.isFinite(num) ? num : 0
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class SatoriGateway {
|
|
40
|
+
private socket?: WebSocket
|
|
41
|
+
private pingDispose?: () => void
|
|
42
|
+
private reconnectDispose?: () => void
|
|
43
|
+
private stopped = true
|
|
44
|
+
private retry = 0
|
|
45
|
+
private sequence = 0
|
|
46
|
+
private logins: SatoriLoginInfo[] = []
|
|
47
|
+
private online = false
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
private ctx: Context,
|
|
51
|
+
private config: Config,
|
|
52
|
+
private database: ChatDatabase,
|
|
53
|
+
private recorder: Recorder,
|
|
54
|
+
private logger: PluginLogger,
|
|
55
|
+
private onPayload: SatoriGatewayHandler,
|
|
56
|
+
) {}
|
|
57
|
+
|
|
58
|
+
async start() {
|
|
59
|
+
this.stopped = false
|
|
60
|
+
this.retry = 0
|
|
61
|
+
this.sequence = await this.loadSequence()
|
|
62
|
+
this.connect()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dispose() {
|
|
66
|
+
this.stopped = true
|
|
67
|
+
this.pingDispose?.()
|
|
68
|
+
this.reconnectDispose?.()
|
|
69
|
+
this.socket?.close()
|
|
70
|
+
this.socket = undefined
|
|
71
|
+
this.setOnline(false)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
getLogins(): SatoriLoginInfo[] {
|
|
75
|
+
return this.logins.map((item) => ({ ...item }))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async loadSequence(): Promise<number> {
|
|
79
|
+
const value = await this.database.getMeta('satori:sn')
|
|
80
|
+
return getNumber(value)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private connect() {
|
|
84
|
+
if (this.stopped) return
|
|
85
|
+
let socket: WebSocket
|
|
86
|
+
try {
|
|
87
|
+
socket = this.ctx.http.ws(toSatoriEventUrl(resolveSatoriEndpoint(this.ctx)))
|
|
88
|
+
} catch (error) {
|
|
89
|
+
this.logger.warn('Satori 连接创建失败:', error)
|
|
90
|
+
this.scheduleReconnect()
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
this.socket = socket
|
|
94
|
+
|
|
95
|
+
socket.addEventListener('open', () => {
|
|
96
|
+
if (this.socket !== socket) {
|
|
97
|
+
socket.close()
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
this.retry = 0
|
|
101
|
+
this.setOnline(true)
|
|
102
|
+
socket.send(JSON.stringify({
|
|
103
|
+
op: Opcode.IDENTIFY,
|
|
104
|
+
body: {
|
|
105
|
+
token: this.token,
|
|
106
|
+
sn: this.sequence || undefined,
|
|
107
|
+
},
|
|
108
|
+
}))
|
|
109
|
+
if (!this.pingDispose) {
|
|
110
|
+
this.pingDispose = this.ctx.setInterval(() => {
|
|
111
|
+
if (this.socket?.readyState === WS_OPEN) {
|
|
112
|
+
this.socket.send(JSON.stringify({ op: Opcode.PING, body: {} }))
|
|
113
|
+
}
|
|
114
|
+
}, 10000)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
socket.addEventListener('message', (event) => {
|
|
119
|
+
void this.handleMessage(event.data).catch((error) => {
|
|
120
|
+
this.logger.warn('Satori 事件处理失败:', error)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
socket.addEventListener('close', () => {
|
|
125
|
+
if (this.socket === socket) this.socket = undefined
|
|
126
|
+
this.pingDispose?.()
|
|
127
|
+
this.pingDispose = undefined
|
|
128
|
+
this.setOnline(false)
|
|
129
|
+
this.scheduleReconnect()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
socket.addEventListener('error', () => {
|
|
133
|
+
socket.close()
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private get token(): string {
|
|
138
|
+
return getString(this.ctx.satori?.server?.config?.token)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private scheduleReconnect() {
|
|
142
|
+
if (this.stopped || this.reconnectDispose) return
|
|
143
|
+
const delay = Math.min(RECONNECT_MAX_DELAY, 1000 * 2 ** this.retry)
|
|
144
|
+
this.retry += 1
|
|
145
|
+
this.logger.logInfo('Satori 将在', delay, 'ms 后重连')
|
|
146
|
+
this.reconnectDispose = this.ctx.setTimeout(() => {
|
|
147
|
+
this.reconnectDispose = undefined
|
|
148
|
+
this.connect()
|
|
149
|
+
}, delay)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async handleMessage(data: unknown) {
|
|
153
|
+
let payload: { op?: unknown; body?: unknown }
|
|
154
|
+
try {
|
|
155
|
+
payload = JSON.parse(String(data)) as { op?: unknown; body?: unknown }
|
|
156
|
+
} catch {
|
|
157
|
+
this.logger.warn('Satori 消息解析失败')
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (payload.op === Opcode.READY) {
|
|
162
|
+
const body = getObject(payload.body)
|
|
163
|
+
this.logins = this.normalizeLogins(body.logins)
|
|
164
|
+
this.onPayload({ kind: 'ready', logins: this.getLogins() })
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (payload.op !== Opcode.EVENT) return
|
|
169
|
+
|
|
170
|
+
const body = getObject(payload.body)
|
|
171
|
+
const sn = getNumber(body.sn)
|
|
172
|
+
if (sn) {
|
|
173
|
+
this.sequence = sn
|
|
174
|
+
void this.database.setMeta('satori:sn', sn).catch((error) => {
|
|
175
|
+
this.logger.warn('Satori sequence 保存失败:', error)
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const type = getString(body.type)
|
|
180
|
+
if (type === 'login-added' || type === 'login-updated') {
|
|
181
|
+
const next = this.normalizeLogins([body.login])
|
|
182
|
+
if (next.length) {
|
|
183
|
+
this.logins = this.logins.filter((item) => {
|
|
184
|
+
return !(item.platform === next[0].platform && item.selfId === next[0].selfId)
|
|
185
|
+
})
|
|
186
|
+
this.logins.push(next[0])
|
|
187
|
+
this.onPayload({ kind: 'ready', logins: this.getLogins() })
|
|
188
|
+
}
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
if (type === 'login-removed') {
|
|
192
|
+
const login = getObject(body.login)
|
|
193
|
+
const loginUser = getObject(login.user)
|
|
194
|
+
const platform = getString(body.platform) || getString(login.platform)
|
|
195
|
+
const selfId = getString(body.self_id) || getString(body.selfId) || getString(loginUser.id)
|
|
196
|
+
this.logins = this.logins.filter((item) => {
|
|
197
|
+
return !(item.platform === platform && item.selfId === selfId)
|
|
198
|
+
})
|
|
199
|
+
this.onPayload({ kind: 'ready', logins: this.getLogins() })
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const login = getObject(body.login)
|
|
204
|
+
const loginUser = getObject(login.user)
|
|
205
|
+
const platform = getString(body.platform) || getString(login.platform)
|
|
206
|
+
const selfId = getString(body.self_id) || getString(body.selfId) || getString(loginUser.id)
|
|
207
|
+
const event: SatoriEventPayload = {
|
|
208
|
+
type: getString(body.type),
|
|
209
|
+
platform,
|
|
210
|
+
selfId,
|
|
211
|
+
timestamp: getNumber(body.timestamp) || Date.now(),
|
|
212
|
+
sn,
|
|
213
|
+
body,
|
|
214
|
+
}
|
|
215
|
+
if (this.isBlocked(platform)) return
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
await this.recorder.handleEvent(body)
|
|
219
|
+
} catch (error) {
|
|
220
|
+
this.logger.warn('Satori 消息写入失败:', error)
|
|
221
|
+
}
|
|
222
|
+
this.onPayload({ kind: 'event', event })
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private normalizeLogins(value: unknown): SatoriLoginInfo[] {
|
|
226
|
+
if (!Array.isArray(value)) return []
|
|
227
|
+
const result: SatoriLoginInfo[] = []
|
|
228
|
+
for (const item of value) {
|
|
229
|
+
const login = getObject(item)
|
|
230
|
+
const user = getObject(login.user)
|
|
231
|
+
const platform = getString(login.platform) || getString(user.platform)
|
|
232
|
+
const selfId = getString(login.self_id) || getString(login.selfId) || getString(user.id)
|
|
233
|
+
if (!platform || !selfId || this.isBlocked(platform)) continue
|
|
234
|
+
result.push({
|
|
235
|
+
platform,
|
|
236
|
+
selfId,
|
|
237
|
+
name: getString(user.name) || getString(user.nick) || getString(user.nickname) || selfId,
|
|
238
|
+
avatar: getString(user.avatar) || undefined,
|
|
239
|
+
status: getNumber(login.status),
|
|
240
|
+
features: Array.isArray(login.features) ? login.features.map(String) : [],
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
return result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private isBlocked(platform: string): boolean {
|
|
247
|
+
return (this.config.blockedPlatforms ?? []).some((item) => {
|
|
248
|
+
return item.exactMatch
|
|
249
|
+
? platform === item.platformName
|
|
250
|
+
: platform.includes(item.platformName)
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private setOnline(online: boolean) {
|
|
255
|
+
if (this.online === online) return
|
|
256
|
+
this.online = online
|
|
257
|
+
this.onPayload({ kind: 'status', online })
|
|
258
|
+
}
|
|
259
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,69 +1,79 @@
|
|
|
1
|
-
import { Context } from 'koishi'
|
|
2
|
-
import { Console } from '@koishijs/console'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
|
|
5
|
-
import { Config } from './config'
|
|
6
|
-
import { createPluginLogger } from './logger'
|
|
7
|
-
import { ContactCacheService } from './cache'
|
|
8
|
-
import { ChatDatabase } from './database'
|
|
1
|
+
import { Context } from 'koishi'
|
|
2
|
+
import { Console } from '@koishijs/console'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { Config } from './config'
|
|
6
|
+
import { createPluginLogger } from './logger'
|
|
7
|
+
import { ContactCacheService } from './cache'
|
|
8
|
+
import { ChatDatabase } from './database'
|
|
9
9
|
import { Recorder } from './recorder'
|
|
10
|
+
import { SatoriGateway } from './gateway'
|
|
10
11
|
import { MediaManager } from './media'
|
|
11
12
|
import { SelfMessageRecorder } from './self-message'
|
|
12
13
|
import { registerBootstrap } from './bootstrap'
|
|
13
14
|
import { registerWeb } from './web'
|
|
14
|
-
|
|
15
|
-
export const name = 'chat-patch'
|
|
16
|
-
export const reusable = false
|
|
17
|
-
export const filter = false
|
|
15
|
+
|
|
16
|
+
export const name = 'chat-patch'
|
|
17
|
+
export const reusable = false
|
|
18
|
+
export const filter = false
|
|
18
19
|
export const inject = {
|
|
19
|
-
required: ['console', 'server', 'satori.server'],
|
|
20
|
+
required: ['console', 'server', 'http', 'satori.server'],
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
declare module 'koishi' {
|
|
23
|
-
interface Context {
|
|
24
|
-
console: Console
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const usage = `
|
|
29
|
-
---
|
|
30
|
-
|
|
31
|
-
基于 Satori 协议的 Koishi 后台聊天室。
|
|
32
|
-
需要在 Koishi 中启用 server-satori,并在插件内构建 web 应用。
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
`
|
|
36
|
-
|
|
37
|
-
export { Config } from './config'
|
|
38
|
-
|
|
39
|
-
export async function apply(ctx: Context, config: Config) {
|
|
40
|
-
const pluginLogger = createPluginLogger(ctx.logger('chat-patch'), config)
|
|
41
|
-
|
|
42
|
-
const database = new ChatDatabase(ctx, config, pluginLogger)
|
|
43
|
-
await database.initialize()
|
|
44
|
-
const contactCache = new ContactCacheService(ctx, database, pluginLogger)
|
|
45
|
-
|
|
46
|
-
const media = new MediaManager(ctx, config, database, pluginLogger)
|
|
47
|
-
media.start()
|
|
48
|
-
|
|
49
|
-
const recorder = new Recorder(
|
|
50
|
-
recorder
|
|
22
|
+
|
|
23
|
+
declare module 'koishi' {
|
|
24
|
+
interface Context {
|
|
25
|
+
console: Console
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const usage = `
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
基于 Satori 协议的 Koishi 后台聊天室。
|
|
33
|
+
需要在 Koishi 中启用 server-satori,并在插件内构建 web 应用。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
export { Config } from './config'
|
|
39
|
+
|
|
40
|
+
export async function apply(ctx: Context, config: Config) {
|
|
41
|
+
const pluginLogger = createPluginLogger(ctx.logger('chat-patch'), config)
|
|
42
|
+
|
|
43
|
+
const database = new ChatDatabase(ctx, config, pluginLogger)
|
|
44
|
+
await database.initialize()
|
|
45
|
+
const contactCache = new ContactCacheService(ctx, database, pluginLogger)
|
|
46
|
+
|
|
47
|
+
const media = new MediaManager(ctx, config, database, pluginLogger)
|
|
48
|
+
media.start()
|
|
49
|
+
|
|
50
|
+
const recorder = new Recorder(config, database, media, contactCache, pluginLogger)
|
|
51
|
+
const gateway = new SatoriGateway(ctx, config, database, recorder, pluginLogger, (payload) => {
|
|
52
|
+
void ctx.console.broadcast('chat-patch/event', payload).catch((error) => {
|
|
53
|
+
pluginLogger.warn('推送前端事件失败:', error)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
51
56
|
|
|
52
57
|
const selfMessages = new SelfMessageRecorder(ctx, config, database, media, pluginLogger)
|
|
53
58
|
selfMessages.start()
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
ctx.console.addEntry({
|
|
59
|
-
dev: path.resolve(__dirname, '../client/index.ts'),
|
|
60
|
-
prod: path.resolve(__dirname, '../dist'),
|
|
60
|
+
void gateway.start().catch((error) => {
|
|
61
|
+
pluginLogger.warn('Satori 网关启动失败:', error)
|
|
61
62
|
})
|
|
62
63
|
|
|
64
|
+
registerBootstrap(ctx, config, database, pluginLogger, () => gateway.getLogins())
|
|
65
|
+
registerWeb(ctx, config, database, contactCache, media, pluginLogger)
|
|
66
|
+
|
|
67
|
+
ctx.console.addEntry({
|
|
68
|
+
dev: path.resolve(__dirname, '../client/index.ts'),
|
|
69
|
+
prod: path.resolve(__dirname, '../dist'),
|
|
70
|
+
})
|
|
71
|
+
|
|
63
72
|
ctx.on('dispose', async () => {
|
|
73
|
+
gateway.dispose()
|
|
64
74
|
selfMessages.dispose()
|
|
65
75
|
media.dispose()
|
|
66
|
-
await database.dispose()
|
|
67
|
-
pluginLogger.logInfo('chat-patch 已卸载')
|
|
68
|
-
})
|
|
69
|
-
}
|
|
76
|
+
await database.dispose()
|
|
77
|
+
pluginLogger.logInfo('chat-patch 已卸载')
|
|
78
|
+
})
|
|
79
|
+
}
|
package/src/recorder.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { Context, Session } from 'koishi'
|
|
2
|
-
|
|
3
1
|
import { Config } from './config'
|
|
4
2
|
import { ContactCacheService } from './cache'
|
|
5
3
|
import { ChatDatabase } from './database'
|
|
@@ -9,13 +7,34 @@ import { PluginLogger } from './logger'
|
|
|
9
7
|
|
|
10
8
|
const MESSAGE_TYPES = new Set(['message', 'message-created'])
|
|
11
9
|
|
|
10
|
+
function getObject(value: unknown): Record<string, unknown> {
|
|
11
|
+
return typeof value === 'object' && value !== null
|
|
12
|
+
? value as Record<string, unknown>
|
|
13
|
+
: {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getString(value: unknown): string {
|
|
17
|
+
return typeof value === 'string' ? value : ''
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getNumber(value: unknown): number {
|
|
21
|
+
const num = Number(value)
|
|
22
|
+
return Number.isFinite(num) ? num : 0
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
function normalizeGroupId(value: string): string {
|
|
13
26
|
return String(value)
|
|
14
27
|
}
|
|
15
28
|
|
|
29
|
+
function isPrivateChannelType(value: unknown): boolean {
|
|
30
|
+
const num = Number(value)
|
|
31
|
+
if (Number.isFinite(num)) return num === 1
|
|
32
|
+
const text = String(value ?? '').toLowerCase()
|
|
33
|
+
return text === 'direct' || text === 'private'
|
|
34
|
+
}
|
|
35
|
+
|
|
16
36
|
export class Recorder {
|
|
17
37
|
constructor(
|
|
18
|
-
private ctx: Context,
|
|
19
38
|
private config: Config,
|
|
20
39
|
private database: ChatDatabase,
|
|
21
40
|
private media: MediaManager,
|
|
@@ -23,16 +42,19 @@ export class Recorder {
|
|
|
23
42
|
private logger: PluginLogger,
|
|
24
43
|
) {}
|
|
25
44
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
45
|
+
// 后端直连 Satori 后,由 SatoriGateway 逐条送入原始事件
|
|
46
|
+
async handleEvent(body: Record<string, unknown>): Promise<boolean> {
|
|
47
|
+
const type = getString(body.type)
|
|
48
|
+
const login = getObject(body.login)
|
|
49
|
+
const platform = getString(body.platform) || getString(login.platform)
|
|
50
|
+
if (this.isBlocked(platform)) return false
|
|
51
|
+
if (type === 'message-deleted') {
|
|
52
|
+
await this.handleMessageDeleted(body)
|
|
53
|
+
return true
|
|
54
|
+
}
|
|
55
|
+
if (!MESSAGE_TYPES.has(type)) return false
|
|
56
|
+
await this.handleMessageCreated(body)
|
|
57
|
+
return true
|
|
36
58
|
}
|
|
37
59
|
|
|
38
60
|
private isBlocked(platform: string): boolean {
|
|
@@ -43,28 +65,32 @@ export class Recorder {
|
|
|
43
65
|
})
|
|
44
66
|
}
|
|
45
67
|
|
|
46
|
-
private async
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
68
|
+
private async handleMessageCreated(body: Record<string, unknown>) {
|
|
69
|
+
const login = getObject(body.login)
|
|
70
|
+
const loginUser = getObject(login.user)
|
|
71
|
+
const platform = getString(body.platform) || getString(login.platform)
|
|
72
|
+
const selfId = getString(body.self_id) || getString(body.selfId) || getString(loginUser.id)
|
|
73
|
+
const message = getObject(body.message)
|
|
74
|
+
const channel = getObject(body.channel)
|
|
75
|
+
const guild = getObject(body.guild)
|
|
76
|
+
const user = getObject(body.user)
|
|
77
|
+
const sn = getNumber(body.sn)
|
|
78
|
+
const timestamp = getNumber(body.timestamp) || Date.now()
|
|
53
79
|
const record: MessageRecord = {
|
|
54
|
-
id: message
|
|
55
|
-
sequence:
|
|
56
|
-
type:
|
|
80
|
+
id: getString(message.id) || `satori-${sn}`,
|
|
81
|
+
sequence: sn,
|
|
82
|
+
type: getString(body.type),
|
|
57
83
|
platform,
|
|
58
|
-
selfId
|
|
59
|
-
channelId:
|
|
60
|
-
guildId:
|
|
61
|
-
userId:
|
|
62
|
-
timestamp
|
|
63
|
-
timestampMs:
|
|
84
|
+
selfId,
|
|
85
|
+
channelId: getString(channel.id) || undefined,
|
|
86
|
+
guildId: getString(guild.id) || undefined,
|
|
87
|
+
userId: getString(user.id) || undefined,
|
|
88
|
+
timestamp,
|
|
89
|
+
timestampMs: timestamp,
|
|
64
90
|
receivedAt: Date.now(),
|
|
65
|
-
content:
|
|
66
|
-
elements: message
|
|
67
|
-
raw:
|
|
91
|
+
content: getString(message.content) || getString(message.raw_message) || undefined,
|
|
92
|
+
elements: Array.isArray(message.elements) ? message.elements as unknown[] : undefined,
|
|
93
|
+
raw: body,
|
|
68
94
|
}
|
|
69
95
|
|
|
70
96
|
try {
|
|
@@ -72,52 +98,51 @@ export class Recorder {
|
|
|
72
98
|
} catch (error) {
|
|
73
99
|
this.logger.warn('写入历史消息失败:', error)
|
|
74
100
|
}
|
|
75
|
-
void this.cacheMessageContacts(
|
|
101
|
+
void this.cacheMessageContacts(body).catch((error) => {
|
|
76
102
|
this.logger.warn('缓存消息联系人失败:', error)
|
|
77
103
|
})
|
|
78
|
-
void this.cacheMessageMedia(
|
|
104
|
+
void this.cacheMessageMedia(message, getString(channel.id)).catch((error) => {
|
|
79
105
|
this.logger.warn('异步缓存消息媒体失败:', error)
|
|
80
106
|
})
|
|
81
107
|
}
|
|
82
108
|
|
|
83
|
-
private async handleMessageDeleted(
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|| event.guild?.id
|
|
94
|
-
|| session.channelId
|
|
95
|
-
|| '',
|
|
96
|
-
)
|
|
109
|
+
private async handleMessageDeleted(body: Record<string, unknown>) {
|
|
110
|
+
const login = getObject(body.login)
|
|
111
|
+
const loginUser = getObject(login.user)
|
|
112
|
+
const platform = getString(body.platform) || getString(login.platform)
|
|
113
|
+
const selfId = getString(body.self_id) || getString(body.selfId) || getString(loginUser.id)
|
|
114
|
+
const message = getObject(body.message)
|
|
115
|
+
const channel = getObject(body.channel)
|
|
116
|
+
const guild = getObject(body.guild)
|
|
117
|
+
const messageId = getString(message.id)
|
|
118
|
+
const channelId = getString(channel.id) || getString(guild.id)
|
|
97
119
|
if (!messageId || !channelId) return
|
|
98
120
|
const patch = { revoked: true, revokedAt: Date.now() }
|
|
99
|
-
await this.database.updateMessageRevoked(platform,
|
|
100
|
-
await this.database.updateSelfMessageByMessageId(platform,
|
|
121
|
+
await this.database.updateMessageRevoked(platform, selfId, channelId, messageId, patch)
|
|
122
|
+
await this.database.updateSelfMessageByMessageId(platform, selfId, channelId, messageId, patch)
|
|
101
123
|
}
|
|
102
124
|
|
|
103
|
-
private async cacheMessageContacts(
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
125
|
+
private async cacheMessageContacts(body: Record<string, unknown>) {
|
|
126
|
+
const login = getObject(body.login)
|
|
127
|
+
const loginUser = getObject(login.user)
|
|
128
|
+
const platform = getString(body.platform) || getString(login.platform)
|
|
129
|
+
const selfId = getString(body.self_id) || getString(body.selfId) || getString(loginUser.id)
|
|
130
|
+
const user = getObject(body.user)
|
|
131
|
+
const guild = getObject(body.guild)
|
|
132
|
+
const channel = getObject(body.channel)
|
|
133
|
+
const member = getObject(body.member)
|
|
134
|
+
const userId = getString(user.id)
|
|
135
|
+
const guildId = getString(guild.id)
|
|
136
|
+
const channelId = getString(channel.id)
|
|
137
|
+
const isPrivateChannel = isPrivateChannelType(channel.type)
|
|
116
138
|
const groupId = guildId || (isPrivateChannel ? '' : normalizeGroupId(channelId)) || ''
|
|
117
|
-
const userName = user
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
139
|
+
const userName = getString(user.name)
|
|
140
|
+
|| getString(user.nick)
|
|
141
|
+
|| getString(member.nick)
|
|
142
|
+
|| getString(member.name)
|
|
143
|
+
const userAvatar = getString(user.avatar) || getString(member.avatar)
|
|
144
|
+
const groupName = getString(guild.name) || getString(channel.name)
|
|
145
|
+
const groupAvatar = getString(guild.avatar)
|
|
121
146
|
|
|
122
147
|
if (groupId) {
|
|
123
148
|
const groupItem = await this.contactCache.getGroup(
|
|
@@ -128,7 +153,7 @@ export class Recorder {
|
|
|
128
153
|
channelId || groupId,
|
|
129
154
|
groupName,
|
|
130
155
|
groupAvatar,
|
|
131
|
-
channel
|
|
156
|
+
channel.type,
|
|
132
157
|
)
|
|
133
158
|
if (userId) {
|
|
134
159
|
const userItem = await this.contactCache.getUser(
|
|
@@ -152,9 +177,9 @@ export class Recorder {
|
|
|
152
177
|
id: userId,
|
|
153
178
|
user_id: userId,
|
|
154
179
|
nickname: userName || userItem?.name,
|
|
155
|
-
card: member
|
|
180
|
+
card: member.nick || member.name || '',
|
|
156
181
|
avatar: userItem?.avatar || userAvatar || undefined,
|
|
157
|
-
role: member
|
|
182
|
+
role: member.title || '',
|
|
158
183
|
},
|
|
159
184
|
},
|
|
160
185
|
)
|
|
@@ -173,12 +198,7 @@ export class Recorder {
|
|
|
173
198
|
}
|
|
174
199
|
}
|
|
175
200
|
|
|
176
|
-
private async cacheMessageMedia(
|
|
177
|
-
|
|
178
|
-
event.channel?.id
|
|
179
|
-
|| event.guild?.id
|
|
180
|
-
|| '',
|
|
181
|
-
)
|
|
182
|
-
await this.media.cacheMessageMedia(event.message, channelId)
|
|
201
|
+
private async cacheMessageMedia(message: Record<string, unknown>, channelId: string) {
|
|
202
|
+
await this.media.cacheMessageMedia(message, channelId)
|
|
183
203
|
}
|
|
184
204
|
}
|
package/src/satori.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Context } from 'koishi'
|
|
2
|
+
import {} from '@koishijs/plugin-server'
|
|
3
|
+
import {} from '@satorijs/plugin-server'
|
|
4
|
+
|
|
5
|
+
export function resolveSatoriEndpoint(ctx: Context): string {
|
|
6
|
+
const url = ctx.satori?.server?.url ?? '/satori'
|
|
7
|
+
const clean = url.startsWith('undefined') ? url.slice(9) : url
|
|
8
|
+
if (/^https?:\/\//i.test(clean)) return clean
|
|
9
|
+
const base = ctx.server?.selfUrl ?? ctx.server?.config?.selfUrl ?? ''
|
|
10
|
+
return `${base}${clean.startsWith('/') ? clean : `/${clean}`}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toSatoriEventUrl(endpoint: string): string {
|
|
14
|
+
const url = new URL(endpoint)
|
|
15
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
16
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/v1/events`
|
|
17
|
+
url.search = ''
|
|
18
|
+
return url.href
|
|
19
|
+
}
|