koishi-plugin-chat-patch 1.2.0 → 2.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/icons/activity.vue +9 -9
- package/client/icons/index.ts +4 -4
- package/client/index.scss +40 -4
- package/client/index.ts +23 -22
- package/client/vue/chat-logic.ts +217 -3784
- package/client/vue/composables/useChatActions.ts +61 -0
- package/client/vue/composables/useChatData.ts +184 -0
- package/client/vue/composables/useImageCache.ts +140 -0
- package/client/vue/index.vue +434 -362
- package/client/vue/types.ts +71 -0
- package/dist/index.js +5 -27
- package/dist/style.css +1 -1
- package/lib/index.js +86 -37
- package/package.json +49 -40
- package/readme.md +25 -25
- package/src/api-handlers.ts +664 -657
- package/src/config.ts +47 -49
- package/src/file-manager.ts +168 -168
- package/src/index.ts +125 -125
- package/src/message-handler.ts +305 -282
- package/src/types.ts +59 -59
- package/src/utils.ts +143 -99
- package/client/vue/style.css +0 -1973
- package/lib/config.d.ts +0 -14
package/src/types.ts
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import { h } from 'koishi'
|
|
2
|
-
|
|
3
|
-
export interface BotInfo {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ChannelInfo {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface QuoteInfo {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface MessageInfo {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface ChatData {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
1
|
+
import { h } from 'koishi'
|
|
2
|
+
|
|
3
|
+
export interface BotInfo {
|
|
4
|
+
selfId: string
|
|
5
|
+
platform: string
|
|
6
|
+
username: string
|
|
7
|
+
avatar?: string
|
|
8
|
+
status: 'online' | 'offline'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ChannelInfo {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
type: number | string
|
|
15
|
+
channelId?: string
|
|
16
|
+
guildName?: string
|
|
17
|
+
isDirect?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface QuoteInfo {
|
|
21
|
+
messageId: string
|
|
22
|
+
id: string
|
|
23
|
+
content: string
|
|
24
|
+
elements?: h[]
|
|
25
|
+
user: {
|
|
26
|
+
id: string
|
|
27
|
+
name: string
|
|
28
|
+
userId: string
|
|
29
|
+
avatar?: string
|
|
30
|
+
username: string
|
|
31
|
+
}
|
|
32
|
+
timestamp: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MessageInfo {
|
|
36
|
+
id: string
|
|
37
|
+
content: string
|
|
38
|
+
userId: string
|
|
39
|
+
username: string
|
|
40
|
+
avatar?: string
|
|
41
|
+
timestamp: number
|
|
42
|
+
channelId: string
|
|
43
|
+
selfId: string
|
|
44
|
+
elements?: h[]
|
|
45
|
+
type: 'user' | 'bot'
|
|
46
|
+
guildId?: string
|
|
47
|
+
guildName?: string
|
|
48
|
+
platform: string
|
|
49
|
+
quote?: QuoteInfo
|
|
50
|
+
isDirect?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ChatData {
|
|
54
|
+
bots: Record<string, BotInfo>
|
|
55
|
+
channels: Record<string, Record<string, ChannelInfo>>
|
|
56
|
+
messages: Record<string, MessageInfo[]>
|
|
57
|
+
pinnedBots: string[]
|
|
58
|
+
pinnedChannels: string[]
|
|
59
|
+
lastSaveTime?: number
|
|
60
60
|
}
|
package/src/utils.ts
CHANGED
|
@@ -1,100 +1,144 @@
|
|
|
1
|
-
import { Config } from './config'
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
1
|
+
import { Config } from './config'
|
|
2
|
+
import { Context } from 'koishi'
|
|
3
|
+
import { writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
|
|
8
|
+
export class Utils {
|
|
9
|
+
constructor(private config: Config, private ctx?: Context) { }
|
|
10
|
+
|
|
11
|
+
// 检查平台是否被屏蔽
|
|
12
|
+
isPlatformBlocked(platform: string): boolean {
|
|
13
|
+
if (!this.config.blockedPlatforms || this.config.blockedPlatforms.length === 0) {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const blockedPlatform of this.config.blockedPlatforms) {
|
|
18
|
+
if (blockedPlatform.exactMatch) {
|
|
19
|
+
if (platform === blockedPlatform.platformName) {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
if (platform.includes(blockedPlatform.platformName)) {
|
|
24
|
+
return true
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 递归提取所有文本内容的函数
|
|
33
|
+
extractTextContent(elements: any[]): string {
|
|
34
|
+
let text = ''
|
|
35
|
+
for (const element of elements) {
|
|
36
|
+
if (element.type === 'text') {
|
|
37
|
+
text += element.attrs?.content || ''
|
|
38
|
+
} else if (element.type === 'p') {
|
|
39
|
+
if (element.children && element.children.length > 0) {
|
|
40
|
+
text += this.extractTextContent(element.children) + '\n'
|
|
41
|
+
}
|
|
42
|
+
} else if (element.children && element.children.length > 0) {
|
|
43
|
+
text += this.extractTextContent(element.children)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return text
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 检查字符串是否为base64格式
|
|
50
|
+
private isBase64(str: string): boolean {
|
|
51
|
+
if (!str || typeof str !== 'string') return false
|
|
52
|
+
|
|
53
|
+
// 检查是否以data:开头的base64格式
|
|
54
|
+
if (str.startsWith('data:')) {
|
|
55
|
+
return str.includes('base64,')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 检查纯base64字符串(长度大于100且符合base64格式)
|
|
59
|
+
if (str.length > 100) {
|
|
60
|
+
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/
|
|
61
|
+
return base64Regex.test(str)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 持久化 base64 图片并返回本地文件 URL
|
|
68
|
+
persistBase64Image(base64Data: string): string {
|
|
69
|
+
if (!this.ctx || !base64Data.startsWith('data:image/')) return base64Data
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const dir = join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-images')
|
|
73
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
74
|
+
|
|
75
|
+
// 生成文件名:时间戳 + 内容哈希
|
|
76
|
+
const hash = createHash('md5').update(base64Data).digest('hex')
|
|
77
|
+
const ext = base64Data.split(';')[0].split('/')[1] || 'png'
|
|
78
|
+
const filename = `${Date.now()}_${hash}.${ext}`
|
|
79
|
+
const filePath = join(dir, filename)
|
|
80
|
+
|
|
81
|
+
// 写入文件
|
|
82
|
+
const base64Content = base64Data.split(',')[1]
|
|
83
|
+
writeFileSync(filePath, Buffer.from(base64Content, 'base64'))
|
|
84
|
+
|
|
85
|
+
// 清理旧图片
|
|
86
|
+
this.cleanupPersistImages(dir)
|
|
87
|
+
|
|
88
|
+
return pathToFileURL(filePath).href
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return base64Data
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private cleanupPersistImages(dir: string) {
|
|
95
|
+
try {
|
|
96
|
+
const files = readdirSync(dir)
|
|
97
|
+
.map(name => ({ name, path: join(dir, name), mtime: statSync(join(dir, name)).mtimeMs }))
|
|
98
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
99
|
+
|
|
100
|
+
if (files.length > this.config.maxPersistImages) {
|
|
101
|
+
files.slice(this.config.maxPersistImages).forEach(f => unlinkSync(f.path))
|
|
102
|
+
}
|
|
103
|
+
} catch (e) { }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 清理对象中的base64内容,改为持久化存储
|
|
107
|
+
cleanBase64Content(obj: any): any {
|
|
108
|
+
if (obj === null || obj === undefined) {
|
|
109
|
+
return obj
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof obj === 'string') {
|
|
113
|
+
if (this.isBase64(obj)) {
|
|
114
|
+
return this.persistBase64Image(obj)
|
|
115
|
+
}
|
|
116
|
+
return obj
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (Array.isArray(obj)) {
|
|
120
|
+
return obj.map(item => this.cleanBase64Content(item))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof obj === 'object') {
|
|
124
|
+
const cleaned: any = {}
|
|
125
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
126
|
+
// 特别处理常见的base64字段
|
|
127
|
+
if (typeof value === 'string' && (
|
|
128
|
+
key === 'src' ||
|
|
129
|
+
key === 'url' ||
|
|
130
|
+
key === 'file' ||
|
|
131
|
+
key === 'data' ||
|
|
132
|
+
key === 'content'
|
|
133
|
+
) && this.isBase64(value)) {
|
|
134
|
+
cleaned[key] = this.persistBase64Image(value)
|
|
135
|
+
} else {
|
|
136
|
+
cleaned[key] = this.cleanBase64Content(value)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return cleaned
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return obj
|
|
143
|
+
}
|
|
100
144
|
}
|