koishi-plugin-openai-compatible 1.0.8 → 1.0.9
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/lib/index.d.ts +71 -8
- package/lib/index.js +664 -142
- package/lib/types.d.ts +33 -28
- package/lib/types.js +145 -0
- package/package.json +1 -1
- package/src/index.ts +835 -174
- package/src/types.ts +222 -34
- package/lib/api.d.ts +0 -7
- package/lib/api.js +0 -32
- package/lib/config.d.ts +0 -23
- package/lib/config.js +0 -54
- package/lib/emotion.d.ts +0 -9
- package/lib/emotion.js +0 -32
- package/lib/locales/zh.d.ts +0 -44
- package/lib/locales/zh.js +0 -45
- package/lib/storage.d.ts +0 -10
- package/lib/storage.js +0 -25
- package/src/api.ts +0 -40
- package/src/config.ts +0 -91
- package/src/emotion.ts +0 -37
- package/src/locales/zh.ts +0 -43
- package/src/storage.ts +0 -27
package/src/index.ts
CHANGED
|
@@ -1,174 +1,835 @@
|
|
|
1
|
-
import { Context, Schema } from 'koishi'
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
1
|
+
import { Context, Schema, Session, Logger } from 'koishi'
|
|
2
|
+
import { Config, EmotionType, EmojiConfig } from './types'
|
|
3
|
+
|
|
4
|
+
declare module 'koishi' {
|
|
5
|
+
interface Context {
|
|
6
|
+
openai?: OpenAICompatible
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// 冷却时间管理器
|
|
11
|
+
class CooldownManager {
|
|
12
|
+
private cooldowns = new Map<string, number>()
|
|
13
|
+
|
|
14
|
+
constructor(private cooldown: number) {}
|
|
15
|
+
|
|
16
|
+
check(userId: string): { available: boolean; remaining: number } {
|
|
17
|
+
if (this.cooldown <= 0) return { available: true, remaining: 0 }
|
|
18
|
+
|
|
19
|
+
const lastTime = this.cooldowns.get(userId)
|
|
20
|
+
const now = Date.now()
|
|
21
|
+
|
|
22
|
+
if (!lastTime) {
|
|
23
|
+
this.cooldowns.set(userId, now)
|
|
24
|
+
return { available: true, remaining: 0 }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const elapsed = (now - lastTime) / 1000
|
|
28
|
+
const remaining = this.cooldown - elapsed
|
|
29
|
+
|
|
30
|
+
if (remaining > 0) {
|
|
31
|
+
return { available: false, remaining: Math.ceil(remaining) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.cooldowns.set(userId, now)
|
|
35
|
+
return { available: true, remaining: 0 }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
reset(userId: string) {
|
|
39
|
+
this.cooldowns.delete(userId)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// OpenAI兼容客户端
|
|
44
|
+
class OpenAICompatibleClient {
|
|
45
|
+
private logger: Logger
|
|
46
|
+
private endpoint: string
|
|
47
|
+
private apiKey: string
|
|
48
|
+
private timeout: number
|
|
49
|
+
private maxRetries: number
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
private config: Config,
|
|
53
|
+
logger: Logger,
|
|
54
|
+
isEmotion: boolean = false
|
|
55
|
+
) {
|
|
56
|
+
this.logger = logger.extend(isEmotion ? 'emotion-client' : 'client')
|
|
57
|
+
|
|
58
|
+
// 如果是情绪分析客户端,使用情绪分析配置(如果提供)
|
|
59
|
+
if (isEmotion && config.emotionAnalysis.enabled) {
|
|
60
|
+
this.endpoint = config.emotionAnalysis.endpoint || config.endpoint
|
|
61
|
+
this.endpoint = this.endpoint.replace(/\/$/, '')
|
|
62
|
+
this.apiKey = config.emotionAnalysis.apiKey || config.apiKey
|
|
63
|
+
this.timeout = config.emotionTimeout
|
|
64
|
+
} else {
|
|
65
|
+
this.endpoint = config.endpoint.replace(/\/$/, '')
|
|
66
|
+
this.apiKey = config.apiKey
|
|
67
|
+
this.timeout = config.timeout
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.maxRetries = config.maxRetries
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async createChatCompletion(
|
|
74
|
+
messages: any[],
|
|
75
|
+
config?: Partial<Config>,
|
|
76
|
+
isEmotion: boolean = false
|
|
77
|
+
): Promise<string> {
|
|
78
|
+
const url = `${this.endpoint}/chat/completions`
|
|
79
|
+
const headers = {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 根据是否为情绪分析设置不同的参数
|
|
85
|
+
let requestConfig: any
|
|
86
|
+
|
|
87
|
+
if (isEmotion) {
|
|
88
|
+
requestConfig = {
|
|
89
|
+
model: config?.emotionAnalysis?.model || this.config.emotionAnalysis.model,
|
|
90
|
+
messages,
|
|
91
|
+
max_tokens: config?.emotionMaxTokens || this.config.emotionMaxTokens,
|
|
92
|
+
temperature: config?.emotionTemperature || this.config.emotionTemperature,
|
|
93
|
+
top_p: 1,
|
|
94
|
+
presence_penalty: 0,
|
|
95
|
+
frequency_penalty: 0,
|
|
96
|
+
stream: false,
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
requestConfig = {
|
|
100
|
+
model: config?.model || this.config.model,
|
|
101
|
+
messages,
|
|
102
|
+
max_tokens: config?.maxTokens || this.config.maxTokens,
|
|
103
|
+
temperature: config?.temperature || this.config.temperature,
|
|
104
|
+
top_p: config?.topP || this.config.topP,
|
|
105
|
+
presence_penalty: config?.presencePenalty || this.config.presencePenalty,
|
|
106
|
+
frequency_penalty: config?.frequencyPenalty || this.config.frequencyPenalty,
|
|
107
|
+
stream: false,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let lastError: Error | null = null
|
|
112
|
+
for (let i = 0; i <= this.maxRetries; i++) {
|
|
113
|
+
try {
|
|
114
|
+
const controller = new AbortController()
|
|
115
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout)
|
|
116
|
+
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers,
|
|
120
|
+
body: JSON.stringify(requestConfig),
|
|
121
|
+
signal: controller.signal,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
clearTimeout(timeoutId)
|
|
125
|
+
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
const errorText = await response.text()
|
|
128
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = await response.json()
|
|
132
|
+
|
|
133
|
+
if (data.error) {
|
|
134
|
+
throw new Error(data.error.message || 'Unknown API error')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return data.choices[0]?.message?.content?.trim() || ''
|
|
138
|
+
} catch (error: any) {
|
|
139
|
+
lastError = error
|
|
140
|
+
this.logger.warn(`Request failed (attempt ${i + 1}/${this.maxRetries + 1}):`, error.message)
|
|
141
|
+
|
|
142
|
+
if (i < this.maxRetries) {
|
|
143
|
+
// 指数退避
|
|
144
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (lastError) {
|
|
150
|
+
throw lastError
|
|
151
|
+
} else {
|
|
152
|
+
throw new Error('Unknown error occurred')
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async testConnection(): Promise<boolean> {
|
|
157
|
+
try {
|
|
158
|
+
const url = `${this.endpoint}/models`
|
|
159
|
+
const controller = new AbortController()
|
|
160
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000)
|
|
161
|
+
|
|
162
|
+
const response = await fetch(url, {
|
|
163
|
+
headers: {
|
|
164
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
165
|
+
},
|
|
166
|
+
signal: controller.signal,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
clearTimeout(timeoutId)
|
|
170
|
+
return response.ok
|
|
171
|
+
} catch (error: any) {
|
|
172
|
+
this.logger.error('Connection test failed:', error.message)
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 情绪分析器
|
|
179
|
+
class EmotionAnalyzer {
|
|
180
|
+
private client: OpenAICompatibleClient
|
|
181
|
+
private logger: Logger
|
|
182
|
+
private emotionEmojis: Record<string, EmojiConfig>
|
|
183
|
+
private showEmotionImage: boolean
|
|
184
|
+
private imageAsMarkdown: boolean
|
|
185
|
+
|
|
186
|
+
constructor(
|
|
187
|
+
private config: Config,
|
|
188
|
+
logger: Logger
|
|
189
|
+
) {
|
|
190
|
+
this.logger = logger.extend('emotion-analyzer')
|
|
191
|
+
this.client = new OpenAICompatibleClient(config, logger, true)
|
|
192
|
+
this.emotionEmojis = { ...config.emotionEmojis }
|
|
193
|
+
this.showEmotionImage = config.showEmotionImage
|
|
194
|
+
this.imageAsMarkdown = config.imageAsMarkdown
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async analyze(text: string): Promise<{
|
|
198
|
+
emotion: EmotionType
|
|
199
|
+
emoji: string
|
|
200
|
+
imageUrl?: string
|
|
201
|
+
displayText: string
|
|
202
|
+
success: boolean
|
|
203
|
+
error?: string
|
|
204
|
+
}> {
|
|
205
|
+
if (!this.config.emotionAnalysis.enabled || !text.trim()) {
|
|
206
|
+
const neutralConfig = this.emotionEmojis['neutral'] || { text: '😐', image: '' }
|
|
207
|
+
return {
|
|
208
|
+
emotion: 'neutral',
|
|
209
|
+
emoji: neutralConfig.text,
|
|
210
|
+
imageUrl: neutralConfig.image,
|
|
211
|
+
displayText: neutralConfig.text,
|
|
212
|
+
success: true,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const messages = [
|
|
218
|
+
{
|
|
219
|
+
role: 'system',
|
|
220
|
+
content: '你是一个情绪分析专家,只能返回单个情绪类型单词,不要解释,不要额外文本。',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
role: 'user',
|
|
224
|
+
content: `${this.config.emotionAnalysis.prompt}\n\n文本:${text}`,
|
|
225
|
+
},
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
const response = await this.client.createChatCompletion(messages, this.config, true)
|
|
229
|
+
|
|
230
|
+
// 清理和标准化情绪类型
|
|
231
|
+
const emotion = this.normalizeEmotion(response)
|
|
232
|
+
|
|
233
|
+
// 获取对应的表情配置
|
|
234
|
+
const emojiConfig = this.getEmojiConfig(emotion)
|
|
235
|
+
|
|
236
|
+
// 构建显示文本
|
|
237
|
+
let displayText = emojiConfig.text
|
|
238
|
+
|
|
239
|
+
// 如果有图片链接且启用了图片显示
|
|
240
|
+
if (emojiConfig.image && this.showEmotionImage) {
|
|
241
|
+
if (this.imageAsMarkdown) {
|
|
242
|
+
// 使用Markdown格式
|
|
243
|
+
displayText = ``
|
|
244
|
+
} else {
|
|
245
|
+
// 使用CQ码格式(适用于QQ平台)
|
|
246
|
+
displayText = `[CQ:image,file=${emojiConfig.image}]`
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.logger.debug(`Analyzed emotion: ${emotion} -> ${displayText}`)
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
emotion,
|
|
254
|
+
emoji: emojiConfig.text,
|
|
255
|
+
imageUrl: emojiConfig.image,
|
|
256
|
+
displayText,
|
|
257
|
+
success: true,
|
|
258
|
+
}
|
|
259
|
+
} catch (error: any) {
|
|
260
|
+
this.logger.error('Emotion analysis failed:', error)
|
|
261
|
+
|
|
262
|
+
const neutralConfig = this.emotionEmojis['neutral'] || { text: '😐', image: '' }
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
emotion: 'neutral',
|
|
266
|
+
emoji: neutralConfig.text,
|
|
267
|
+
imageUrl: neutralConfig.image,
|
|
268
|
+
displayText: neutralConfig.text,
|
|
269
|
+
success: false,
|
|
270
|
+
error: error.message,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private normalizeEmotion(text: string): EmotionType {
|
|
276
|
+
if (!text) return 'neutral'
|
|
277
|
+
|
|
278
|
+
// 清理文本
|
|
279
|
+
const cleaned = text.toLowerCase().trim()
|
|
280
|
+
|
|
281
|
+
// 常见的情绪类型映射
|
|
282
|
+
const emotionMap: Record<string, EmotionType> = {
|
|
283
|
+
'happy': 'happy',
|
|
284
|
+
'happiness': 'happy',
|
|
285
|
+
'joy': 'happy',
|
|
286
|
+
'joyful': 'happy',
|
|
287
|
+
'glad': 'happy',
|
|
288
|
+
|
|
289
|
+
'sad': 'sad',
|
|
290
|
+
'sadness': 'sad',
|
|
291
|
+
'unhappy': 'sad',
|
|
292
|
+
'depressed': 'sad',
|
|
293
|
+
|
|
294
|
+
'angry': 'angry',
|
|
295
|
+
'anger': 'angry',
|
|
296
|
+
'mad': 'angry',
|
|
297
|
+
'furious': 'angry',
|
|
298
|
+
|
|
299
|
+
'neutral': 'neutral',
|
|
300
|
+
'normal': 'neutral',
|
|
301
|
+
'default': 'neutral',
|
|
302
|
+
|
|
303
|
+
'surprised': 'surprised',
|
|
304
|
+
'surprise': 'surprised',
|
|
305
|
+
'shocked': 'surprised',
|
|
306
|
+
|
|
307
|
+
'fearful': 'fearful',
|
|
308
|
+
'fear': 'fearful',
|
|
309
|
+
'scared': 'fearful',
|
|
310
|
+
'afraid': 'fearful',
|
|
311
|
+
|
|
312
|
+
'disgusted': 'disgusted',
|
|
313
|
+
'disgust': 'disgusted',
|
|
314
|
+
|
|
315
|
+
'excited': 'excited',
|
|
316
|
+
'excitement': 'excited',
|
|
317
|
+
|
|
318
|
+
'calm': 'calm',
|
|
319
|
+
'calmness': 'calm',
|
|
320
|
+
'peaceful': 'calm',
|
|
321
|
+
|
|
322
|
+
'confused': 'confused',
|
|
323
|
+
'confusion': 'confused',
|
|
324
|
+
'puzzled': 'confused',
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 检查是否包含已知情绪关键词
|
|
328
|
+
for (const [key, emotion] of Object.entries(emotionMap)) {
|
|
329
|
+
if (cleaned.includes(key) || cleaned === key) {
|
|
330
|
+
return emotion
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 如果没有匹配,尝试提取第一个单词
|
|
335
|
+
const firstWord = cleaned.split(/[,\s\.]+/)[0]
|
|
336
|
+
if (firstWord && firstWord.length > 1) {
|
|
337
|
+
return firstWord as EmotionType
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return 'neutral'
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private getEmojiConfig(emotion: EmotionType): EmojiConfig {
|
|
344
|
+
// 首先尝试精确匹配
|
|
345
|
+
const exactMatch = this.emotionEmojis[emotion]
|
|
346
|
+
if (exactMatch) return exactMatch
|
|
347
|
+
|
|
348
|
+
// 尝试小写匹配
|
|
349
|
+
const lowerMatch = this.emotionEmojis[emotion.toLowerCase()]
|
|
350
|
+
if (lowerMatch) return lowerMatch
|
|
351
|
+
|
|
352
|
+
// 默认返回中性表情
|
|
353
|
+
return this.emotionEmojis['neutral'] || { text: '😐', image: '' }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
setEmotionEmojis(emojis: Record<string, EmojiConfig>) {
|
|
357
|
+
this.emotionEmojis = { ...this.emotionEmojis, ...emojis }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
getEmotionEmojis(): Record<string, EmojiConfig> {
|
|
361
|
+
return { ...this.emotionEmojis }
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
getEmojiConfigForEmotion(emotion: string): EmojiConfig {
|
|
365
|
+
return this.getEmojiConfig(emotion)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
updateDisplaySettings(showImage: boolean, useMarkdown: boolean) {
|
|
369
|
+
this.showEmotionImage = showImage
|
|
370
|
+
this.imageAsMarkdown = useMarkdown
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 主插件类
|
|
375
|
+
class OpenAICompatible {
|
|
376
|
+
private client: OpenAICompatibleClient
|
|
377
|
+
private emotionAnalyzer: EmotionAnalyzer
|
|
378
|
+
private cooldownManager: CooldownManager
|
|
379
|
+
private logger: Logger
|
|
380
|
+
|
|
381
|
+
constructor(
|
|
382
|
+
private ctx: Context,
|
|
383
|
+
private config: Config
|
|
384
|
+
) {
|
|
385
|
+
this.logger = ctx.logger('openai-compatible')
|
|
386
|
+
this.client = new OpenAICompatibleClient(config, this.logger)
|
|
387
|
+
this.emotionAnalyzer = new EmotionAnalyzer(config, this.logger)
|
|
388
|
+
this.cooldownManager = new CooldownManager(config.cooldown)
|
|
389
|
+
|
|
390
|
+
this.registerCommand()
|
|
391
|
+
this.registerService()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private registerService() {
|
|
395
|
+
this.ctx.openai = this
|
|
396
|
+
|
|
397
|
+
this.ctx.on('dispose', () => {
|
|
398
|
+
this.ctx.openai = undefined
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private registerCommand() {
|
|
403
|
+
const { showError } = this.config
|
|
404
|
+
|
|
405
|
+
this.ctx.command('ai <message:text>', '与AI对话')
|
|
406
|
+
.option('model', '-m <model:string>', { fallback: this.config.model })
|
|
407
|
+
.option('temperature', '-t <temp:number>', { fallback: this.config.temperature })
|
|
408
|
+
.option('system', '-s <systemPrompt:string>', { fallback: this.config.systemPrompt })
|
|
409
|
+
.option('noEmotion', '-n')
|
|
410
|
+
.option('noImage', '-i')
|
|
411
|
+
.action(async ({ session, options }, message) => {
|
|
412
|
+
if (!message) return '请输入消息内容。'
|
|
413
|
+
|
|
414
|
+
const userId = session?.userId || 'unknown'
|
|
415
|
+
|
|
416
|
+
// 检查黑名单
|
|
417
|
+
if (this.config.blacklist.includes(userId)) {
|
|
418
|
+
return showError ? '您已被加入黑名单,无法使用此功能。' : ''
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 检查冷却时间
|
|
422
|
+
const cooldownCheck = this.cooldownManager.check(userId)
|
|
423
|
+
if (!cooldownCheck.available) {
|
|
424
|
+
return showError ? `冷却时间剩余 ${cooldownCheck.remaining} 秒。` : ''
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
if (session) {
|
|
429
|
+
await session.send('正在思考中...')
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 构建消息
|
|
433
|
+
const messages = []
|
|
434
|
+
|
|
435
|
+
// 添加系统提示
|
|
436
|
+
const systemContent = options?.system || this.config.systemPrompt
|
|
437
|
+
if (systemContent) {
|
|
438
|
+
messages.push({
|
|
439
|
+
role: 'system',
|
|
440
|
+
content: systemContent,
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 添加用户消息和提示词
|
|
445
|
+
let userMessage = message
|
|
446
|
+
if (this.config.prompt) {
|
|
447
|
+
userMessage = `${this.config.prompt}\n\n${message}`
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
messages.push({
|
|
451
|
+
role: 'user',
|
|
452
|
+
content: userMessage,
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
// 调用API获取主回复
|
|
456
|
+
const mainResponse = await this.client.createChatCompletion(messages, {
|
|
457
|
+
model: options?.model,
|
|
458
|
+
temperature: options?.temperature,
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
if (!mainResponse) {
|
|
462
|
+
return 'AI没有返回任何内容。'
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 情绪分析(如果不禁用)
|
|
466
|
+
let emotionResult = null
|
|
467
|
+
if (this.config.emotionAnalysis.enabled && !options?.noEmotion) {
|
|
468
|
+
try {
|
|
469
|
+
// 临时修改显示设置(如果指定了禁用图片)
|
|
470
|
+
const showImage = !options?.noImage && this.config.showEmotionImage
|
|
471
|
+
this.emotionAnalyzer.updateDisplaySettings(showImage, this.config.imageAsMarkdown)
|
|
472
|
+
|
|
473
|
+
// 并行处理情绪分析,设置超时
|
|
474
|
+
const emotionPromise = this.emotionAnalyzer.analyze(mainResponse)
|
|
475
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
476
|
+
setTimeout(() => reject(new Error('情绪分析超时')), this.config.emotionTimeout)
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
emotionResult = await Promise.race([emotionPromise, timeoutPromise]) as any
|
|
480
|
+
} catch (error: any) {
|
|
481
|
+
this.logger.warn('Emotion analysis skipped or failed:', error.message)
|
|
482
|
+
emotionResult = {
|
|
483
|
+
emotion: 'neutral',
|
|
484
|
+
emoji: '😐',
|
|
485
|
+
displayText: '😐',
|
|
486
|
+
success: false,
|
|
487
|
+
error: error.message,
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 构建最终回复
|
|
493
|
+
let finalResponse = mainResponse
|
|
494
|
+
|
|
495
|
+
if (emotionResult?.success) {
|
|
496
|
+
// 添加情绪显示
|
|
497
|
+
finalResponse += `\n\n${emotionResult.displayText} (情绪: ${emotionResult.emotion})`
|
|
498
|
+
} else if (emotionResult && this.config.showError) {
|
|
499
|
+
// 情绪分析失败但启用了错误提示
|
|
500
|
+
this.logger.debug('Emotion analysis failed, but main response sent')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return finalResponse
|
|
504
|
+
} catch (error: any) {
|
|
505
|
+
this.logger.error('AI request failed:', error)
|
|
506
|
+
return showError ? `请求失败:${error.message}` : ''
|
|
507
|
+
}
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
// 添加测试命令
|
|
511
|
+
this.ctx.command('ai.test', '测试AI连接')
|
|
512
|
+
.option('emotion', '-e')
|
|
513
|
+
.option('image', '-i')
|
|
514
|
+
.action(async ({ session, options }) => {
|
|
515
|
+
try {
|
|
516
|
+
if (session) {
|
|
517
|
+
await session.send('正在测试连接...')
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (options?.emotion && this.config.emotionAnalysis.enabled) {
|
|
521
|
+
const connected = await this.client.testConnection()
|
|
522
|
+
if (connected) {
|
|
523
|
+
// 测试情绪分析
|
|
524
|
+
const testText = '我今天非常开心!'
|
|
525
|
+
const emotionResult = await this.emotionAnalyzer.analyze(testText)
|
|
526
|
+
let resultText = `连接测试成功!\n情绪分析测试:\n文本:"${testText}"\n分析结果:${emotionResult.emotion} ${emotionResult.displayText}`
|
|
527
|
+
|
|
528
|
+
if (emotionResult.imageUrl) {
|
|
529
|
+
resultText += `\n图片链接:${emotionResult.imageUrl}`
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return resultText
|
|
533
|
+
} else {
|
|
534
|
+
return '连接测试失败!'
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
const connected = await this.client.testConnection()
|
|
538
|
+
return connected ? '连接测试成功!' : '连接测试失败!'
|
|
539
|
+
}
|
|
540
|
+
} catch (error: any) {
|
|
541
|
+
return showError ? `测试失败:${error.message}` : ''
|
|
542
|
+
}
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
// 添加重置冷却命令
|
|
546
|
+
this.ctx.command('ai.reset <userId:string>', '重置用户冷却时间')
|
|
547
|
+
.action(({ session }, userId) => {
|
|
548
|
+
if (!userId && session) {
|
|
549
|
+
const sessionUserId = session.userId
|
|
550
|
+
if (sessionUserId) {
|
|
551
|
+
this.cooldownManager.reset(sessionUserId)
|
|
552
|
+
return '已重置您的冷却时间。'
|
|
553
|
+
} else {
|
|
554
|
+
return '无法获取您的用户ID。'
|
|
555
|
+
}
|
|
556
|
+
} else if (userId) {
|
|
557
|
+
this.cooldownManager.reset(userId)
|
|
558
|
+
return `已重置用户 ${userId} 的冷却时间。`
|
|
559
|
+
} else {
|
|
560
|
+
return '请提供用户ID。'
|
|
561
|
+
}
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
// 添加情绪表情管理命令
|
|
565
|
+
this.ctx.command('ai.emotion', '情绪分析管理')
|
|
566
|
+
.subcommand('.test <text:text>', '测试情绪分析')
|
|
567
|
+
.option('image', '-i')
|
|
568
|
+
.action(async ({ session, options }, text) => {
|
|
569
|
+
if (!this.config.emotionAnalysis.enabled) {
|
|
570
|
+
return '情绪分析功能未启用。'
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (!text) {
|
|
574
|
+
return '请输入要分析的文本。'
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
if (session) {
|
|
579
|
+
await session.send('正在分析情绪...')
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 临时修改显示设置
|
|
583
|
+
const showImage = options?.image ? this.config.showEmotionImage : false
|
|
584
|
+
this.emotionAnalyzer.updateDisplaySettings(showImage, this.config.imageAsMarkdown)
|
|
585
|
+
|
|
586
|
+
const result = await this.emotionAnalyzer.analyze(text)
|
|
587
|
+
|
|
588
|
+
let responseText = `文本:"${text}"\n情绪分析结果:${result.emotion} ${result.displayText}`
|
|
589
|
+
|
|
590
|
+
if (result.imageUrl) {
|
|
591
|
+
responseText += `\n图片链接:${result.imageUrl}`
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (result.success) {
|
|
595
|
+
return responseText
|
|
596
|
+
} else {
|
|
597
|
+
return `情绪分析失败:${result.error}\n${responseText}`
|
|
598
|
+
}
|
|
599
|
+
} catch (error: any) {
|
|
600
|
+
return showError ? `情绪分析失败:${error.message}` : ''
|
|
601
|
+
}
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
.subcommand('.list', '查看情绪表情映射')
|
|
605
|
+
.action(() => {
|
|
606
|
+
const emojis = this.emotionAnalyzer.getEmotionEmojis()
|
|
607
|
+
const list = Object.entries(emojis)
|
|
608
|
+
.map(([emotion, config]) => {
|
|
609
|
+
const imageInfo = config.image ? `\n 图片链接: ${config.image}` : ''
|
|
610
|
+
return `${emotion}: ${config.text}${imageInfo}`
|
|
611
|
+
})
|
|
612
|
+
.join('\n\n')
|
|
613
|
+
return `情绪表情映射:\n${list}`
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
.subcommand('.get <emotion:string>', '获取情绪表情配置')
|
|
617
|
+
.action(({ session }, emotion) => {
|
|
618
|
+
if (!emotion) {
|
|
619
|
+
return '请提供情绪类型。'
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const config = this.emotionAnalyzer.getEmojiConfigForEmotion(emotion)
|
|
623
|
+
let response = `${emotion}: ${config.text}`
|
|
624
|
+
|
|
625
|
+
if (config.image) {
|
|
626
|
+
response += `\n图片链接: ${config.image}`
|
|
627
|
+
|
|
628
|
+
if (this.config.showEmotionImage) {
|
|
629
|
+
if (this.config.imageAsMarkdown) {
|
|
630
|
+
response += `\n显示为: `
|
|
631
|
+
} else {
|
|
632
|
+
response += `\n显示为: [CQ:image,file=${config.image}]`
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
} else {
|
|
636
|
+
response += '\n未配置图片链接'
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return response
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
.subcommand('.set <emotion:string> <text:string> [image:string]', '设置情绪表情配置', { authority: 3 })
|
|
643
|
+
.action(({ session }, emotion, text, image) => {
|
|
644
|
+
if (!emotion || !text) {
|
|
645
|
+
return '请提供情绪类型和文本表情。'
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const newConfig: EmojiConfig = {
|
|
649
|
+
text,
|
|
650
|
+
image: image || '',
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const newEmojis = { [emotion]: newConfig }
|
|
654
|
+
this.emotionAnalyzer.setEmotionEmojis(newEmojis)
|
|
655
|
+
|
|
656
|
+
// 更新配置(内存中)
|
|
657
|
+
this.config.emotionEmojis = this.emotionAnalyzer.getEmotionEmojis()
|
|
658
|
+
|
|
659
|
+
let response = `已设置 ${emotion} 的配置:\n文本表情: ${text}`
|
|
660
|
+
|
|
661
|
+
if (image) {
|
|
662
|
+
response += `\n图片链接: ${image}`
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return response
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
.subcommand('.image <enabled:boolean>', '启用/禁用情绪图片显示', { authority: 3 })
|
|
669
|
+
.action(({ session }, enabled) => {
|
|
670
|
+
if (enabled === undefined) {
|
|
671
|
+
return `当前情绪图片显示状态: ${this.config.showEmotionImage ? '启用' : '禁用'}`
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
this.config.showEmotionImage = enabled
|
|
675
|
+
this.emotionAnalyzer.updateDisplaySettings(enabled, this.config.imageAsMarkdown)
|
|
676
|
+
|
|
677
|
+
return `已${enabled ? '启用' : '禁用'}情绪图片显示`
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
.subcommand('.format <format:string>', '设置图片显示格式(markdown/cq)', { authority: 3 })
|
|
681
|
+
.action(({ session }, format) => {
|
|
682
|
+
if (!format || !['markdown', 'cq'].includes(format.toLowerCase())) {
|
|
683
|
+
return '请指定格式:markdown 或 cq'
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const useMarkdown = format.toLowerCase() === 'markdown'
|
|
687
|
+
this.config.imageAsMarkdown = useMarkdown
|
|
688
|
+
this.emotionAnalyzer.updateDisplaySettings(this.config.showEmotionImage, useMarkdown)
|
|
689
|
+
|
|
690
|
+
return `已设置图片显示格式为: ${format}`
|
|
691
|
+
})
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// 公共API方法
|
|
695
|
+
async chat(
|
|
696
|
+
message: string,
|
|
697
|
+
options?: {
|
|
698
|
+
userId?: string
|
|
699
|
+
model?: string
|
|
700
|
+
temperature?: number
|
|
701
|
+
systemPrompt?: string
|
|
702
|
+
enableEmotion?: boolean
|
|
703
|
+
showImage?: boolean
|
|
704
|
+
}
|
|
705
|
+
): Promise<{
|
|
706
|
+
text: string
|
|
707
|
+
emotion?: EmotionType
|
|
708
|
+
emoji?: string
|
|
709
|
+
imageUrl?: string
|
|
710
|
+
displayText?: string
|
|
711
|
+
emotionSuccess?: boolean
|
|
712
|
+
}> {
|
|
713
|
+
const userId = options?.userId || 'anonymous'
|
|
714
|
+
|
|
715
|
+
// 检查黑名单
|
|
716
|
+
if (this.config.blacklist.includes(userId)) {
|
|
717
|
+
throw new Error('User is blacklisted')
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// 检查冷却时间
|
|
721
|
+
const cooldownCheck = this.cooldownManager.check(userId)
|
|
722
|
+
if (!cooldownCheck.available) {
|
|
723
|
+
throw new Error(`Cooldown: ${cooldownCheck.remaining}s remaining`)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// 构建消息
|
|
727
|
+
const messages = []
|
|
728
|
+
|
|
729
|
+
if (options?.systemPrompt || this.config.systemPrompt) {
|
|
730
|
+
messages.push({
|
|
731
|
+
role: 'system',
|
|
732
|
+
content: options?.systemPrompt || this.config.systemPrompt,
|
|
733
|
+
})
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
let userMessage = message
|
|
737
|
+
if (this.config.prompt) {
|
|
738
|
+
userMessage = `${this.config.prompt}\n\n${message}`
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
messages.push({
|
|
742
|
+
role: 'user',
|
|
743
|
+
content: userMessage,
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
// 获取主回复
|
|
747
|
+
const mainResponse = await this.client.createChatCompletion(messages, {
|
|
748
|
+
model: options?.model,
|
|
749
|
+
temperature: options?.temperature,
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
// 情绪分析
|
|
753
|
+
let emotionResult = null
|
|
754
|
+
const enableEmotion = options?.enableEmotion ?? this.config.emotionAnalysis.enabled
|
|
755
|
+
if (enableEmotion) {
|
|
756
|
+
try {
|
|
757
|
+
// 临时修改显示设置
|
|
758
|
+
const showImage = options?.showImage ?? this.config.showEmotionImage
|
|
759
|
+
this.emotionAnalyzer.updateDisplaySettings(showImage, this.config.imageAsMarkdown)
|
|
760
|
+
|
|
761
|
+
emotionResult = await this.emotionAnalyzer.analyze(mainResponse)
|
|
762
|
+
} catch (error: any) {
|
|
763
|
+
this.logger.warn('Emotion analysis failed in API call:', error.message)
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// 构建结果
|
|
768
|
+
const result: any = {
|
|
769
|
+
text: mainResponse,
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (emotionResult) {
|
|
773
|
+
result.emotion = emotionResult.emotion
|
|
774
|
+
result.emoji = emotionResult.emoji
|
|
775
|
+
result.imageUrl = emotionResult.imageUrl
|
|
776
|
+
result.displayText = emotionResult.displayText
|
|
777
|
+
result.emotionSuccess = emotionResult.success
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return result
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async analyzeEmotion(text: string, showImage?: boolean): Promise<{
|
|
784
|
+
emotion: EmotionType
|
|
785
|
+
emoji: string
|
|
786
|
+
imageUrl?: string
|
|
787
|
+
displayText: string
|
|
788
|
+
success: boolean
|
|
789
|
+
error?: string
|
|
790
|
+
}> {
|
|
791
|
+
// 临时修改显示设置
|
|
792
|
+
const displayImage = showImage ?? this.config.showEmotionImage
|
|
793
|
+
this.emotionAnalyzer.updateDisplaySettings(displayImage, this.config.imageAsMarkdown)
|
|
794
|
+
|
|
795
|
+
return this.emotionAnalyzer.analyze(text)
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async test(): Promise<boolean> {
|
|
799
|
+
return this.client.testConnection()
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// 获取情绪分析器实例
|
|
803
|
+
getEmotionAnalyzer(): EmotionAnalyzer {
|
|
804
|
+
return this.emotionAnalyzer
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// 更新配置(供运行时调整)
|
|
808
|
+
updateConfig(newConfig: Partial<Config>) {
|
|
809
|
+
// 更新相关配置
|
|
810
|
+
if (newConfig.emotionEmojis) {
|
|
811
|
+
this.emotionAnalyzer.setEmotionEmojis(newConfig.emotionEmojis)
|
|
812
|
+
this.config.emotionEmojis = { ...this.config.emotionEmojis, ...newConfig.emotionEmojis }
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (newConfig.showEmotionImage !== undefined || newConfig.imageAsMarkdown !== undefined) {
|
|
816
|
+
const showImage = newConfig.showEmotionImage ?? this.config.showEmotionImage
|
|
817
|
+
const useMarkdown = newConfig.imageAsMarkdown ?? this.config.imageAsMarkdown
|
|
818
|
+
this.emotionAnalyzer.updateDisplaySettings(showImage, useMarkdown)
|
|
819
|
+
|
|
820
|
+
if (newConfig.showEmotionImage !== undefined) {
|
|
821
|
+
this.config.showEmotionImage = newConfig.showEmotionImage
|
|
822
|
+
}
|
|
823
|
+
if (newConfig.imageAsMarkdown !== undefined) {
|
|
824
|
+
this.config.imageAsMarkdown = newConfig.imageAsMarkdown
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export { OpenAICompatible, Config, EmotionAnalyzer }
|
|
831
|
+
export * from './types'
|
|
832
|
+
|
|
833
|
+
export default (ctx: Context, config: Config) => {
|
|
834
|
+
return new OpenAICompatible(ctx, config)
|
|
835
|
+
}
|