koishi-plugin-chat-patch 2.1.2 → 2.3.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/icons/activity.vue +9 -9
- package/client/icons/index.ts +4 -4
- package/client/index.scss +90 -90
- package/client/index.ts +25 -25
- package/client/vue/chat-logic.ts +683 -678
- package/client/vue/composables/useChatActions.ts +1 -1
- package/client/vue/composables/useChatData.ts +1 -1
- package/client/vue/composables/useImageCache.ts +1 -1
- package/client/vue/composables/useVideoCache.ts +130 -0
- package/client/vue/index.vue +658 -612
- package/client/vue/types.ts +1 -1
- package/dist/index.js +2 -2
- package/lib/api-handlers.d.ts +1 -0
- package/lib/file-manager.d.ts +25 -0
- package/lib/index.js +362 -200
- package/lib/message-handler.d.ts +7 -5
- package/lib/utils.d.ts +13 -0
- package/package.json +52 -52
- package/readme.md +25 -25
- package/src/api-handlers.ts +704 -704
- package/src/config.ts +45 -46
- package/src/file-manager.ts +215 -169
- package/src/index.ts +175 -125
- package/src/message-handler.ts +440 -407
- package/src/types.ts +62 -62
- package/src/utils.ts +151 -144
package/src/config.ts
CHANGED
|
@@ -1,46 +1,45 @@
|
|
|
1
|
-
import { Schema } from 'koishi'
|
|
2
|
-
|
|
3
|
-
export interface Config {
|
|
4
|
-
loggerinfo: boolean
|
|
5
|
-
clearIndexedDBOnStart: boolean
|
|
6
|
-
maxMessagesPerChannel: number
|
|
7
|
-
keepMessagesOnClear: number
|
|
8
|
-
maxPersistImages: number
|
|
9
|
-
blockedPlatforms: Array<{
|
|
10
|
-
platformName: string
|
|
11
|
-
exactMatch: boolean
|
|
12
|
-
}>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const Config: Schema<Config> = Schema.intersect([
|
|
16
|
-
Schema.object({
|
|
17
|
-
maxMessagesPerChannel: Schema.number().default(500).description('每个群组最大保存消息数量').min(50).max(1500).step(1),
|
|
18
|
-
keepMessagesOnClear: Schema.number().default(50).description('手动清理历史记录时保留的消息数量').min(0).max(1000).step(1),
|
|
19
|
-
maxPersistImages: Schema.number().default(100).description('持久化存储的图片缓存数量').min(10).max(500).step(1),
|
|
20
|
-
blockedPlatforms: Schema.array(Schema.object({
|
|
21
|
-
platformName: Schema.string().description('平台名称或关键词'),
|
|
22
|
-
exactMatch: Schema.boolean().default(false).description('完全匹配?如果关闭,包含关键词即屏蔽').default(true)
|
|
23
|
-
})).role('table').description('屏蔽的平台列表').default(
|
|
24
|
-
[
|
|
25
|
-
{
|
|
26
|
-
"platformName": "qq",
|
|
27
|
-
"exactMatch": true
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"platformName": "qqguild",
|
|
31
|
-
"exactMatch": true
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"platformName": "sandbox",
|
|
35
|
-
"exactMatch": false
|
|
36
|
-
}
|
|
37
|
-
]
|
|
38
|
-
),
|
|
39
|
-
}).description('基础设置'),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
])
|
|
1
|
+
import { Schema } from 'koishi'
|
|
2
|
+
|
|
3
|
+
export interface Config {
|
|
4
|
+
loggerinfo: boolean
|
|
5
|
+
clearIndexedDBOnStart: boolean
|
|
6
|
+
maxMessagesPerChannel: number
|
|
7
|
+
keepMessagesOnClear: number
|
|
8
|
+
maxPersistImages: number
|
|
9
|
+
blockedPlatforms: Array<{
|
|
10
|
+
platformName: string
|
|
11
|
+
exactMatch: boolean
|
|
12
|
+
}>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Config: Schema<Config> = Schema.intersect([
|
|
16
|
+
Schema.object({
|
|
17
|
+
maxMessagesPerChannel: Schema.number().default(500).description('每个群组最大保存消息数量').min(50).max(1500).step(1),
|
|
18
|
+
keepMessagesOnClear: Schema.number().default(50).description('手动清理历史记录时保留的消息数量').min(0).max(1000).step(1),
|
|
19
|
+
maxPersistImages: Schema.number().default(100).description('持久化存储的图片缓存数量').min(10).max(500).step(1),
|
|
20
|
+
blockedPlatforms: Schema.array(Schema.object({
|
|
21
|
+
platformName: Schema.string().description('平台名称或关键词'),
|
|
22
|
+
exactMatch: Schema.boolean().default(false).description('完全匹配?如果关闭,包含关键词即屏蔽').default(true)
|
|
23
|
+
})).role('table').description('屏蔽的平台列表').default(
|
|
24
|
+
[
|
|
25
|
+
{
|
|
26
|
+
"platformName": "qq",
|
|
27
|
+
"exactMatch": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"platformName": "qqguild",
|
|
31
|
+
"exactMatch": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"platformName": "sandbox",
|
|
35
|
+
"exactMatch": false
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
),
|
|
39
|
+
}).description('基础设置'),
|
|
40
|
+
|
|
41
|
+
Schema.object({
|
|
42
|
+
clearIndexedDBOnStart: Schema.boolean().default(true).description('启动时强制清空IndexedDB缓存(适用于紧急情况,防止浏览器卡死)'),
|
|
43
|
+
loggerinfo: Schema.boolean().default(false).description('日志调试模式').experimental(),
|
|
44
|
+
}).description('开发者选项'),
|
|
45
|
+
])
|
package/src/file-manager.ts
CHANGED
|
@@ -1,169 +1,215 @@
|
|
|
1
|
-
import { ChatData, MessageInfo } from './types'
|
|
2
|
-
import { Context, Logger } from 'koishi'
|
|
3
|
-
import { Config } from './config'
|
|
4
|
-
import { Utils } from './utils'
|
|
5
|
-
|
|
6
|
-
import path from 'node:path'
|
|
7
|
-
import fs from 'node:fs'
|
|
8
|
-
|
|
9
|
-
export class FileManager {
|
|
10
|
-
private dataFilePath: string
|
|
11
|
-
private fileOperationLock = Promise.resolve()
|
|
12
|
-
private logger: Logger
|
|
13
|
-
private utils: Utils
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
1
|
+
import { ChatData, MessageInfo } from './types'
|
|
2
|
+
import { Context, Logger } from 'koishi'
|
|
3
|
+
import { Config } from './config'
|
|
4
|
+
import { Utils } from './utils'
|
|
5
|
+
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
|
|
9
|
+
export class FileManager {
|
|
10
|
+
private dataFilePath: string
|
|
11
|
+
private fileOperationLock = Promise.resolve()
|
|
12
|
+
private logger: Logger
|
|
13
|
+
private utils: Utils
|
|
14
|
+
|
|
15
|
+
private memoryCache: ChatData | null = null
|
|
16
|
+
|
|
17
|
+
private pendingMessages: MessageInfo[] = []
|
|
18
|
+
|
|
19
|
+
private writeTimer: (() => void) | null = null
|
|
20
|
+
|
|
21
|
+
private readonly WRITE_DEBOUNCE_MS = 1000
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private ctx: Context,
|
|
25
|
+
private config: Config
|
|
26
|
+
) {
|
|
27
|
+
this.dataFilePath = path.resolve(ctx.baseDir, 'data', 'chat-patch', 'chat-data.json')
|
|
28
|
+
this.logger = ctx.logger('chat-patch')
|
|
29
|
+
this.utils = new Utils(config)
|
|
30
|
+
|
|
31
|
+
this.memoryCache = this.readChatDataFromFile()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private ensureDataDir() {
|
|
35
|
+
const dir = path.dirname(this.dataFilePath)
|
|
36
|
+
if (!fs.existsSync(dir)) {
|
|
37
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
readChatDataFromFile(): ChatData {
|
|
42
|
+
|
|
43
|
+
if (this.memoryCache) {
|
|
44
|
+
return this.memoryCache
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.nextTick(() => {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(this.dataFilePath)) {
|
|
50
|
+
const jsonData = fs.readFileSync(this.dataFilePath, 'utf8')
|
|
51
|
+
const data = JSON.parse(jsonData)
|
|
52
|
+
this.memoryCache = {
|
|
53
|
+
bots: data.bots || {},
|
|
54
|
+
channels: data.channels || {},
|
|
55
|
+
messages: data.messages || {},
|
|
56
|
+
pinnedBots: data.pinnedBots || [],
|
|
57
|
+
pinnedChannels: data.pinnedChannels || [],
|
|
58
|
+
lastSaveTime: data.lastSaveTime
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
this.logger.error('读取聊天数据失败:', error)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
this.memoryCache = {
|
|
67
|
+
bots: {},
|
|
68
|
+
channels: {},
|
|
69
|
+
messages: {},
|
|
70
|
+
pinnedBots: [],
|
|
71
|
+
pinnedChannels: []
|
|
72
|
+
}
|
|
73
|
+
return this.memoryCache
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
writeChatDataToFile(data: ChatData) {
|
|
77
|
+
|
|
78
|
+
data.lastSaveTime = Date.now()
|
|
79
|
+
this.memoryCache = data
|
|
80
|
+
|
|
81
|
+
process.nextTick(() => {
|
|
82
|
+
try {
|
|
83
|
+
this.ensureDataDir()
|
|
84
|
+
const jsonData = JSON.stringify(data, null, 2)
|
|
85
|
+
fs.writeFileSync(this.dataFilePath, jsonData, 'utf8')
|
|
86
|
+
} catch (error) {
|
|
87
|
+
this.logger.error('写入聊天数据失败:', error)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private scheduleWrite() {
|
|
93
|
+
|
|
94
|
+
if (this.writeTimer) {
|
|
95
|
+
this.writeTimer()
|
|
96
|
+
this.writeTimer = null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.writeTimer = this.ctx.setTimeout(() => {
|
|
100
|
+
|
|
101
|
+
process.nextTick(() => {
|
|
102
|
+
this.flushPendingMessages()
|
|
103
|
+
})
|
|
104
|
+
this.writeTimer = null
|
|
105
|
+
}, this.WRITE_DEBOUNCE_MS)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private flushPendingMessages() {
|
|
109
|
+
if (this.pendingMessages.length === 0) return
|
|
110
|
+
|
|
111
|
+
const messagesToWrite = [...this.pendingMessages]
|
|
112
|
+
this.pendingMessages = []
|
|
113
|
+
|
|
114
|
+
const data = this.memoryCache || this.readChatDataFromFile()
|
|
115
|
+
|
|
116
|
+
for (const messageInfo of messagesToWrite) {
|
|
117
|
+
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`
|
|
118
|
+
|
|
119
|
+
if (!data.messages[channelKey]) {
|
|
120
|
+
data.messages[channelKey] = []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const existingMessage = data.messages[channelKey].find(m => m.id === messageInfo.id)
|
|
124
|
+
if (existingMessage) {
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!messageInfo.timestamp) {
|
|
129
|
+
messageInfo.timestamp = Date.now()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo) as MessageInfo
|
|
133
|
+
data.messages[channelKey].push(cleanedMessageInfo)
|
|
134
|
+
|
|
135
|
+
if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
|
|
136
|
+
data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp)
|
|
137
|
+
data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.writeChatDataToFile(data)
|
|
142
|
+
this.logInfo(`批量写入 ${messagesToWrite.length} 条消息`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
cleanExcessMessages(data: ChatData): ChatData {
|
|
146
|
+
let cleanedCount = 0
|
|
147
|
+
const cleanedMessages: Record<string, MessageInfo[]> = {}
|
|
148
|
+
|
|
149
|
+
for (const [channelKey, messages] of Object.entries(data.messages)) {
|
|
150
|
+
if (messages.length > this.config.maxMessagesPerChannel) {
|
|
151
|
+
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp)
|
|
152
|
+
const keptMessages = sortedMessages.slice(-this.config.maxMessagesPerChannel)
|
|
153
|
+
cleanedCount += messages.length - keptMessages.length
|
|
154
|
+
cleanedMessages[channelKey] = keptMessages
|
|
155
|
+
this.logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`)
|
|
156
|
+
} else {
|
|
157
|
+
cleanedMessages[channelKey] = messages
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (cleanedCount > 0) {
|
|
162
|
+
this.logInfo('总共清理超量消息:', cleanedCount, '条')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
...data,
|
|
167
|
+
messages: cleanedMessages
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async addMessageToFile(messageInfo: MessageInfo) {
|
|
172
|
+
|
|
173
|
+
this.pendingMessages.push(messageInfo)
|
|
174
|
+
|
|
175
|
+
const data = this.memoryCache || this.readChatDataFromFile()
|
|
176
|
+
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`
|
|
177
|
+
|
|
178
|
+
if (!data.messages[channelKey]) {
|
|
179
|
+
data.messages[channelKey] = []
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const existingMessage = data.messages[channelKey].find(m => m.id === messageInfo.id)
|
|
183
|
+
if (!existingMessage) {
|
|
184
|
+
if (!messageInfo.timestamp) {
|
|
185
|
+
messageInfo.timestamp = Date.now()
|
|
186
|
+
}
|
|
187
|
+
const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo) as MessageInfo
|
|
188
|
+
data.messages[channelKey].push(cleanedMessageInfo)
|
|
189
|
+
|
|
190
|
+
if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
|
|
191
|
+
data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp)
|
|
192
|
+
data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.memoryCache = data
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.scheduleWrite()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
dispose() {
|
|
202
|
+
if (this.writeTimer) {
|
|
203
|
+
this.writeTimer()
|
|
204
|
+
this.writeTimer = null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.flushPendingMessages()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private logInfo(...args: any[]) {
|
|
211
|
+
if (this.config.loggerinfo) {
|
|
212
|
+
(this.logger.info as (...args: any[]) => void)(...args)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|