koishi-plugin-chat-patch 2.1.1 → 2.2.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/src/utils.ts CHANGED
@@ -1,144 +1,163 @@
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
- }
144
- }
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
+ // isBotMessage: 是否为机器人发送的消息
108
+ cleanBase64Content(obj: any, isBotMessage: boolean = false): any {
109
+ if (obj === null || obj === undefined) {
110
+ return obj
111
+ }
112
+
113
+ if (typeof obj === 'string') {
114
+ if (this.isBase64(obj)) {
115
+ // 如果是机器人消息且是非图片的Base64,直接返回占位符
116
+ if (isBotMessage && !obj.startsWith('data:image/')) {
117
+ return '[富媒体内容已省略]'
118
+ }
119
+ return this.persistBase64Image(obj)
120
+ }
121
+ return obj
122
+ }
123
+
124
+ if (Array.isArray(obj)) {
125
+ return obj.map(item => this.cleanBase64Content(item, isBotMessage))
126
+ }
127
+
128
+ if (typeof obj === 'object') {
129
+ const cleaned: any = {}
130
+ for (const [key, value] of Object.entries(obj)) {
131
+ // 检查元素类型,如果是机器人消息的非图片元素,跳过Base64处理
132
+ if (isBotMessage && obj.type && !['text', 'image', 'img'].includes(obj.type)) {
133
+ // 对于video、audio等元素,如果src是Base64,替换为占位符
134
+ if (typeof value === 'string' && (key === 'src' || key === 'url' || key === 'file') && this.isBase64(value)) {
135
+ cleaned[key] = '[富媒体内容已省略]'
136
+ continue
137
+ }
138
+ }
139
+
140
+ // 特别处理常见的base64字段
141
+ if (typeof value === 'string' && (
142
+ key === 'src' ||
143
+ key === 'url' ||
144
+ key === 'file' ||
145
+ key === 'data' ||
146
+ key === 'content'
147
+ ) && this.isBase64(value)) {
148
+ // 如果是机器人消息且不是图片Base64,跳过
149
+ if (isBotMessage && !value.startsWith('data:image/')) {
150
+ cleaned[key] = '[富媒体内容已省略]'
151
+ } else {
152
+ cleaned[key] = this.persistBase64Image(value)
153
+ }
154
+ } else {
155
+ cleaned[key] = this.cleanBase64Content(value, isBotMessage)
156
+ }
157
+ }
158
+ return cleaned
159
+ }
160
+
161
+ return obj
162
+ }
163
+ }