koishi-plugin-chatluna-anuneko-api-adapter 1.0.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/anuneko-requester.d.ts +1 -5
- package/lib/index.d.ts +4 -1
- package/lib/index.js +233 -81
- package/lib/session-manager.d.ts +20 -0
- package/package.json +1 -1
- package/src/anuneko-requester.ts +58 -73
- package/src/index.ts +66 -21
- package/src/locales/en-US.schema.yml +2 -0
- package/src/locales/zh-CN.schema.yml +2 -0
- package/src/session-manager.ts +166 -0
|
@@ -6,14 +6,10 @@ import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat';
|
|
|
6
6
|
import { Context } from 'koishi';
|
|
7
7
|
export declare class AnunekoRequester extends ModelRequester {
|
|
8
8
|
_pluginConfig: Config;
|
|
9
|
-
private sessionMap;
|
|
10
|
-
private modelMap;
|
|
11
9
|
constructor(ctx: Context, _configPool: ClientConfigPool<ClientConfig>, _pluginConfig: Config, _plugin: ChatLunaPlugin);
|
|
12
|
-
clearSession(userId: string): boolean;
|
|
13
|
-
clearAllSessions(): number;
|
|
14
10
|
buildHeaders(): Record<string, string>;
|
|
15
11
|
private createNewSession;
|
|
16
|
-
private
|
|
12
|
+
private getOrCreateSession;
|
|
17
13
|
private sendChoice;
|
|
18
14
|
completionStreamInternal(params: ModelRequestParams): AsyncGenerator<ChatGenerationChunk>;
|
|
19
15
|
get logger(): import("reggol");
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Context, Logger, Schema } from 'koishi';
|
|
2
2
|
import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat';
|
|
3
|
+
import { SessionManager } from './session-manager';
|
|
3
4
|
export declare let logger: Logger;
|
|
4
5
|
export declare let anunekoClient: any;
|
|
5
|
-
export declare
|
|
6
|
+
export declare let sessionManager: SessionManager | null;
|
|
7
|
+
export declare const reusable = false;
|
|
6
8
|
export declare const usage = "\n<p><strong>\u96F6\u6210\u672C\u3001\u5FEB\u901F\u4F53\u9A8CChatluna</strong>\u3002</p>\n<ul>\n<li><strong>API\u6765\u6E90\uFF1A</strong> anuneko.com</li>\n<li>\n<strong>\u63A5\u53E3\u5730\u5740\uFF1A</strong>\n<a href=\"https://anuneko.com\" target=\"_blank\" rel=\"noopener noreferrer\">https://anuneko.com</a>\n</li>\n</ul>\n<p><strong>\u8BF7\u6CE8\u610F\uFF1A</strong></p>\n<p>\u8BE5\u670D\u52A1\u9700\u8981\u914D\u7F6E\u6709\u6548\u7684 x-token \u624D\u80FD\u4F7F\u7528\u3002</p>\n<p>\u8BE5\u670D\u52A1\u53EF\u80FD\u9700\u8981\u79D1\u5B66\u4E0A\u7F51\u3002</p>\n<p>\u652F\u6301\u6A58\u732B(Orange Cat)\u548C\u9ED1\u732B(Exotic Shorthair)\u4E24\u79CD\u6A21\u578B\u3002</p>\n\n---\n\nx-token \u83B7\u53D6\u65B9\u6CD5\u89C1\u4ED3\u5E93\u6587\u4EF6 https://github.com/koishi-shangxue-plugins/koishi-shangxue-apps/blob/main/plugins/chatluna-anuneko-api-adapter/data/2025-12-23_19-01-43.png\n";
|
|
7
9
|
export declare function apply(ctx: Context, config: Config): void;
|
|
8
10
|
export interface Config extends ChatLunaPlugin.Config {
|
|
@@ -11,6 +13,7 @@ export interface Config extends ChatLunaPlugin.Config {
|
|
|
11
13
|
cookie?: string;
|
|
12
14
|
loggerinfo: boolean;
|
|
13
15
|
requestTimeout: number;
|
|
16
|
+
sessionMode: 'shared' | 'per-user' | 'always-new';
|
|
14
17
|
}
|
|
15
18
|
export declare const Config: Schema<Config>;
|
|
16
19
|
export declare const inject: string[];
|
package/lib/index.js
CHANGED
|
@@ -23,14 +23,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
23
23
|
// src/locales/zh-CN.schema.yml
|
|
24
24
|
var require_zh_CN_schema = __commonJS({
|
|
25
25
|
"src/locales/zh-CN.schema.yml"(exports2, module2) {
|
|
26
|
-
module2.exports = { platform: "平台名称", xToken: "anuneko API 的 x-token", cookie: "anuneko API 的 Cookie(可选)", loggerinfo: "日志调试模式" };
|
|
26
|
+
module2.exports = { platform: "平台名称", xToken: "anuneko API 的 x-token", cookie: "anuneko API 的 Cookie(可选)", sessionMode: "会话管理模式", loggerinfo: "日志调试模式", requestTimeout: "请求超时时间" };
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
// src/locales/en-US.schema.yml
|
|
31
31
|
var require_en_US_schema = __commonJS({
|
|
32
32
|
"src/locales/en-US.schema.yml"(exports2, module2) {
|
|
33
|
-
module2.exports = { platform: "Platform name", xToken: "x-token for anuneko API", cookie: "Cookie for anuneko API (optional)", loggerinfo: "Logger debug mode" };
|
|
33
|
+
module2.exports = { platform: "Platform name", xToken: "x-token for anuneko API", cookie: "Cookie for anuneko API (optional)", sessionMode: "Session management mode", loggerinfo: "Logger debug mode", requestTimeout: "Request timeout" };
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -44,13 +44,14 @@ __export(src_exports, {
|
|
|
44
44
|
logger: () => logger2,
|
|
45
45
|
name: () => name,
|
|
46
46
|
reusable: () => reusable,
|
|
47
|
+
sessionManager: () => sessionManager,
|
|
47
48
|
usage: () => usage
|
|
48
49
|
});
|
|
49
50
|
module.exports = __toCommonJS(src_exports);
|
|
50
51
|
var import_koishi = require("koishi");
|
|
51
52
|
var import_chat = require("koishi-plugin-chatluna/services/chat");
|
|
52
53
|
var import_error2 = require("koishi-plugin-chatluna/utils/error");
|
|
53
|
-
var
|
|
54
|
+
var import_logger4 = require("koishi-plugin-chatluna/utils/logger");
|
|
54
55
|
|
|
55
56
|
// src/anuneko-client.ts
|
|
56
57
|
var import_client = require("koishi-plugin-chatluna/llm-core/platform/client");
|
|
@@ -88,24 +89,6 @@ var AnunekoRequester = class extends import_api.ModelRequester {
|
|
|
88
89
|
static {
|
|
89
90
|
__name(this, "AnunekoRequester");
|
|
90
91
|
}
|
|
91
|
-
// 存储每个用户的会话ID
|
|
92
|
-
sessionMap = /* @__PURE__ */ new Map();
|
|
93
|
-
// 存储每个用户的当前模型
|
|
94
|
-
modelMap = /* @__PURE__ */ new Map();
|
|
95
|
-
// 清理指定用户的会话
|
|
96
|
-
clearSession(userId) {
|
|
97
|
-
const hasSession = this.sessionMap.has(userId);
|
|
98
|
-
this.sessionMap.delete(userId);
|
|
99
|
-
this.modelMap.delete(userId);
|
|
100
|
-
return hasSession;
|
|
101
|
-
}
|
|
102
|
-
// 清理所有会话
|
|
103
|
-
clearAllSessions() {
|
|
104
|
-
const count = this.sessionMap.size;
|
|
105
|
-
this.sessionMap.clear();
|
|
106
|
-
this.modelMap.clear();
|
|
107
|
-
return count;
|
|
108
|
-
}
|
|
109
92
|
// 构建请求头
|
|
110
93
|
buildHeaders() {
|
|
111
94
|
const headers = {
|
|
@@ -125,7 +108,7 @@ var AnunekoRequester = class extends import_api.ModelRequester {
|
|
|
125
108
|
return headers;
|
|
126
109
|
}
|
|
127
110
|
// 创建新会话
|
|
128
|
-
async createNewSession(
|
|
111
|
+
async createNewSession(modelName) {
|
|
129
112
|
const headers = this.buildHeaders();
|
|
130
113
|
const data = { model: modelName };
|
|
131
114
|
try {
|
|
@@ -140,10 +123,7 @@ var AnunekoRequester = class extends import_api.ModelRequester {
|
|
|
140
123
|
const responseData = await response.json();
|
|
141
124
|
const chatId = responseData.chat_id || responseData.id;
|
|
142
125
|
if (chatId) {
|
|
143
|
-
this.sessionMap.set(userId, chatId);
|
|
144
|
-
this.modelMap.set(userId, modelName);
|
|
145
126
|
logInfo("New session created with ID:", chatId);
|
|
146
|
-
await this.switchModel(userId, chatId, modelName);
|
|
147
127
|
return chatId;
|
|
148
128
|
}
|
|
149
129
|
} catch (error) {
|
|
@@ -151,28 +131,38 @@ var AnunekoRequester = class extends import_api.ModelRequester {
|
|
|
151
131
|
}
|
|
152
132
|
return null;
|
|
153
133
|
}
|
|
154
|
-
//
|
|
155
|
-
async
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
134
|
+
// 根据配置获取或创建会话
|
|
135
|
+
async getOrCreateSession(modelName, conversationId) {
|
|
136
|
+
if (!sessionManager) {
|
|
137
|
+
logInfo("Session manager not initialized, creating new session");
|
|
138
|
+
return await this.createNewSession(modelName);
|
|
139
|
+
}
|
|
140
|
+
const mode = this._pluginConfig.sessionMode;
|
|
141
|
+
let sessionKey;
|
|
142
|
+
switch (mode) {
|
|
143
|
+
case "shared":
|
|
144
|
+
sessionKey = `shared-${modelName}`;
|
|
145
|
+
break;
|
|
146
|
+
case "per-user":
|
|
147
|
+
sessionKey = `user-${conversationId}-${modelName}`;
|
|
148
|
+
break;
|
|
149
|
+
case "always-new":
|
|
150
|
+
logInfo("Mode: always-new, creating new session");
|
|
151
|
+
return await this.createNewSession(modelName);
|
|
152
|
+
default:
|
|
153
|
+
sessionKey = `user-${conversationId}-${modelName}`;
|
|
154
|
+
}
|
|
155
|
+
const existingSession = sessionManager.getSession(sessionKey);
|
|
156
|
+
if (existingSession) {
|
|
157
|
+
logInfo("Using existing session:", existingSession, "for key:", sessionKey);
|
|
158
|
+
return existingSession;
|
|
174
159
|
}
|
|
175
|
-
|
|
160
|
+
const newSession = await this.createNewSession(modelName);
|
|
161
|
+
if (newSession) {
|
|
162
|
+
sessionManager.setSession(sessionKey, newSession, modelName);
|
|
163
|
+
logInfo("Created and saved new session:", newSession, "for key:", sessionKey);
|
|
164
|
+
}
|
|
165
|
+
return newSession;
|
|
176
166
|
}
|
|
177
167
|
// 自动选择分支
|
|
178
168
|
async sendChoice(msgId) {
|
|
@@ -204,24 +194,20 @@ var AnunekoRequester = class extends import_api.ModelRequester {
|
|
|
204
194
|
return;
|
|
205
195
|
}
|
|
206
196
|
const prompt = lastMessage.content;
|
|
207
|
-
const sessionKey = lastMessage.channelId || lastMessage.id || "default";
|
|
208
|
-
logInfo("使用会话标识:", sessionKey);
|
|
209
197
|
let modelName = "Orange Cat";
|
|
210
198
|
if (params.model.includes("exotic") || params.model.includes("shorthair")) {
|
|
211
199
|
modelName = "Exotic Shorthair";
|
|
212
200
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
201
|
+
const conversationId = params.id || "default";
|
|
202
|
+
logInfo("Conversation ID from chatluna:", conversationId);
|
|
203
|
+
const sessionId = await this.getOrCreateSession(modelName, conversationId);
|
|
204
|
+
if (!sessionId) {
|
|
205
|
+
const errorText = "获取会话失败,请稍后再试。";
|
|
206
|
+
yield new import_outputs.ChatGenerationChunk({
|
|
207
|
+
text: errorText,
|
|
208
|
+
message: new import_messages.AIMessageChunk({ content: errorText })
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
225
211
|
}
|
|
226
212
|
const headers = this.buildHeaders();
|
|
227
213
|
const url = `https://anuneko.com/api/v1/msg/${sessionId}/stream`;
|
|
@@ -398,10 +384,147 @@ var AnunekoClient = class extends import_client.PlatformModelAndEmbeddingsClient
|
|
|
398
384
|
}
|
|
399
385
|
};
|
|
400
386
|
|
|
387
|
+
// src/session-manager.ts
|
|
388
|
+
var import_node_fs = require("node:fs");
|
|
389
|
+
var import_node_path = require("node:path");
|
|
390
|
+
var SessionManager = class {
|
|
391
|
+
constructor(ctx, dataDir) {
|
|
392
|
+
this.ctx = ctx;
|
|
393
|
+
this.dataDir = dataDir;
|
|
394
|
+
this.tempFilePath = (0, import_node_path.join)(dataDir, "temp.json");
|
|
395
|
+
this.ensureDataDir();
|
|
396
|
+
this.loadSessions();
|
|
397
|
+
}
|
|
398
|
+
static {
|
|
399
|
+
__name(this, "SessionManager");
|
|
400
|
+
}
|
|
401
|
+
sessions = /* @__PURE__ */ new Map();
|
|
402
|
+
tempFilePath;
|
|
403
|
+
saveTimeout = null;
|
|
404
|
+
disposed = false;
|
|
405
|
+
// 确保数据目录存在
|
|
406
|
+
async ensureDataDir() {
|
|
407
|
+
try {
|
|
408
|
+
await import_node_fs.promises.mkdir(this.dataDir, { recursive: true });
|
|
409
|
+
logInfo("数据目录已确保存在:", this.dataDir);
|
|
410
|
+
} catch (error) {
|
|
411
|
+
this.ctx.logger.error("创建数据目录失败:", error);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// 从文件加载会话数据
|
|
415
|
+
async loadSessions() {
|
|
416
|
+
try {
|
|
417
|
+
const data = await import_node_fs.promises.readFile(this.tempFilePath, "utf-8");
|
|
418
|
+
const storage = JSON.parse(data);
|
|
419
|
+
this.sessions = new Map(Object.entries(storage.sessions || {}));
|
|
420
|
+
logInfo("会话数据加载成功,共", this.sessions.size, "个会话");
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (error.code === "ENOENT") {
|
|
423
|
+
logInfo("会话文件不存在,将创建新文件");
|
|
424
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
425
|
+
} else {
|
|
426
|
+
this.ctx.logger.error("加载会话数据失败:", error);
|
|
427
|
+
this.sessions = /* @__PURE__ */ new Map();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// 保存会话数据到文件(带防抖)
|
|
432
|
+
scheduleSave() {
|
|
433
|
+
if (this.disposed) return;
|
|
434
|
+
if (this.saveTimeout) {
|
|
435
|
+
clearTimeout(this.saveTimeout);
|
|
436
|
+
}
|
|
437
|
+
this.saveTimeout = setTimeout(() => {
|
|
438
|
+
this.saveSessions();
|
|
439
|
+
}, 500);
|
|
440
|
+
}
|
|
441
|
+
// 实际保存到文件
|
|
442
|
+
async saveSessions() {
|
|
443
|
+
if (this.disposed) return;
|
|
444
|
+
try {
|
|
445
|
+
const storage = {
|
|
446
|
+
sessions: Object.fromEntries(this.sessions)
|
|
447
|
+
};
|
|
448
|
+
await import_node_fs.promises.writeFile(
|
|
449
|
+
this.tempFilePath,
|
|
450
|
+
JSON.stringify(storage, null, 2),
|
|
451
|
+
"utf-8"
|
|
452
|
+
);
|
|
453
|
+
logInfo("会话数据已保存");
|
|
454
|
+
} catch (error) {
|
|
455
|
+
this.ctx.logger.error("保存会话数据失败:", error);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// 获取会话 ID
|
|
459
|
+
getSession(key) {
|
|
460
|
+
const session = this.sessions.get(key);
|
|
461
|
+
if (session) {
|
|
462
|
+
session.lastUsed = Date.now();
|
|
463
|
+
this.scheduleSave();
|
|
464
|
+
logInfo("获取已存在的会话:", key, "->", session.chatId);
|
|
465
|
+
return session.chatId;
|
|
466
|
+
}
|
|
467
|
+
logInfo("未找到会话:", key);
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
// 设置会话 ID
|
|
471
|
+
setSession(key, chatId, modelName) {
|
|
472
|
+
this.sessions.set(key, {
|
|
473
|
+
chatId,
|
|
474
|
+
modelName,
|
|
475
|
+
lastUsed: Date.now()
|
|
476
|
+
});
|
|
477
|
+
this.scheduleSave();
|
|
478
|
+
logInfo("保存新会话:", key, "->", chatId);
|
|
479
|
+
}
|
|
480
|
+
// 删除会话
|
|
481
|
+
deleteSession(key) {
|
|
482
|
+
const deleted = this.sessions.delete(key);
|
|
483
|
+
if (deleted) {
|
|
484
|
+
this.scheduleSave();
|
|
485
|
+
logInfo("删除会话:", key);
|
|
486
|
+
}
|
|
487
|
+
return deleted;
|
|
488
|
+
}
|
|
489
|
+
// 清理过期会话(超过 24 小时未使用)
|
|
490
|
+
cleanExpiredSessions() {
|
|
491
|
+
const now = Date.now();
|
|
492
|
+
const expireTime = 24 * 60 * 60 * 1e3;
|
|
493
|
+
let cleaned = 0;
|
|
494
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
495
|
+
if (now - session.lastUsed > expireTime) {
|
|
496
|
+
this.sessions.delete(key);
|
|
497
|
+
cleaned++;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (cleaned > 0) {
|
|
501
|
+
this.scheduleSave();
|
|
502
|
+
logInfo("清理了", cleaned, "个过期会话");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// 清空所有会话
|
|
506
|
+
clearAll() {
|
|
507
|
+
this.sessions.clear();
|
|
508
|
+
this.scheduleSave();
|
|
509
|
+
logInfo("已清空所有会话");
|
|
510
|
+
}
|
|
511
|
+
// 销毁时保存并清理
|
|
512
|
+
dispose() {
|
|
513
|
+
this.disposed = true;
|
|
514
|
+
if (this.saveTimeout) {
|
|
515
|
+
clearTimeout(this.saveTimeout);
|
|
516
|
+
this.saveTimeout = null;
|
|
517
|
+
}
|
|
518
|
+
this.saveSessions();
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
401
522
|
// src/index.ts
|
|
523
|
+
var import_node_path2 = require("node:path");
|
|
402
524
|
var logger2;
|
|
403
525
|
var anunekoClient = null;
|
|
404
|
-
var
|
|
526
|
+
var sessionManager = null;
|
|
527
|
+
var reusable = false;
|
|
405
528
|
var usage = `
|
|
406
529
|
<p><strong>零成本、快速体验Chatluna</strong>。</p>
|
|
407
530
|
<ul>
|
|
@@ -421,8 +544,18 @@ var usage = `
|
|
|
421
544
|
x-token 获取方法见仓库文件 https://github.com/koishi-shangxue-plugins/koishi-shangxue-apps/blob/main/plugins/chatluna-anuneko-api-adapter/data/2025-12-23_19-01-43.png
|
|
422
545
|
`;
|
|
423
546
|
function apply(ctx, config2) {
|
|
424
|
-
logger2 = (0,
|
|
547
|
+
logger2 = (0, import_logger4.createLogger)(ctx, "chatluna-anuneko-api-adapter");
|
|
425
548
|
initializeLogger(logger2, config2);
|
|
549
|
+
const dataDir = (0, import_node_path2.join)(ctx.baseDir, "data", "chatluna-anuneko-api-adapter");
|
|
550
|
+
sessionManager = new SessionManager(ctx, dataDir);
|
|
551
|
+
const cleanupInterval = setInterval(() => {
|
|
552
|
+
sessionManager?.cleanExpiredSessions();
|
|
553
|
+
}, 60 * 60 * 1e3);
|
|
554
|
+
ctx.on("dispose", () => {
|
|
555
|
+
clearInterval(cleanupInterval);
|
|
556
|
+
sessionManager?.dispose();
|
|
557
|
+
sessionManager = null;
|
|
558
|
+
});
|
|
426
559
|
ctx.command("anuneko <message:text>", "测试 anuneko API").action(async ({ session }, message) => {
|
|
427
560
|
if (!message) {
|
|
428
561
|
return "请输入消息内容,例如:/anuneko 你好";
|
|
@@ -531,26 +664,39 @@ function apply(ctx, config2) {
|
|
|
531
664
|
return `❌ 请求失败: ${error.message}`;
|
|
532
665
|
}
|
|
533
666
|
});
|
|
534
|
-
ctx.command("anuneko-clean", "
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return "❌ 无法访问会话管理器";
|
|
550
|
-
} catch (error) {
|
|
551
|
-
logger2.error("清理失败:", error);
|
|
552
|
-
return `❌ 清理失败: ${error.message}`;
|
|
667
|
+
ctx.command("anuneko-clean", "清理当前频道的对话记录").action(async ({ session }) => {
|
|
668
|
+
return `对话历史由 ChatLuna 管理,请使用以下命令清理:
|
|
669
|
+
• chatluna room clear - 清空当前房间的对话历史
|
|
670
|
+
• chatluna room delete <房间名称> - 删除指定房间
|
|
671
|
+
• chatluna room list - 查看所有房间`;
|
|
672
|
+
});
|
|
673
|
+
ctx.command("anuneko-session", "管理 Anuneko 会话").action(async () => {
|
|
674
|
+
return `Anuneko 会话管理命令:
|
|
675
|
+
• anuneko-session.clear - 清空所有会话
|
|
676
|
+
• anuneko-session.cleanup - 清理过期会话(24小时未使用)
|
|
677
|
+
• anuneko-session.mode - 查看当前会话模式`;
|
|
678
|
+
});
|
|
679
|
+
ctx.command("anuneko-session.clear", "清空所有 Anuneko 会话").action(async () => {
|
|
680
|
+
if (!sessionManager) {
|
|
681
|
+
return "❌ 会话管理器未初始化";
|
|
553
682
|
}
|
|
683
|
+
sessionManager.clearAll();
|
|
684
|
+
return "✅ 已清空所有 Anuneko 会话";
|
|
685
|
+
});
|
|
686
|
+
ctx.command("anuneko-session.cleanup", "清理过期的 Anuneko 会话").action(async () => {
|
|
687
|
+
if (!sessionManager) {
|
|
688
|
+
return "❌ 会话管理器未初始化";
|
|
689
|
+
}
|
|
690
|
+
sessionManager.cleanExpiredSessions();
|
|
691
|
+
return "✅ 已清理过期会话";
|
|
692
|
+
});
|
|
693
|
+
ctx.command("anuneko-session.mode", "查看当前会话模式").action(async () => {
|
|
694
|
+
const modeText = {
|
|
695
|
+
"shared": "所有用户共享同一个会话",
|
|
696
|
+
"per-user": "根据用户 ID 区分会话",
|
|
697
|
+
"always-new": "每次对话都新建会话"
|
|
698
|
+
};
|
|
699
|
+
return `当前会话模式:${modeText[config2.sessionMode] || config2.sessionMode}`;
|
|
554
700
|
});
|
|
555
701
|
ctx.on("ready", async () => {
|
|
556
702
|
if (config2.platform == null || config2.platform.length < 1) {
|
|
@@ -590,6 +736,11 @@ var Config2 = import_koishi.Schema.intersect([
|
|
|
590
736
|
platform: import_koishi.Schema.string().default("anuneko"),
|
|
591
737
|
xToken: import_koishi.Schema.string().required().role("textarea", { rows: [2, 4] }).description("anuneko API 的 x-token"),
|
|
592
738
|
cookie: import_koishi.Schema.string().role("textarea", { rows: [2, 4] }).description("anuneko API 的 Cookie(可选)"),
|
|
739
|
+
sessionMode: import_koishi.Schema.union([
|
|
740
|
+
import_koishi.Schema.const("shared").description("所有用户共享同一个会话"),
|
|
741
|
+
import_koishi.Schema.const("per-user").description("根据用户 ID 区分会话"),
|
|
742
|
+
import_koishi.Schema.const("always-new").description("每次对话都新建会话")
|
|
743
|
+
]).default("per-user").description("会话管理模式"),
|
|
593
744
|
loggerinfo: import_koishi.Schema.boolean().default(false).description("日志调试模式").experimental(),
|
|
594
745
|
requestTimeout: import_koishi.Schema.number().default(120).description("请求 API 的超时时间,单位为秒。")
|
|
595
746
|
}).description("基础设置"),
|
|
@@ -609,5 +760,6 @@ var name = "chatluna-anuneko-api-adapter";
|
|
|
609
760
|
logger,
|
|
610
761
|
name,
|
|
611
762
|
reusable,
|
|
763
|
+
sessionManager,
|
|
612
764
|
usage
|
|
613
765
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Context } from 'koishi';
|
|
2
|
+
export declare class SessionManager {
|
|
3
|
+
private ctx;
|
|
4
|
+
private dataDir;
|
|
5
|
+
private sessions;
|
|
6
|
+
private tempFilePath;
|
|
7
|
+
private saveTimeout;
|
|
8
|
+
private disposed;
|
|
9
|
+
constructor(ctx: Context, dataDir: string);
|
|
10
|
+
private ensureDataDir;
|
|
11
|
+
private loadSessions;
|
|
12
|
+
private scheduleSave;
|
|
13
|
+
private saveSessions;
|
|
14
|
+
getSession(key: string): string | null;
|
|
15
|
+
setSession(key: string, chatId: string, modelName: string): void;
|
|
16
|
+
deleteSession(key: string): boolean;
|
|
17
|
+
cleanExpiredSessions(): void;
|
|
18
|
+
clearAll(): void;
|
|
19
|
+
dispose(): void;
|
|
20
|
+
}
|
package/package.json
CHANGED
package/src/anuneko-requester.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ClientConfig,
|
|
8
8
|
ClientConfigPool
|
|
9
9
|
} from 'koishi-plugin-chatluna/llm-core/platform/config'
|
|
10
|
-
import { Config, logger } from './index'
|
|
10
|
+
import { Config, logger, sessionManager } from './index'
|
|
11
11
|
import { logInfo } from './logger'
|
|
12
12
|
import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat'
|
|
13
13
|
import { Context } from 'koishi'
|
|
@@ -19,20 +19,11 @@ import {
|
|
|
19
19
|
|
|
20
20
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
21
21
|
|
|
22
|
-
interface KoishiHumanMessage extends HumanMessage {
|
|
23
|
-
channelId?: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
22
|
interface InternalModelRequestParams extends ModelRequestParams {
|
|
27
23
|
input: (HumanMessage | SystemMessage)[]
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
export class AnunekoRequester extends ModelRequester {
|
|
31
|
-
// 存储每个用户的会话ID
|
|
32
|
-
private sessionMap = new Map<string, string>()
|
|
33
|
-
// 存储每个用户的当前模型
|
|
34
|
-
private modelMap = new Map<string, string>()
|
|
35
|
-
|
|
36
27
|
constructor(
|
|
37
28
|
ctx: Context,
|
|
38
29
|
_configPool: ClientConfigPool<ClientConfig>,
|
|
@@ -42,22 +33,6 @@ export class AnunekoRequester extends ModelRequester {
|
|
|
42
33
|
super(ctx, _configPool, _pluginConfig, _plugin)
|
|
43
34
|
}
|
|
44
35
|
|
|
45
|
-
// 清理指定用户的会话
|
|
46
|
-
public clearSession(userId: string): boolean {
|
|
47
|
-
const hasSession = this.sessionMap.has(userId)
|
|
48
|
-
this.sessionMap.delete(userId)
|
|
49
|
-
this.modelMap.delete(userId)
|
|
50
|
-
return hasSession
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// 清理所有会话
|
|
54
|
-
public clearAllSessions(): number {
|
|
55
|
-
const count = this.sessionMap.size
|
|
56
|
-
this.sessionMap.clear()
|
|
57
|
-
this.modelMap.clear()
|
|
58
|
-
return count
|
|
59
|
-
}
|
|
60
|
-
|
|
61
36
|
// 构建请求头
|
|
62
37
|
public buildHeaders() {
|
|
63
38
|
const headers: Record<string, string> = {
|
|
@@ -80,7 +55,7 @@ export class AnunekoRequester extends ModelRequester {
|
|
|
80
55
|
}
|
|
81
56
|
|
|
82
57
|
// 创建新会话
|
|
83
|
-
private async createNewSession(
|
|
58
|
+
private async createNewSession(modelName: string): Promise<string | null> {
|
|
84
59
|
const headers = this.buildHeaders()
|
|
85
60
|
const data = { model: modelName }
|
|
86
61
|
|
|
@@ -97,12 +72,7 @@ export class AnunekoRequester extends ModelRequester {
|
|
|
97
72
|
const responseData = await response.json()
|
|
98
73
|
const chatId = responseData.chat_id || responseData.id
|
|
99
74
|
if (chatId) {
|
|
100
|
-
this.sessionMap.set(userId, chatId)
|
|
101
|
-
this.modelMap.set(userId, modelName)
|
|
102
75
|
logInfo('New session created with ID:', chatId)
|
|
103
|
-
|
|
104
|
-
// 切换模型以确保一致性
|
|
105
|
-
await this.switchModel(userId, chatId, modelName)
|
|
106
76
|
return chatId
|
|
107
77
|
}
|
|
108
78
|
} catch (error) {
|
|
@@ -112,31 +82,52 @@ export class AnunekoRequester extends ModelRequester {
|
|
|
112
82
|
return null
|
|
113
83
|
}
|
|
114
84
|
|
|
115
|
-
//
|
|
116
|
-
private async
|
|
117
|
-
|
|
118
|
-
|
|
85
|
+
// 根据配置获取或创建会话
|
|
86
|
+
private async getOrCreateSession(
|
|
87
|
+
modelName: string,
|
|
88
|
+
conversationId: string
|
|
89
|
+
): Promise<string | null> {
|
|
90
|
+
if (!sessionManager) {
|
|
91
|
+
logInfo('Session manager not initialized, creating new session')
|
|
92
|
+
return await this.createNewSession(modelName)
|
|
93
|
+
}
|
|
119
94
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
95
|
+
const mode = this._pluginConfig.sessionMode
|
|
96
|
+
|
|
97
|
+
// 根据模式确定会话键
|
|
98
|
+
let sessionKey: string
|
|
99
|
+
switch (mode) {
|
|
100
|
+
case 'shared':
|
|
101
|
+
// 所有用户共享同一个会话
|
|
102
|
+
sessionKey = `shared-${modelName}`
|
|
103
|
+
break
|
|
104
|
+
case 'per-user':
|
|
105
|
+
// 根据 conversationId 区分会话
|
|
106
|
+
sessionKey = `user-${conversationId}-${modelName}`
|
|
107
|
+
break
|
|
108
|
+
case 'always-new':
|
|
109
|
+
// 每次都创建新会话
|
|
110
|
+
logInfo('Mode: always-new, creating new session')
|
|
111
|
+
return await this.createNewSession(modelName)
|
|
112
|
+
default:
|
|
113
|
+
sessionKey = `user-${conversationId}-${modelName}`
|
|
114
|
+
}
|
|
129
115
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
116
|
+
// 尝试获取已存在的会话
|
|
117
|
+
const existingSession = sessionManager.getSession(sessionKey)
|
|
118
|
+
if (existingSession) {
|
|
119
|
+
logInfo('Using existing session:', existingSession, 'for key:', sessionKey)
|
|
120
|
+
return existingSession
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 创建新会话并保存
|
|
124
|
+
const newSession = await this.createNewSession(modelName)
|
|
125
|
+
if (newSession) {
|
|
126
|
+
sessionManager.setSession(sessionKey, newSession, modelName)
|
|
127
|
+
logInfo('Created and saved new session:', newSession, 'for key:', sessionKey)
|
|
137
128
|
}
|
|
138
129
|
|
|
139
|
-
return
|
|
130
|
+
return newSession
|
|
140
131
|
}
|
|
141
132
|
|
|
142
133
|
// 自动选择分支
|
|
@@ -166,7 +157,7 @@ export class AnunekoRequester extends ModelRequester {
|
|
|
166
157
|
// 过滤掉所有非 HumanMessage 的消息,并只取最后一条
|
|
167
158
|
const humanMessages = internalParams.input.filter(
|
|
168
159
|
(message) => message instanceof HumanMessage
|
|
169
|
-
)
|
|
160
|
+
)
|
|
170
161
|
const lastMessage = humanMessages.at(-1)
|
|
171
162
|
|
|
172
163
|
logInfo('Receive params from chatluna', JSON.stringify(params, null, 2))
|
|
@@ -177,10 +168,6 @@ export class AnunekoRequester extends ModelRequester {
|
|
|
177
168
|
}
|
|
178
169
|
|
|
179
170
|
const prompt = lastMessage.content as string
|
|
180
|
-
// 使用 channelId 作为会话标识,如果没有则使用 userId
|
|
181
|
-
const sessionKey = lastMessage.channelId || lastMessage.id || 'default'
|
|
182
|
-
|
|
183
|
-
logInfo('使用会话标识:', sessionKey)
|
|
184
171
|
|
|
185
172
|
// 从模型名称推断使用的模型
|
|
186
173
|
let modelName = 'Orange Cat' // 默认橘猫
|
|
@@ -188,21 +175,19 @@ export class AnunekoRequester extends ModelRequester {
|
|
|
188
175
|
modelName = 'Exotic Shorthair'
|
|
189
176
|
}
|
|
190
177
|
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return
|
|
205
|
-
}
|
|
178
|
+
// 从 params.id 获取 conversationId(这是 chatluna 提供的会话 ID)
|
|
179
|
+
const conversationId = params.id || 'default'
|
|
180
|
+
logInfo('Conversation ID from chatluna:', conversationId)
|
|
181
|
+
|
|
182
|
+
// 根据配置获取或创建会话
|
|
183
|
+
const sessionId = await this.getOrCreateSession(modelName, conversationId)
|
|
184
|
+
if (!sessionId) {
|
|
185
|
+
const errorText = '获取会话失败,请稍后再试。'
|
|
186
|
+
yield new ChatGenerationChunk({
|
|
187
|
+
text: errorText,
|
|
188
|
+
message: new AIMessageChunk({ content: errorText })
|
|
189
|
+
})
|
|
190
|
+
return
|
|
206
191
|
}
|
|
207
192
|
|
|
208
193
|
const headers = this.buildHeaders()
|
package/src/index.ts
CHANGED
|
@@ -7,10 +7,13 @@ import {
|
|
|
7
7
|
import { createLogger } from 'koishi-plugin-chatluna/utils/logger'
|
|
8
8
|
import { AnunekoClient } from './anuneko-client'
|
|
9
9
|
import { initializeLogger, logInfo } from './logger'
|
|
10
|
+
import { SessionManager } from './session-manager'
|
|
11
|
+
import { join } from 'node:path'
|
|
10
12
|
|
|
11
13
|
export let logger: Logger
|
|
12
14
|
export let anunekoClient: any = null
|
|
13
|
-
export
|
|
15
|
+
export let sessionManager: SessionManager | null = null
|
|
16
|
+
export const reusable = false
|
|
14
17
|
export const usage = `
|
|
15
18
|
<p><strong>零成本、快速体验Chatluna</strong>。</p>
|
|
16
19
|
<ul>
|
|
@@ -34,6 +37,22 @@ export function apply(ctx: Context, config: Config) {
|
|
|
34
37
|
logger = createLogger(ctx, 'chatluna-anuneko-api-adapter')
|
|
35
38
|
initializeLogger(logger, config)
|
|
36
39
|
|
|
40
|
+
// 初始化会话管理器
|
|
41
|
+
const dataDir = join(ctx.baseDir, 'data', 'chatluna-anuneko-api-adapter')
|
|
42
|
+
sessionManager = new SessionManager(ctx, dataDir)
|
|
43
|
+
|
|
44
|
+
// 定期清理过期会话(每小时执行一次)
|
|
45
|
+
const cleanupInterval = setInterval(() => {
|
|
46
|
+
sessionManager?.cleanExpiredSessions()
|
|
47
|
+
}, 60 * 60 * 1000)
|
|
48
|
+
|
|
49
|
+
// 在插件卸载时清理
|
|
50
|
+
ctx.on('dispose', () => {
|
|
51
|
+
clearInterval(cleanupInterval)
|
|
52
|
+
sessionManager?.dispose()
|
|
53
|
+
sessionManager = null
|
|
54
|
+
})
|
|
55
|
+
|
|
37
56
|
// 测试命令
|
|
38
57
|
ctx.command('anuneko <message:text>', '测试 anuneko API')
|
|
39
58
|
.action(async ({ session }, message) => {
|
|
@@ -176,31 +195,49 @@ export function apply(ctx: Context, config: Config) {
|
|
|
176
195
|
})
|
|
177
196
|
|
|
178
197
|
// 清理命令
|
|
179
|
-
ctx.command('anuneko-clean', '
|
|
198
|
+
ctx.command('anuneko-clean', '清理当前频道的对话记录')
|
|
180
199
|
.action(async ({ session }) => {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
200
|
+
return `对话历史由 ChatLuna 管理,请使用以下命令清理:
|
|
201
|
+
• chatluna room clear - 清空当前房间的对话历史
|
|
202
|
+
• chatluna room delete <房间名称> - 删除指定房间
|
|
203
|
+
• chatluna room list - 查看所有房间`
|
|
204
|
+
})
|
|
185
205
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
206
|
+
// 会话管理命令
|
|
207
|
+
ctx.command('anuneko-session', '管理 Anuneko 会话')
|
|
208
|
+
.action(async () => {
|
|
209
|
+
return `Anuneko 会话管理命令:
|
|
210
|
+
• anuneko-session.clear - 清空所有会话
|
|
211
|
+
• anuneko-session.cleanup - 清理过期会话(24小时未使用)
|
|
212
|
+
• anuneko-session.mode - 查看当前会话模式`
|
|
213
|
+
})
|
|
189
214
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
215
|
+
ctx.command('anuneko-session.clear', '清空所有 Anuneko 会话')
|
|
216
|
+
.action(async () => {
|
|
217
|
+
if (!sessionManager) {
|
|
218
|
+
return '❌ 会话管理器未初始化'
|
|
219
|
+
}
|
|
220
|
+
sessionManager.clearAll()
|
|
221
|
+
return '✅ 已清空所有 Anuneko 会话'
|
|
222
|
+
})
|
|
198
223
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return
|
|
224
|
+
ctx.command('anuneko-session.cleanup', '清理过期的 Anuneko 会话')
|
|
225
|
+
.action(async () => {
|
|
226
|
+
if (!sessionManager) {
|
|
227
|
+
return '❌ 会话管理器未初始化'
|
|
228
|
+
}
|
|
229
|
+
sessionManager.cleanExpiredSessions()
|
|
230
|
+
return '✅ 已清理过期会话'
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
ctx.command('anuneko-session.mode', '查看当前会话模式')
|
|
234
|
+
.action(async () => {
|
|
235
|
+
const modeText = {
|
|
236
|
+
'shared': '所有用户共享同一个会话',
|
|
237
|
+
'per-user': '根据用户 ID 区分会话',
|
|
238
|
+
'always-new': '每次对话都新建会话'
|
|
203
239
|
}
|
|
240
|
+
return `当前会话模式:${modeText[config.sessionMode] || config.sessionMode}`
|
|
204
241
|
})
|
|
205
242
|
|
|
206
243
|
ctx.on('ready', async () => {
|
|
@@ -248,6 +285,7 @@ export interface Config extends ChatLunaPlugin.Config {
|
|
|
248
285
|
cookie?: string
|
|
249
286
|
loggerinfo: boolean
|
|
250
287
|
requestTimeout: number
|
|
288
|
+
sessionMode: 'shared' | 'per-user' | 'always-new'
|
|
251
289
|
}
|
|
252
290
|
|
|
253
291
|
export const Config: Schema<Config> = Schema.intersect([
|
|
@@ -260,6 +298,13 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
260
298
|
cookie: Schema.string()
|
|
261
299
|
.role('textarea', { rows: [2, 4] })
|
|
262
300
|
.description('anuneko API 的 Cookie(可选)'),
|
|
301
|
+
sessionMode: Schema.union([
|
|
302
|
+
Schema.const('shared').description('所有用户共享同一个会话'),
|
|
303
|
+
Schema.const('per-user').description('根据用户 ID 区分会话'),
|
|
304
|
+
Schema.const('always-new').description('每次对话都新建会话')
|
|
305
|
+
])
|
|
306
|
+
.default('per-user')
|
|
307
|
+
.description('会话管理模式'),
|
|
263
308
|
loggerinfo: Schema.boolean()
|
|
264
309
|
.default(false)
|
|
265
310
|
.description('日志调试模式')
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { Context } from 'koishi'
|
|
2
|
+
import { promises as fs } from 'node:fs'
|
|
3
|
+
import { join, dirname } from 'node:path'
|
|
4
|
+
import { logInfo } from './logger'
|
|
5
|
+
|
|
6
|
+
// 会话数据结构
|
|
7
|
+
interface SessionData {
|
|
8
|
+
chatId: string
|
|
9
|
+
modelName: string
|
|
10
|
+
lastUsed: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 会话存储结构
|
|
14
|
+
interface SessionStorage {
|
|
15
|
+
sessions: Record<string, SessionData>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SessionManager {
|
|
19
|
+
private sessions: Map<string, SessionData> = new Map()
|
|
20
|
+
private tempFilePath: string
|
|
21
|
+
private saveTimeout: NodeJS.Timeout | null = null
|
|
22
|
+
private disposed = false
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private ctx: Context,
|
|
26
|
+
private dataDir: string
|
|
27
|
+
) {
|
|
28
|
+
this.tempFilePath = join(dataDir, 'temp.json')
|
|
29
|
+
this.ensureDataDir()
|
|
30
|
+
this.loadSessions()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 确保数据目录存在
|
|
34
|
+
private async ensureDataDir() {
|
|
35
|
+
try {
|
|
36
|
+
await fs.mkdir(this.dataDir, { recursive: true })
|
|
37
|
+
logInfo('数据目录已确保存在:', this.dataDir)
|
|
38
|
+
} catch (error) {
|
|
39
|
+
this.ctx.logger.error('创建数据目录失败:', error)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 从文件加载会话数据
|
|
44
|
+
private async loadSessions() {
|
|
45
|
+
try {
|
|
46
|
+
const data = await fs.readFile(this.tempFilePath, 'utf-8')
|
|
47
|
+
const storage: SessionStorage = JSON.parse(data)
|
|
48
|
+
this.sessions = new Map(Object.entries(storage.sessions || {}))
|
|
49
|
+
logInfo('会话数据加载成功,共', this.sessions.size, '个会话')
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
52
|
+
logInfo('会话文件不存在,将创建新文件')
|
|
53
|
+
this.sessions = new Map()
|
|
54
|
+
} else {
|
|
55
|
+
this.ctx.logger.error('加载会话数据失败:', error)
|
|
56
|
+
this.sessions = new Map()
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 保存会话数据到文件(带防抖)
|
|
62
|
+
private scheduleSave() {
|
|
63
|
+
if (this.disposed) return
|
|
64
|
+
|
|
65
|
+
// 清除之前的定时器
|
|
66
|
+
if (this.saveTimeout) {
|
|
67
|
+
clearTimeout(this.saveTimeout)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 设置新的定时器,500ms 后保存
|
|
71
|
+
this.saveTimeout = setTimeout(() => {
|
|
72
|
+
this.saveSessions()
|
|
73
|
+
}, 500)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 实际保存到文件
|
|
77
|
+
private async saveSessions() {
|
|
78
|
+
if (this.disposed) return
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const storage: SessionStorage = {
|
|
82
|
+
sessions: Object.fromEntries(this.sessions)
|
|
83
|
+
}
|
|
84
|
+
await fs.writeFile(
|
|
85
|
+
this.tempFilePath,
|
|
86
|
+
JSON.stringify(storage, null, 2),
|
|
87
|
+
'utf-8'
|
|
88
|
+
)
|
|
89
|
+
logInfo('会话数据已保存')
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.ctx.logger.error('保存会话数据失败:', error)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 获取会话 ID
|
|
96
|
+
getSession(key: string): string | null {
|
|
97
|
+
const session = this.sessions.get(key)
|
|
98
|
+
if (session) {
|
|
99
|
+
// 更新最后使用时间
|
|
100
|
+
session.lastUsed = Date.now()
|
|
101
|
+
this.scheduleSave()
|
|
102
|
+
logInfo('获取已存在的会话:', key, '->', session.chatId)
|
|
103
|
+
return session.chatId
|
|
104
|
+
}
|
|
105
|
+
logInfo('未找到会话:', key)
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 设置会话 ID
|
|
110
|
+
setSession(key: string, chatId: string, modelName: string) {
|
|
111
|
+
this.sessions.set(key, {
|
|
112
|
+
chatId,
|
|
113
|
+
modelName,
|
|
114
|
+
lastUsed: Date.now()
|
|
115
|
+
})
|
|
116
|
+
this.scheduleSave()
|
|
117
|
+
logInfo('保存新会话:', key, '->', chatId)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 删除会话
|
|
121
|
+
deleteSession(key: string) {
|
|
122
|
+
const deleted = this.sessions.delete(key)
|
|
123
|
+
if (deleted) {
|
|
124
|
+
this.scheduleSave()
|
|
125
|
+
logInfo('删除会话:', key)
|
|
126
|
+
}
|
|
127
|
+
return deleted
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 清理过期会话(超过 24 小时未使用)
|
|
131
|
+
cleanExpiredSessions() {
|
|
132
|
+
const now = Date.now()
|
|
133
|
+
const expireTime = 24 * 60 * 60 * 1000 // 24 小时
|
|
134
|
+
let cleaned = 0
|
|
135
|
+
|
|
136
|
+
for (const [key, session] of this.sessions.entries()) {
|
|
137
|
+
if (now - session.lastUsed > expireTime) {
|
|
138
|
+
this.sessions.delete(key)
|
|
139
|
+
cleaned++
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (cleaned > 0) {
|
|
144
|
+
this.scheduleSave()
|
|
145
|
+
logInfo('清理了', cleaned, '个过期会话')
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// 清空所有会话
|
|
150
|
+
clearAll() {
|
|
151
|
+
this.sessions.clear()
|
|
152
|
+
this.scheduleSave()
|
|
153
|
+
logInfo('已清空所有会话')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 销毁时保存并清理
|
|
157
|
+
dispose() {
|
|
158
|
+
this.disposed = true
|
|
159
|
+
if (this.saveTimeout) {
|
|
160
|
+
clearTimeout(this.saveTimeout)
|
|
161
|
+
this.saveTimeout = null
|
|
162
|
+
}
|
|
163
|
+
// 立即保存
|
|
164
|
+
this.saveSessions()
|
|
165
|
+
}
|
|
166
|
+
}
|