koishi-plugin-chatluna-anuneko-api-adapter 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/src/index.ts ADDED
@@ -0,0 +1,265 @@
1
+ import { Context, Logger, Schema } from 'koishi'
2
+ import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat'
3
+ import {
4
+ ChatLunaError,
5
+ ChatLunaErrorCode
6
+ } from 'koishi-plugin-chatluna/utils/error'
7
+ import { createLogger } from 'koishi-plugin-chatluna/utils/logger'
8
+ import { AnunekoClient } from './anuneko-client'
9
+ import { initializeLogger } from './logger'
10
+
11
+ export let logger: Logger
12
+ export let anunekoClient: any = null
13
+ export const reusable = true
14
+ export const usage = `
15
+ <p><strong>零成本、快速体验Chatluna</strong>。</p>
16
+ <ul>
17
+ <li><strong>API来源:</strong> anuneko.com</li>
18
+ <li>
19
+ <strong>接口地址:</strong>
20
+ <a href="https://anuneko.com" target="_blank" rel="noopener noreferrer">https://anuneko.com</a>
21
+ </li>
22
+ </ul>
23
+ <p><strong>请注意:</strong></p>
24
+ <p>该服务需要配置有效的 x-token 才能使用。支持橘猫(Orange Cat)和黑猫(Exotic Shorthair)两种模型。</p>
25
+ `
26
+
27
+ export function apply(ctx: Context, config: Config) {
28
+ logger = createLogger(ctx, 'chatluna-anuneko-api-adapter')
29
+ initializeLogger(logger, config)
30
+
31
+ // 测试命令
32
+ ctx.command('anuneko <message:text>', '测试 anuneko API')
33
+ .action(async ({ session }, message) => {
34
+ if (!message) {
35
+ return '请输入消息内容,例如:/anuneko 你好'
36
+ }
37
+
38
+ try {
39
+ const headers = {
40
+ 'accept': '*/*',
41
+ 'content-type': 'application/json',
42
+ 'origin': 'https://anuneko.com',
43
+ 'referer': 'https://anuneko.com/',
44
+ 'user-agent': 'Mozilla/5.0',
45
+ 'x-app_id': 'com.anuttacon.neko',
46
+ 'x-client_type': '4',
47
+ 'x-device_id': '7b75a432-6b24-48ad-b9d3-3dc57648e3e3',
48
+ 'x-token': config.xToken
49
+ }
50
+
51
+ if (config.cookie) {
52
+ headers['Cookie'] = config.cookie
53
+ }
54
+
55
+ // 创建新会话
56
+ logger.info('创建新会话...')
57
+ const createResponse = await fetch('https://anuneko.com/api/v1/chat', {
58
+ method: 'POST',
59
+ headers,
60
+ body: JSON.stringify({ model: 'Orange Cat' })
61
+ })
62
+
63
+ const createData = await createResponse.json()
64
+ const chatId = createData.chat_id || createData.id
65
+ if (!chatId) {
66
+ return '❌ 创建会话失败'
67
+ }
68
+
69
+ logger.info('会话创建成功,ID:', chatId)
70
+
71
+ // 发送消息并获取流式响应
72
+ const url = `https://anuneko.com/api/v1/msg/${chatId}/stream`
73
+ const data = { contents: [message] }
74
+
75
+ logger.info('发送消息...')
76
+ const response = await fetch(url, {
77
+ method: 'POST',
78
+ headers,
79
+ body: JSON.stringify(data)
80
+ })
81
+
82
+ if (!response.ok) {
83
+ return `❌ 请求失败: ${response.status} ${response.statusText}`
84
+ }
85
+
86
+ let result = ''
87
+ let currentMsgId: string | null = null
88
+
89
+ // 处理流式响应
90
+ const reader = response.body.getReader()
91
+ const decoder = new TextDecoder()
92
+
93
+ while (true) {
94
+ const { done, value } = await reader.read()
95
+ if (done) break
96
+
97
+ const chunkStr = decoder.decode(value, { stream: true })
98
+ logger.info('收到数据块:', chunkStr.substring(0, 200))
99
+
100
+ const lines = chunkStr.split('\n')
101
+
102
+ for (const line of lines) {
103
+ if (!line.trim()) {
104
+ continue
105
+ }
106
+
107
+ logger.info('处理行:', line.substring(0, 100))
108
+
109
+ if (!line.startsWith('data: ')) {
110
+ logger.warn('非 data: 格式的行:', line)
111
+ continue
112
+ }
113
+
114
+ const rawJson = line.substring(6).trim()
115
+ if (!rawJson) continue
116
+
117
+ try {
118
+ const j = JSON.parse(rawJson)
119
+ logger.info('解析的 JSON:', JSON.stringify(j))
120
+
121
+ if (j.msg_id) {
122
+ currentMsgId = j.msg_id
123
+ logger.info('更新 msg_id:', currentMsgId)
124
+ }
125
+
126
+ // 处理多分支内容
127
+ if (j.c && Array.isArray(j.c)) {
128
+ logger.info('处理多分支内容')
129
+ for (const choice of j.c) {
130
+ const idx = choice.c ?? 0
131
+ if (idx === 0 && choice.v) {
132
+ result += choice.v
133
+ logger.info('添加内容:', choice.v)
134
+ }
135
+ }
136
+ }
137
+ // 处理常规内容
138
+ else if (j.v && typeof j.v === 'string') {
139
+ result += j.v
140
+ logger.info('添加常规内容:', j.v)
141
+ }
142
+ } catch (error) {
143
+ logger.error('解析 JSON 失败:', rawJson, error)
144
+ }
145
+ }
146
+ }
147
+
148
+ logger.info('最终结果长度:', result.length)
149
+ logger.info('最终结果内容:', result)
150
+
151
+ // 自动选择分支
152
+ if (currentMsgId) {
153
+ await fetch('https://anuneko.com/api/v1/msg/select-choice', {
154
+ method: 'POST',
155
+ headers,
156
+ body: JSON.stringify({ msg_id: currentMsgId, choice_idx: 0 })
157
+ })
158
+ }
159
+
160
+ logger.info('收到完整响应')
161
+ return result || '❌ 未收到响应'
162
+ } catch (error) {
163
+ logger.error('请求失败:', error)
164
+ return `❌ 请求失败: ${error.message}`
165
+ }
166
+ })
167
+
168
+ // 清理命令
169
+ ctx.command('anuneko-clean', '清理当前频道的 anuneko 对话记录')
170
+ .action(async ({ session }) => {
171
+ try {
172
+ if (!anunekoClient) {
173
+ return '❌ anuneko 客户端未初始化'
174
+ }
175
+
176
+ // 使用 channelId 作为会话标识
177
+ const sessionKey = session.channelId || session.userId
178
+ const requester = anunekoClient._requester
179
+
180
+ if (requester && typeof requester.clearSession === 'function') {
181
+ const cleared = requester.clearSession(sessionKey)
182
+ if (cleared) {
183
+ return '✅ 已清理当前频道的对话记录,下次对话将创建新会话'
184
+ } else {
185
+ return '✅ 当前频道还没有对话记录'
186
+ }
187
+ }
188
+
189
+ return '❌ 无法访问会话管理器'
190
+ } catch (error) {
191
+ logger.error('清理失败:', error)
192
+ return `❌ 清理失败: ${error.message}`
193
+ }
194
+ })
195
+
196
+ ctx.on('ready', async () => {
197
+ if (config.platform == null || config.platform.length < 1) {
198
+ throw new ChatLunaError(
199
+ ChatLunaErrorCode.UNKNOWN_ERROR,
200
+ new Error('Cannot find any platform')
201
+ )
202
+ }
203
+
204
+ const platform = config.platform
205
+
206
+ const plugin = new ChatLunaPlugin(ctx, config, platform)
207
+
208
+ plugin.parseConfig((config) => {
209
+ // 创建一个假的客户端配置
210
+ return [
211
+ {
212
+ apiKey: config.xToken || 'any',
213
+ apiEndpoint: 'https://anuneko.com',
214
+ platform,
215
+ chatLimit: config.chatTimeLimit,
216
+ timeout: config.timeout,
217
+ maxRetries: config.maxRetries,
218
+ concurrentMaxSize: config.chatConcurrentMaxSize
219
+ }
220
+ ]
221
+ })
222
+
223
+ plugin.registerClient(() => {
224
+ const client = new AnunekoClient(ctx, config, plugin)
225
+ if (!anunekoClient) {
226
+ anunekoClient = client
227
+ }
228
+ return client
229
+ })
230
+
231
+ await plugin.initClient()
232
+ })
233
+ }
234
+
235
+ export interface Config extends ChatLunaPlugin.Config {
236
+ platform: string
237
+ xToken: string
238
+ cookie?: string
239
+ loggerinfo: boolean
240
+ }
241
+
242
+ export const Config: Schema<Config> = Schema.intersect([
243
+ ChatLunaPlugin.Config,
244
+ Schema.object({
245
+ platform: Schema.string().default('anuneko'),
246
+ xToken: Schema.string()
247
+ .required()
248
+ .role('textarea', { rows: [2, 4] })
249
+ .description('anuneko API 的 x-token'),
250
+ cookie: Schema.string()
251
+ .role('textarea', { rows: [2, 4] })
252
+ .description('anuneko API 的 Cookie(可选)'),
253
+ loggerinfo: Schema.boolean()
254
+ .default(false)
255
+ .description('日志调试模式')
256
+ .experimental()
257
+ })
258
+ ]).i18n({
259
+ 'zh-CN': require('./locales/zh-CN.schema.yml'),
260
+ 'en-US': require('./locales/en-US.schema.yml')
261
+ }) as Schema<Config>
262
+
263
+ export const inject = ['chatluna']
264
+
265
+ export const name = 'chatluna-anuneko-api-adapter'
@@ -0,0 +1,4 @@
1
+ platform: Platform name
2
+ xToken: x-token for anuneko API
3
+ cookie: Cookie for anuneko API (optional)
4
+ loggerinfo: Logger debug mode
@@ -0,0 +1,4 @@
1
+ platform: 平台名称
2
+ xToken: anuneko API 的 x-token
3
+ cookie: anuneko API 的 Cookie(可选)
4
+ loggerinfo: 日志调试模式
package/src/logger.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { Logger } from 'koishi'
2
+ import { Config } from './index'
3
+
4
+ export let logger: Logger
5
+ export let config: Config
6
+
7
+ // 初始化日志记录器
8
+ export function initializeLogger(newLogger: Logger, newConfig: Config) {
9
+ logger = newLogger
10
+ config = newConfig
11
+ }
12
+
13
+ // 输出调试信息
14
+ export function logInfo(...args: any[]) {
15
+ if (config?.loggerinfo) {
16
+ (logger?.info as (...args: any[]) => void)?.(...args)
17
+ }
18
+ }