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