koishi-plugin-chat-model 1.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/README.md +114 -0
- package/index.js +378 -0
- package/lib/claude-adapter.js +140 -0
- package/lib/gemini-adapter.js +171 -0
- package/lib/openai-adapter.js +100 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# koishi-plugin-chat-model
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-chat-model)
|
|
4
|
+
|
|
5
|
+
当消息没有触发其他命令时,自动使用大型语言模型(LLM)进行对话,支持记忆上下文。
|
|
6
|
+
|
|
7
|
+
## 功能特点
|
|
8
|
+
|
|
9
|
+
- 支持多种大型语言模型:
|
|
10
|
+
- OpenAI GPT系列 (GPT-3.5-turbo、GPT-4等)
|
|
11
|
+
- Anthropic Claude系列 (Claude 3 Opus、Claude 3 Sonnet、Claude 3 Haiku等)
|
|
12
|
+
- Google Gemini系列 (Gemini Pro、Gemini 1.5等)
|
|
13
|
+
- 自定义模型 (通过适配器支持)
|
|
14
|
+
- 自动在没有匹配到其他命令的情况下触发对话
|
|
15
|
+
- 保持上下文记忆,支持连续对话
|
|
16
|
+
- 可配置触发条件(私聊/群聊、前缀、触发概率等)
|
|
17
|
+
- 支持自定义系统提示语
|
|
18
|
+
- 支持用户使用限制(每日最大对话次数)
|
|
19
|
+
- 提供清除上下文的命令
|
|
20
|
+
|
|
21
|
+
## 安装
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install koishi-plugin-chat-model
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 配置项
|
|
28
|
+
|
|
29
|
+
| 配置项 | 类型 | 默认值 | 说明 |
|
|
30
|
+
|-------|------|-------|------|
|
|
31
|
+
| modelType | select | openai | 对话模型类型,可选:openai、claude、gemini、custom |
|
|
32
|
+
| apiKey | string | | API密钥,必填 |
|
|
33
|
+
| apiEndpoint | string | (根据模型不同) | API地址,可选,用于修改默认API端点 |
|
|
34
|
+
| modelName | string | (根据模型不同) | 模型名称,如:gpt-3.5-turbo、claude-3-sonnet等 |
|
|
35
|
+
| systemPrompt | string | 你是一个有用的AI助手。 | 系统提示词,用于定义AI助手的行为和能力 |
|
|
36
|
+
| contextSize | number | 10 | 上下文记忆的消息数量(轮数),每轮包含一条用户消息和一条助手回复 |
|
|
37
|
+
| temperature | number | 0.7 | 温度参数,控制回复的随机性,0-2之间 |
|
|
38
|
+
| responseTimeout | number | 60 | 响应超时时间,单位为秒 |
|
|
39
|
+
| triggerRatio | number | 100 | 触发概率,范围0-100%之间 |
|
|
40
|
+
| triggerPrefix | string | | 触发前缀,不填则任何未命中命令的消息都会触发 |
|
|
41
|
+
| triggerPrivate | boolean | true | 是否在私聊中自动触发 |
|
|
42
|
+
| triggerGroup | boolean | false | 是否在群聊中自动触发 |
|
|
43
|
+
| customModelAdapter | string | | 自定义模型适配器路径(仅在modelType=custom时有效) |
|
|
44
|
+
| usageLimit.enabled | boolean | false | 是否启用使用限制 |
|
|
45
|
+
| usageLimit.maxMessagesPerUser | number | 100 | 每用户每日最大消息数 |
|
|
46
|
+
| usageLimit.resetTime | string | 00:00 | 使用计数重置时间,24小时制 |
|
|
47
|
+
|
|
48
|
+
## 使用方法
|
|
49
|
+
|
|
50
|
+
1. 在Koishi应用中安装并启用本插件
|
|
51
|
+
2. 配置相应的API密钥和其他设置
|
|
52
|
+
3. 向机器人发送不匹配其他命令的消息即可触发对话
|
|
53
|
+
|
|
54
|
+
### 清除上下文
|
|
55
|
+
|
|
56
|
+
当需要重置对话上下文时,可使用以下命令:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
清除上下文
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 使用系统提示词
|
|
63
|
+
|
|
64
|
+
通过修改系统提示词,可以改变AI助手的行为和风格。例如:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
你是一位资深的编程助手,擅长解答与JavaScript、Python和数据库相关的问题,回答简洁专业,并提供实用的代码示例。
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 使用前缀过滤
|
|
71
|
+
|
|
72
|
+
如果只希望特定格式的消息触发AI对话,可以设置`triggerPrefix`。比如设置为`@AI `(注意结尾有空格),则只有以`@AI `开头的消息才会触发AI回复。
|
|
73
|
+
|
|
74
|
+
## 自定义模型适配器
|
|
75
|
+
|
|
76
|
+
如果需要支持其他语言模型,可以创建自定义适配器:
|
|
77
|
+
|
|
78
|
+
1. 创建一个实现了`generateResponse`方法的类
|
|
79
|
+
2. 将该类的路径填入`customModelAdapter`配置项
|
|
80
|
+
3. 设置`modelType`为`custom`
|
|
81
|
+
|
|
82
|
+
自定义适配器的最小实现示例:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
class CustomAdapter {
|
|
86
|
+
constructor(ctx, config) {
|
|
87
|
+
this.ctx = ctx
|
|
88
|
+
this.config = config
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async generateResponse(messages, session) {
|
|
92
|
+
// 实现与您的模型API通信的逻辑
|
|
93
|
+
// 返回生成的文本回复
|
|
94
|
+
return '这是自定义模型的回复'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async dispose() {
|
|
98
|
+
// 清理资源(如果需要)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = CustomAdapter
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 数据库表
|
|
106
|
+
|
|
107
|
+
本插件会创建以下数据库表:
|
|
108
|
+
|
|
109
|
+
- `chatModelContext`:存储用户的对话上下文
|
|
110
|
+
- `chatModelUsage`:存储用户的使用统计
|
|
111
|
+
|
|
112
|
+
## 协议
|
|
113
|
+
|
|
114
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
const { Context, Schema } = require('koishi')
|
|
2
|
+
|
|
3
|
+
// 插件名称
|
|
4
|
+
exports.name = 'chat-model'
|
|
5
|
+
|
|
6
|
+
// 插件配置项
|
|
7
|
+
exports.Config = Schema.object({
|
|
8
|
+
modelType: Schema.union([
|
|
9
|
+
Schema.const('openai').description('OpenAI (GPT-3.5/GPT-4)'),
|
|
10
|
+
Schema.const('claude').description('Anthropic Claude'),
|
|
11
|
+
Schema.const('gemini').description('Google Gemini'),
|
|
12
|
+
Schema.const('custom').description('自定义模型')
|
|
13
|
+
]).default('openai').description('对话模型类型'),
|
|
14
|
+
apiKey: Schema.string().role('secret').description('API密钥'),
|
|
15
|
+
apiEndpoint: Schema.string().default('https://api.openai.com/v1').description('API地址(可选)'),
|
|
16
|
+
modelName: Schema.string().default('gpt-3.5-turbo').description('模型名称'),
|
|
17
|
+
systemPrompt: Schema.string().default('你是一个有用的AI助手。').description('系统提示词'),
|
|
18
|
+
contextSize: Schema.number().default(10).description('上下文记忆的消息数量(默认: 10)'),
|
|
19
|
+
temperature: Schema.number().min(0).max(2).step(0.1).default(0.7).description('温度参数(0-2之间)'),
|
|
20
|
+
responseTimeout: Schema.number().default(60).description('响应超时时间(秒)'),
|
|
21
|
+
triggerRatio: Schema.number().min(0).max(100).step(1).default(100).description('触发概率(0-100%之间)'),
|
|
22
|
+
triggerPrefix: Schema.string().description('触发前缀,不填则任何未命中命令的消息都会触发模型响应'),
|
|
23
|
+
triggerPrivate: Schema.boolean().default(true).description('是否在私聊中自动触发'),
|
|
24
|
+
triggerGroup: Schema.boolean().default(false).description('是否在群聊中自动触发'),
|
|
25
|
+
customModelAdapter: Schema.string().description('自定义模型适配器路径(仅modelType=custom时有效)'),
|
|
26
|
+
usageLimit: Schema.object({
|
|
27
|
+
enabled: Schema.boolean().default(false).description('是否启用使用限制'),
|
|
28
|
+
maxMessagesPerUser: Schema.number().default(100).description('每用户每日最大消息数'),
|
|
29
|
+
resetTime: Schema.string().default('00:00').description('计数重置时间(24小时制,如 00:00)')
|
|
30
|
+
}).description('使用限制配置')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// 声明依赖的服务或插件
|
|
34
|
+
exports.using = ['database']
|
|
35
|
+
|
|
36
|
+
// 插件主体逻辑
|
|
37
|
+
exports.apply = (ctx, config) => {
|
|
38
|
+
// 从lib目录加载适配器
|
|
39
|
+
const ModelAdapter = loadModelAdapter(ctx, config)
|
|
40
|
+
const modelInstance = new ModelAdapter(ctx, config)
|
|
41
|
+
|
|
42
|
+
// 初始化数据库
|
|
43
|
+
setupDatabase(ctx)
|
|
44
|
+
|
|
45
|
+
// 消息处理器
|
|
46
|
+
const messageHandler = createMessageHandler(ctx, config, modelInstance)
|
|
47
|
+
|
|
48
|
+
// 注册middleware - 在消息中间件管道的末尾捕获未处理的消息
|
|
49
|
+
ctx.middleware(async (session, next) => {
|
|
50
|
+
// 首先尝试使用Koishi的其他处理器处理消息
|
|
51
|
+
const handled = await next()
|
|
52
|
+
|
|
53
|
+
// 如果消息已被处理,或者是不应该触发的消息类型,则直接返回
|
|
54
|
+
if (handled || shouldIgnoreMessage(session, config)) {
|
|
55
|
+
return handled
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 检查是否有自定义前缀,如果有则检查消息是否以该前缀开头
|
|
59
|
+
if (config.triggerPrefix && !session.content.startsWith(config.triggerPrefix)) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 移除前缀(如果有)
|
|
64
|
+
let content = session.content
|
|
65
|
+
if (config.triggerPrefix) {
|
|
66
|
+
content = content.substring(config.triggerPrefix.length).trim()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 如果内容为空,则不处理
|
|
70
|
+
if (!content) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 使用概率判断是否响应
|
|
75
|
+
if (config.triggerRatio < 100 && Math.random() * 100 > config.triggerRatio) {
|
|
76
|
+
ctx.logger.debug('根据概率设置不响应此消息')
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 检查用户使用限制
|
|
81
|
+
if (config.usageLimit?.enabled && !await checkUsageLimit(ctx, session.userId)) {
|
|
82
|
+
await session.send('今日对话次数已达上限,请明天再来')
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
// 显示正在输入状态
|
|
88
|
+
await session.sendQueued('正在思考中...')
|
|
89
|
+
|
|
90
|
+
// 处理消息并发送回复
|
|
91
|
+
const reply = await messageHandler(session, content)
|
|
92
|
+
|
|
93
|
+
// 如果没有回复内容,则跳过发送
|
|
94
|
+
if (!reply) {
|
|
95
|
+
ctx.logger.warn('模型没有返回有效回复')
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 发送回复
|
|
100
|
+
await session.send(reply)
|
|
101
|
+
|
|
102
|
+
// 更新用户使用计数
|
|
103
|
+
if (config.usageLimit?.enabled) {
|
|
104
|
+
await updateUsageCount(ctx, session.userId)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 标记消息为已处理
|
|
108
|
+
return true
|
|
109
|
+
} catch (error) {
|
|
110
|
+
ctx.logger.error('处理消息时出错:', error)
|
|
111
|
+
await session.send(`处理消息时出错: ${error.message}`)
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
}, true) // true表示这个中间件应该在所有其他中间件之后执行
|
|
115
|
+
|
|
116
|
+
// 注册清理上下文的命令
|
|
117
|
+
ctx.command('清除上下文', '清除与AI助手的对话上下文')
|
|
118
|
+
.action(async ({ session }) => {
|
|
119
|
+
await clearUserContext(ctx, session.userId)
|
|
120
|
+
return '已清除对话上下文'
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// 关闭插件时清理资源
|
|
124
|
+
ctx.on('dispose', async () => {
|
|
125
|
+
// 清理资源
|
|
126
|
+
await modelInstance.dispose()
|
|
127
|
+
ctx.logger.info('聊天模型插件已卸载')
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 加载对应的模型适配器
|
|
132
|
+
function loadModelAdapter(ctx, config) {
|
|
133
|
+
try {
|
|
134
|
+
switch(config.modelType) {
|
|
135
|
+
case 'openai':
|
|
136
|
+
return require('./lib/openai-adapter')
|
|
137
|
+
case 'claude':
|
|
138
|
+
return require('./lib/claude-adapter')
|
|
139
|
+
case 'gemini':
|
|
140
|
+
return require('./lib/gemini-adapter')
|
|
141
|
+
case 'custom':
|
|
142
|
+
if (!config.customModelAdapter) {
|
|
143
|
+
throw new Error('使用自定义模型时必须提供适配器路径')
|
|
144
|
+
}
|
|
145
|
+
return require(config.customModelAdapter)
|
|
146
|
+
default:
|
|
147
|
+
ctx.logger.warn(`未知模型类型: ${config.modelType},将使用OpenAI适配器`)
|
|
148
|
+
return require('./lib/openai-adapter')
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
ctx.logger.error(`加载模型适配器失败: ${error.message}`)
|
|
152
|
+
throw new Error(`加载模型适配器失败: ${error.message}`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 设置数据库表结构
|
|
157
|
+
function setupDatabase(ctx) {
|
|
158
|
+
// 用于存储上下文历史记录
|
|
159
|
+
ctx.model.extend('chatModelContext', {
|
|
160
|
+
// 用户ID
|
|
161
|
+
userId: 'string',
|
|
162
|
+
// 上下文历史(作为JSON字符串存储)
|
|
163
|
+
context: 'json',
|
|
164
|
+
// 最后更新时间
|
|
165
|
+
updatedAt: 'timestamp'
|
|
166
|
+
}, {
|
|
167
|
+
primary: 'userId'
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// 用于存储使用统计
|
|
171
|
+
ctx.model.extend('chatModelUsage', {
|
|
172
|
+
// 用户ID
|
|
173
|
+
userId: 'string',
|
|
174
|
+
// 当日消息计数
|
|
175
|
+
dailyCount: 'integer',
|
|
176
|
+
// 上次重置时间
|
|
177
|
+
lastResetDate: 'string'
|
|
178
|
+
}, {
|
|
179
|
+
primary: 'userId'
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 创建消息处理器函数
|
|
184
|
+
function createMessageHandler(ctx, config, modelInstance) {
|
|
185
|
+
return async (session, content) => {
|
|
186
|
+
// 获取用户上下文
|
|
187
|
+
const userContext = await getUserContext(ctx, session.userId)
|
|
188
|
+
|
|
189
|
+
// 添加新的用户消息
|
|
190
|
+
userContext.push({
|
|
191
|
+
role: 'user',
|
|
192
|
+
content: content
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// 限制上下文大小
|
|
196
|
+
while (userContext.length > config.contextSize * 2 + 1) { // +1是因为我们要保留system消息
|
|
197
|
+
// 移除最早的用户消息和对应的助手回复(成对删除)
|
|
198
|
+
userContext.splice(1, 2)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 从配置中获取系统提示,并确保它始终是第一条消息
|
|
202
|
+
if (userContext.length === 0 || userContext[0].role !== 'system') {
|
|
203
|
+
userContext.unshift({
|
|
204
|
+
role: 'system',
|
|
205
|
+
content: config.systemPrompt
|
|
206
|
+
})
|
|
207
|
+
} else {
|
|
208
|
+
// 更新系统提示内容
|
|
209
|
+
userContext[0].content = config.systemPrompt
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// 设置超时
|
|
213
|
+
const timeout = (config.responseTimeout || 60) * 1000
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
// 请求模型响应
|
|
217
|
+
const response = await Promise.race([
|
|
218
|
+
modelInstance.generateResponse(userContext, session),
|
|
219
|
+
new Promise((_, reject) =>
|
|
220
|
+
setTimeout(() => reject(new Error('响应超时')), timeout)
|
|
221
|
+
)
|
|
222
|
+
])
|
|
223
|
+
|
|
224
|
+
// 添加助手回复到上下文
|
|
225
|
+
userContext.push({
|
|
226
|
+
role: 'assistant',
|
|
227
|
+
content: response
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
// 保存更新的上下文
|
|
231
|
+
await saveUserContext(ctx, session.userId, userContext)
|
|
232
|
+
|
|
233
|
+
return response
|
|
234
|
+
} catch (error) {
|
|
235
|
+
ctx.logger.error('生成回复失败:', error)
|
|
236
|
+
return `抱歉,生成回复时发生错误: ${error.message}`
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 获取用户上下文
|
|
242
|
+
async function getUserContext(ctx, userId) {
|
|
243
|
+
const record = await ctx.database.get('chatModelContext', { userId })
|
|
244
|
+
|
|
245
|
+
if (record && record.length > 0) {
|
|
246
|
+
return record[0].context || []
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return []
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 保存用户上下文
|
|
253
|
+
async function saveUserContext(ctx, userId, context) {
|
|
254
|
+
const now = new Date().getTime()
|
|
255
|
+
|
|
256
|
+
// 尝试更新现有记录
|
|
257
|
+
const result = await ctx.database.set('chatModelContext', { userId }, {
|
|
258
|
+
context,
|
|
259
|
+
updatedAt: now
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// 如果没有更新任何记录,则创建新记录
|
|
263
|
+
if (result === 0) {
|
|
264
|
+
await ctx.database.create('chatModelContext', {
|
|
265
|
+
userId,
|
|
266
|
+
context,
|
|
267
|
+
updatedAt: now
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 清除用户上下文
|
|
273
|
+
async function clearUserContext(ctx, userId) {
|
|
274
|
+
await ctx.database.set('chatModelContext', { userId }, {
|
|
275
|
+
context: [],
|
|
276
|
+
updatedAt: new Date().getTime()
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// 检查当前日期是否需要重置使用计数
|
|
281
|
+
async function checkAndResetUsage(ctx, userId, config) {
|
|
282
|
+
const records = await ctx.database.get('chatModelUsage', { userId })
|
|
283
|
+
|
|
284
|
+
if (!records || records.length === 0) {
|
|
285
|
+
return true
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const record = records[0]
|
|
289
|
+
const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD
|
|
290
|
+
|
|
291
|
+
// 如果日期不同,重置计数
|
|
292
|
+
if (record.lastResetDate !== today) {
|
|
293
|
+
await ctx.database.set('chatModelUsage', { userId }, {
|
|
294
|
+
dailyCount: 0,
|
|
295
|
+
lastResetDate: today
|
|
296
|
+
})
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 返回是否未超过限制
|
|
301
|
+
return record.dailyCount < (config.usageLimit?.maxMessagesPerUser || 100)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// 检查使用限制
|
|
305
|
+
async function checkUsageLimit(ctx, userId) {
|
|
306
|
+
const records = await ctx.database.get('chatModelUsage', { userId })
|
|
307
|
+
|
|
308
|
+
if (!records || records.length === 0) {
|
|
309
|
+
return true // 首次使用,允许
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const record = records[0]
|
|
313
|
+
const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD
|
|
314
|
+
|
|
315
|
+
// 如果日期不同,重置计数
|
|
316
|
+
if (record.lastResetDate !== today) {
|
|
317
|
+
await ctx.database.set('chatModelUsage', { userId }, {
|
|
318
|
+
dailyCount: 0,
|
|
319
|
+
lastResetDate: today
|
|
320
|
+
})
|
|
321
|
+
return true
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 返回是否未超过限制
|
|
325
|
+
const config = ctx.config.chatModel
|
|
326
|
+
return record.dailyCount < (config.usageLimit?.maxMessagesPerUser || 100)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 更新使用计数
|
|
330
|
+
async function updateUsageCount(ctx, userId) {
|
|
331
|
+
const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD
|
|
332
|
+
const records = await ctx.database.get('chatModelUsage', { userId })
|
|
333
|
+
|
|
334
|
+
if (!records || records.length === 0) {
|
|
335
|
+
// 创建新记录
|
|
336
|
+
await ctx.database.create('chatModelUsage', {
|
|
337
|
+
userId,
|
|
338
|
+
dailyCount: 1,
|
|
339
|
+
lastResetDate: today
|
|
340
|
+
})
|
|
341
|
+
} else {
|
|
342
|
+
// 更新现有记录
|
|
343
|
+
const record = records[0]
|
|
344
|
+
|
|
345
|
+
// 如果是新的一天,重置计数
|
|
346
|
+
if (record.lastResetDate !== today) {
|
|
347
|
+
await ctx.database.set('chatModelUsage', { userId }, {
|
|
348
|
+
dailyCount: 1,
|
|
349
|
+
lastResetDate: today
|
|
350
|
+
})
|
|
351
|
+
} else {
|
|
352
|
+
// 增加计数
|
|
353
|
+
await ctx.database.set('chatModelUsage', { userId }, {
|
|
354
|
+
dailyCount: record.dailyCount + 1
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 判断是否应该忽略消息
|
|
361
|
+
function shouldIgnoreMessage(session, config) {
|
|
362
|
+
// 忽略自己发送的消息
|
|
363
|
+
if (session.userId === session.selfId) {
|
|
364
|
+
return true
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 根据配置决定是否处理私聊/群聊消息
|
|
368
|
+
const isPrivate = session.channelId === session.userId
|
|
369
|
+
if (isPrivate && !config.triggerPrivate) {
|
|
370
|
+
return true
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!isPrivate && !config.triggerGroup) {
|
|
374
|
+
return true
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return false
|
|
378
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const fetch = require('node-fetch')
|
|
2
|
+
const AbortController = require('abort-controller')
|
|
3
|
+
|
|
4
|
+
class ClaudeAdapter {
|
|
5
|
+
constructor(ctx, config) {
|
|
6
|
+
this.ctx = ctx
|
|
7
|
+
this.config = config
|
|
8
|
+
this.apiKey = config.apiKey
|
|
9
|
+
this.apiEndpoint = config.apiEndpoint || 'https://api.anthropic.com/v1'
|
|
10
|
+
// Claude模型名称映射
|
|
11
|
+
const modelMap = {
|
|
12
|
+
'claude-3-opus': 'claude-3-opus-20240229',
|
|
13
|
+
'claude-3-sonnet': 'claude-3-sonnet-20240229',
|
|
14
|
+
'claude-3-haiku': 'claude-3-haiku-20240307',
|
|
15
|
+
'claude-2': 'claude-2.0',
|
|
16
|
+
'claude-instant': 'claude-instant-1.2'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
this.modelName = modelMap[config.modelName] || config.modelName || 'claude-3-sonnet-20240229'
|
|
20
|
+
this.temperature = config.temperature ?? 0.7
|
|
21
|
+
|
|
22
|
+
// 验证必要参数
|
|
23
|
+
if (!this.apiKey) {
|
|
24
|
+
ctx.logger.error('Claude API密钥未设置')
|
|
25
|
+
throw new Error('Claude API密钥未设置')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ctx.logger.info(`Claude适配器已初始化,使用模型: ${this.modelName}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 将Koishi消息格式转换为Claude格式
|
|
33
|
+
* @param {Array} koishiMessages - Koishi格式的消息数组
|
|
34
|
+
* @returns {Array} - Claude API格式的消息数组
|
|
35
|
+
*/
|
|
36
|
+
formatMessages(koishiMessages) {
|
|
37
|
+
// 获取system message (如果有)
|
|
38
|
+
const systemMessage = koishiMessages.find(msg => msg.role === 'system')?.content || ''
|
|
39
|
+
|
|
40
|
+
// 过滤掉system message,只保留user和assistant消息
|
|
41
|
+
const conversationMessages = koishiMessages.filter(msg => msg.role !== 'system')
|
|
42
|
+
|
|
43
|
+
const claudeMessages = conversationMessages.map(msg => ({
|
|
44
|
+
role: msg.role === 'assistant' ? 'assistant' : 'user',
|
|
45
|
+
content: msg.content
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
systemMessage,
|
|
50
|
+
messages: claudeMessages
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 生成回复
|
|
56
|
+
* @param {Array} messages - 对话历史消息
|
|
57
|
+
* @param {Object} session - Koishi会话对象
|
|
58
|
+
* @returns {Promise<string>} - 生成的回复文本
|
|
59
|
+
*/
|
|
60
|
+
async generateResponse(messages, session) {
|
|
61
|
+
this.ctx.logger.debug(`向Claude发送请求,消息数: ${messages.length}`)
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// 格式化消息为Claude格式
|
|
65
|
+
const { systemMessage, messages: claudeMessages } = this.formatMessages(messages)
|
|
66
|
+
|
|
67
|
+
// 设置超时
|
|
68
|
+
const timeout = 60000 // 60秒超时
|
|
69
|
+
const controller = new AbortController()
|
|
70
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
71
|
+
|
|
72
|
+
// 构建API请求
|
|
73
|
+
const response = await fetch(`${this.apiEndpoint}/messages`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: {
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
'x-api-key': this.apiKey,
|
|
78
|
+
'anthropic-version': '2023-06-01'
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
model: this.modelName,
|
|
82
|
+
messages: claudeMessages,
|
|
83
|
+
system: systemMessage,
|
|
84
|
+
temperature: this.temperature,
|
|
85
|
+
max_tokens: 4000,
|
|
86
|
+
metadata: {
|
|
87
|
+
user_id: session.userId
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
signal: controller.signal
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// 清除超时定时器
|
|
94
|
+
clearTimeout(timeoutId)
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const error = await response.json().catch(() => ({}))
|
|
98
|
+
this.ctx.logger.error(`Claude API请求失败: ${response.status} ${response.statusText}`, error)
|
|
99
|
+
throw new Error(`API请求失败: ${response.status} ${response.statusText}${error.error ? ': ' + error.error : ''}`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const data = await response.json()
|
|
103
|
+
|
|
104
|
+
// 检查响应格式
|
|
105
|
+
if (!data.content || !data.content[0] || !data.content[0].text) {
|
|
106
|
+
this.ctx.logger.error('Claude返回了无效的响应格式', data)
|
|
107
|
+
throw new Error('收到无效的API响应')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 记录使用情况
|
|
111
|
+
if (data.usage) {
|
|
112
|
+
this.ctx.logger.debug(`使用了 ${data.usage.input_tokens} 输入令牌和 ${data.usage.output_tokens} 输出令牌`)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return data.content[0].text.trim()
|
|
116
|
+
} catch (error) {
|
|
117
|
+
this.ctx.logger.error('Claude请求失败:', error)
|
|
118
|
+
if (error.name === 'AbortError') {
|
|
119
|
+
throw new Error('请求超时,请稍后再试')
|
|
120
|
+
} else if (error.message.includes('429')) {
|
|
121
|
+
throw new Error('请求过于频繁,请稍后再试')
|
|
122
|
+
} else if (error.message.includes('401')) {
|
|
123
|
+
throw new Error('API密钥无效或已过期')
|
|
124
|
+
} else if (error.message.includes('quota')) {
|
|
125
|
+
throw new Error('API配额已用尽')
|
|
126
|
+
}
|
|
127
|
+
throw error
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 当插件卸载时清理资源
|
|
133
|
+
*/
|
|
134
|
+
async dispose() {
|
|
135
|
+
// Claude适配器不需要特别的清理工作
|
|
136
|
+
this.ctx.logger.debug('Claude适配器资源已清理')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = ClaudeAdapter
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const fetch = require('node-fetch')
|
|
2
|
+
const AbortController = require('abort-controller')
|
|
3
|
+
|
|
4
|
+
class GeminiAdapter {
|
|
5
|
+
constructor(ctx, config) {
|
|
6
|
+
this.ctx = ctx
|
|
7
|
+
this.config = config
|
|
8
|
+
this.apiKey = config.apiKey
|
|
9
|
+
this.apiEndpoint = config.apiEndpoint || 'https://generativelanguage.googleapis.com/v1beta'
|
|
10
|
+
|
|
11
|
+
// Gemini模型映射
|
|
12
|
+
const modelMap = {
|
|
13
|
+
'gemini-pro': 'gemini-pro',
|
|
14
|
+
'gemini-1.5-pro': 'gemini-1.5-pro',
|
|
15
|
+
'gemini-1.5-flash': 'gemini-1.5-flash'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.modelName = modelMap[config.modelName] || config.modelName || 'gemini-pro'
|
|
19
|
+
this.temperature = config.temperature ?? 0.7
|
|
20
|
+
|
|
21
|
+
// 验证必要参数
|
|
22
|
+
if (!this.apiKey) {
|
|
23
|
+
ctx.logger.error('Gemini API密钥未设置')
|
|
24
|
+
throw new Error('Gemini API密钥未设置')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ctx.logger.info(`Gemini适配器已初始化,使用模型: ${this.modelName}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 将Koishi消息格式转换为Gemini格式
|
|
32
|
+
* @param {Array} koishiMessages - Koishi格式的消息数组
|
|
33
|
+
* @returns {Array} - Gemini API格式的消息数组
|
|
34
|
+
*/
|
|
35
|
+
formatMessages(koishiMessages) {
|
|
36
|
+
// 获取system message (如果有)
|
|
37
|
+
const systemMessage = koishiMessages.find(msg => msg.role === 'system')?.content || ''
|
|
38
|
+
|
|
39
|
+
// 将系统消息作为用户的第一条消息(Gemini不直接支持system role)
|
|
40
|
+
let geminiMessages = []
|
|
41
|
+
|
|
42
|
+
if (systemMessage) {
|
|
43
|
+
geminiMessages.push({
|
|
44
|
+
role: 'user',
|
|
45
|
+
parts: [{ text: `系统指令: ${systemMessage}` }]
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// 如果只有系统消息,添加一个模型回复,否则会报错
|
|
49
|
+
geminiMessages.push({
|
|
50
|
+
role: 'model',
|
|
51
|
+
parts: [{ text: '我明白了,我会按照指示行动。有什么可以帮助你的吗?' }]
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 添加用户和助手的消息
|
|
56
|
+
for (let i = 0; i < koishiMessages.length; i++) {
|
|
57
|
+
const msg = koishiMessages[i]
|
|
58
|
+
|
|
59
|
+
// 跳过system消息,因为已经处理过了
|
|
60
|
+
if (msg.role === 'system') continue
|
|
61
|
+
|
|
62
|
+
geminiMessages.push({
|
|
63
|
+
role: msg.role === 'user' ? 'user' : 'model',
|
|
64
|
+
parts: [{ text: msg.content }]
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 确保消息数组是以用户消息开始,并交替出现
|
|
69
|
+
if (geminiMessages.length === 0 || geminiMessages[0].role !== 'user') {
|
|
70
|
+
this.ctx.logger.warn('消息格式不正确,已调整为Gemini的要求')
|
|
71
|
+
// 添加一个空的用户消息
|
|
72
|
+
geminiMessages.unshift({
|
|
73
|
+
role: 'user',
|
|
74
|
+
parts: [{ text: '你好' }]
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return geminiMessages
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 生成回复
|
|
83
|
+
* @param {Array} messages - 对话历史消息
|
|
84
|
+
* @param {Object} session - Koishi会话对象
|
|
85
|
+
* @returns {Promise<string>} - 生成的回复文本
|
|
86
|
+
*/
|
|
87
|
+
async generateResponse(messages, session) {
|
|
88
|
+
this.ctx.logger.debug(`向Gemini发送请求,消息数: ${messages.length}`)
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// 格式化消息为Gemini格式
|
|
92
|
+
const geminiMessages = this.formatMessages(messages)
|
|
93
|
+
|
|
94
|
+
// 设置超时
|
|
95
|
+
const timeout = 60000 // 60秒超时
|
|
96
|
+
const controller = new AbortController()
|
|
97
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
98
|
+
|
|
99
|
+
// 构建API请求URL
|
|
100
|
+
const apiUrl = `${this.apiEndpoint}/models/${this.modelName}:generateContent?key=${this.apiKey}`
|
|
101
|
+
|
|
102
|
+
const response = await fetch(apiUrl, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: {
|
|
105
|
+
'Content-Type': 'application/json'
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
contents: geminiMessages,
|
|
109
|
+
generationConfig: {
|
|
110
|
+
temperature: this.temperature,
|
|
111
|
+
maxOutputTokens: 2048,
|
|
112
|
+
topP: 0.95,
|
|
113
|
+
topK: 40
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
signal: controller.signal
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// 清除超时定时器
|
|
120
|
+
clearTimeout(timeoutId)
|
|
121
|
+
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const error = await response.json().catch(() => ({}))
|
|
124
|
+
this.ctx.logger.error(`Gemini API请求失败: ${response.status} ${response.statusText}`, error)
|
|
125
|
+
throw new Error(`API请求失败: ${response.status} ${response.statusText}${error.error ? ': ' + error.error.message : ''}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const data = await response.json()
|
|
129
|
+
|
|
130
|
+
// 检查响应格式
|
|
131
|
+
if (!data.candidates || !data.candidates[0] || !data.candidates[0].content || !data.candidates[0].content.parts) {
|
|
132
|
+
this.ctx.logger.error('Gemini返回了无效的响应格式', data)
|
|
133
|
+
throw new Error('收到无效的API响应')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 如果响应中包含屏蔽标记,返回相应提示
|
|
137
|
+
if (data.candidates[0].finishReason === 'SAFETY') {
|
|
138
|
+
return '抱歉,您的请求触发了内容安全策略,我无法提供相关内容。'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 记录使用情况
|
|
142
|
+
if (data.usageMetadata) {
|
|
143
|
+
this.ctx.logger.debug(`使用了 ${data.usageMetadata.promptTokenCount} 提示令牌和 ${data.usageMetadata.candidatesTokenCount} 回复令牌`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return data.candidates[0].content.parts[0].text.trim()
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.ctx.logger.error('Gemini请求失败:', error)
|
|
149
|
+
if (error.name === 'AbortError') {
|
|
150
|
+
throw new Error('请求超时,请稍后再试')
|
|
151
|
+
} else if (error.message.includes('429')) {
|
|
152
|
+
throw new Error('请求过于频繁,请稍后再试')
|
|
153
|
+
} else if (error.message.includes('400') && error.message.includes('Invalid API key')) {
|
|
154
|
+
throw new Error('API密钥无效')
|
|
155
|
+
} else if (error.message.includes('quota')) {
|
|
156
|
+
throw new Error('API配额已用尽')
|
|
157
|
+
}
|
|
158
|
+
throw error
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 当插件卸载时清理资源
|
|
164
|
+
*/
|
|
165
|
+
async dispose() {
|
|
166
|
+
// Gemini适配器不需要特别的清理工作
|
|
167
|
+
this.ctx.logger.debug('Gemini适配器资源已清理')
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = GeminiAdapter
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const fetch = require('node-fetch')
|
|
2
|
+
const AbortController = require('abort-controller')
|
|
3
|
+
|
|
4
|
+
class OpenAIAdapter {
|
|
5
|
+
constructor(ctx, config) {
|
|
6
|
+
this.ctx = ctx
|
|
7
|
+
this.config = config
|
|
8
|
+
this.apiKey = config.apiKey
|
|
9
|
+
this.apiEndpoint = config.apiEndpoint || 'https://api.openai.com/v1'
|
|
10
|
+
this.modelName = config.modelName || 'gpt-3.5-turbo'
|
|
11
|
+
this.temperature = config.temperature ?? 0.7
|
|
12
|
+
|
|
13
|
+
// 验证必要参数
|
|
14
|
+
if (!this.apiKey) {
|
|
15
|
+
ctx.logger.error('OpenAI API密钥未设置')
|
|
16
|
+
throw new Error('OpenAI API密钥未设置')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
ctx.logger.info(`OpenAI适配器已初始化,使用模型: ${this.modelName}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 生成回复
|
|
24
|
+
* @param {Array} messages - 对话历史消息
|
|
25
|
+
* @param {Object} session - Koishi会话对象
|
|
26
|
+
* @returns {Promise<string>} - 生成的回复文本
|
|
27
|
+
*/
|
|
28
|
+
async generateResponse(messages, session) {
|
|
29
|
+
this.ctx.logger.debug(`向OpenAI发送请求,消息数: ${messages.length}`)
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// 设置超时
|
|
33
|
+
const timeout = 60000 // 60秒超时
|
|
34
|
+
const controller = new AbortController()
|
|
35
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
36
|
+
|
|
37
|
+
// 构建API请求
|
|
38
|
+
const response = await fetch(`${this.apiEndpoint}/chat/completions`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
43
|
+
},
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
model: this.modelName,
|
|
46
|
+
messages: messages,
|
|
47
|
+
temperature: this.temperature,
|
|
48
|
+
user: session.userId, // 传递用户ID以便OpenAI分析滥用情况
|
|
49
|
+
}),
|
|
50
|
+
signal: controller.signal
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// 清除超时定时器
|
|
54
|
+
clearTimeout(timeoutId)
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const error = await response.json().catch(() => ({}))
|
|
58
|
+
this.ctx.logger.error(`OpenAI API请求失败: ${response.status} ${response.statusText}`, error)
|
|
59
|
+
throw new Error(`API请求失败: ${response.status} ${response.statusText}${error.error?.message ? ': ' + error.error.message : ''}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const data = await response.json()
|
|
63
|
+
|
|
64
|
+
// 检查响应格式
|
|
65
|
+
if (!data.choices || !data.choices[0] || !data.choices[0].message) {
|
|
66
|
+
this.ctx.logger.error('OpenAI返回了无效的响应格式', data)
|
|
67
|
+
throw new Error('收到无效的API响应')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 记录令牌使用情况
|
|
71
|
+
if (data.usage) {
|
|
72
|
+
this.ctx.logger.debug(`使用了 ${data.usage.total_tokens} 个令牌 (提示: ${data.usage.prompt_tokens}, 完成: ${data.usage.completion_tokens})`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return data.choices[0].message.content.trim()
|
|
76
|
+
} catch (error) {
|
|
77
|
+
this.ctx.logger.error('OpenAI请求失败:', error)
|
|
78
|
+
if (error.name === 'AbortError') {
|
|
79
|
+
throw new Error('请求超时,请稍后再试')
|
|
80
|
+
} else if (error.message.includes('429')) {
|
|
81
|
+
throw new Error('请求过于频繁,请稍后再试')
|
|
82
|
+
} else if (error.message.includes('401')) {
|
|
83
|
+
throw new Error('API密钥无效或已过期')
|
|
84
|
+
} else if (error.message.includes('insufficient_quota')) {
|
|
85
|
+
throw new Error('API配额已用尽')
|
|
86
|
+
}
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 当插件卸载时清理资源
|
|
93
|
+
*/
|
|
94
|
+
async dispose() {
|
|
95
|
+
// OpenAI适配器不需要特别的清理工作
|
|
96
|
+
this.ctx.logger.debug('OpenAI适配器资源已清理')
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = OpenAIAdapter
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-chat-model",
|
|
3
|
+
"description": "在没有命令匹配时自动调用大型语言模型进行对话,支持OpenAI、Claude、Gemini等",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"typings": "index.d.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"chatbot",
|
|
13
|
+
"koishi",
|
|
14
|
+
"plugin",
|
|
15
|
+
"ai",
|
|
16
|
+
"openai",
|
|
17
|
+
"gpt",
|
|
18
|
+
"claude",
|
|
19
|
+
"gemini",
|
|
20
|
+
"llm"
|
|
21
|
+
],
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"koishi": "^4.13.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"abort-controller": "^3.0.0",
|
|
27
|
+
"node-fetch": "^2.6.9"
|
|
28
|
+
},
|
|
29
|
+
"koishi": {
|
|
30
|
+
"description": {
|
|
31
|
+
"zh": "当消息没有触发其他命令时,使用大型语言模型进行对话,支持记忆上下文",
|
|
32
|
+
"en": "Use LLM for conversations when messages don't match other commands, with context memory"
|
|
33
|
+
},
|
|
34
|
+
"locales": {
|
|
35
|
+
"zh": {
|
|
36
|
+
"name": "对话模型",
|
|
37
|
+
"description": "当消息没有触发其他命令时,使用大型语言模型进行对话,支持记忆上下文"
|
|
38
|
+
},
|
|
39
|
+
"en": {
|
|
40
|
+
"name": "Chat Model",
|
|
41
|
+
"description": "Use LLM for conversations when messages don't match other commands, with context memory"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/yourusername/koishi-plugin-chat-model.git"
|
|
48
|
+
},
|
|
49
|
+
"author": "YourName"
|
|
50
|
+
}
|