koishi-plugin-chatluna-anuneko-api-adapter 1.1.0 → 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 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +223 -6
- package/lib/session-manager.d.ts +20 -0
- package/package.json +1 -1
- package/src/anuneko-requester.ts +56 -4
- package/src/index.ts +64 -0
- 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
|
@@ -9,6 +9,7 @@ export declare class AnunekoRequester extends ModelRequester {
|
|
|
9
9
|
constructor(ctx: Context, _configPool: ClientConfigPool<ClientConfig>, _pluginConfig: Config, _plugin: ChatLunaPlugin);
|
|
10
10
|
buildHeaders(): Record<string, string>;
|
|
11
11
|
private createNewSession;
|
|
12
|
+
private getOrCreateSession;
|
|
12
13
|
private sendChoice;
|
|
13
14
|
completionStreamInternal(params: ModelRequestParams): AsyncGenerator<ChatGenerationChunk>;
|
|
14
15
|
get logger(): import("reggol");
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
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;
|
|
6
|
+
export declare let sessionManager: SessionManager | null;
|
|
5
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;
|
|
@@ -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");
|
|
@@ -130,6 +131,39 @@ var AnunekoRequester = class extends import_api.ModelRequester {
|
|
|
130
131
|
}
|
|
131
132
|
return null;
|
|
132
133
|
}
|
|
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;
|
|
159
|
+
}
|
|
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;
|
|
166
|
+
}
|
|
133
167
|
// 自动选择分支
|
|
134
168
|
async sendChoice(msgId) {
|
|
135
169
|
const headers = this.buildHeaders();
|
|
@@ -164,9 +198,11 @@ var AnunekoRequester = class extends import_api.ModelRequester {
|
|
|
164
198
|
if (params.model.includes("exotic") || params.model.includes("shorthair")) {
|
|
165
199
|
modelName = "Exotic Shorthair";
|
|
166
200
|
}
|
|
167
|
-
const
|
|
201
|
+
const conversationId = params.id || "default";
|
|
202
|
+
logInfo("Conversation ID from chatluna:", conversationId);
|
|
203
|
+
const sessionId = await this.getOrCreateSession(modelName, conversationId);
|
|
168
204
|
if (!sessionId) {
|
|
169
|
-
const errorText = "
|
|
205
|
+
const errorText = "获取会话失败,请稍后再试。";
|
|
170
206
|
yield new import_outputs.ChatGenerationChunk({
|
|
171
207
|
text: errorText,
|
|
172
208
|
message: new import_messages.AIMessageChunk({ content: errorText })
|
|
@@ -348,9 +384,146 @@ var AnunekoClient = class extends import_client.PlatformModelAndEmbeddingsClient
|
|
|
348
384
|
}
|
|
349
385
|
};
|
|
350
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
|
+
|
|
351
522
|
// src/index.ts
|
|
523
|
+
var import_node_path2 = require("node:path");
|
|
352
524
|
var logger2;
|
|
353
525
|
var anunekoClient = null;
|
|
526
|
+
var sessionManager = null;
|
|
354
527
|
var reusable = false;
|
|
355
528
|
var usage = `
|
|
356
529
|
<p><strong>零成本、快速体验Chatluna</strong>。</p>
|
|
@@ -371,8 +544,18 @@ var usage = `
|
|
|
371
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
|
|
372
545
|
`;
|
|
373
546
|
function apply(ctx, config2) {
|
|
374
|
-
logger2 = (0,
|
|
547
|
+
logger2 = (0, import_logger4.createLogger)(ctx, "chatluna-anuneko-api-adapter");
|
|
375
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
|
+
});
|
|
376
559
|
ctx.command("anuneko <message:text>", "测试 anuneko API").action(async ({ session }, message) => {
|
|
377
560
|
if (!message) {
|
|
378
561
|
return "请输入消息内容,例如:/anuneko 你好";
|
|
@@ -487,6 +670,34 @@ function apply(ctx, config2) {
|
|
|
487
670
|
• chatluna room delete <房间名称> - 删除指定房间
|
|
488
671
|
• chatluna room list - 查看所有房间`;
|
|
489
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 "❌ 会话管理器未初始化";
|
|
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}`;
|
|
700
|
+
});
|
|
490
701
|
ctx.on("ready", async () => {
|
|
491
702
|
if (config2.platform == null || config2.platform.length < 1) {
|
|
492
703
|
throw new import_error2.ChatLunaError(
|
|
@@ -525,6 +736,11 @@ var Config2 = import_koishi.Schema.intersect([
|
|
|
525
736
|
platform: import_koishi.Schema.string().default("anuneko"),
|
|
526
737
|
xToken: import_koishi.Schema.string().required().role("textarea", { rows: [2, 4] }).description("anuneko API 的 x-token"),
|
|
527
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("会话管理模式"),
|
|
528
744
|
loggerinfo: import_koishi.Schema.boolean().default(false).description("日志调试模式").experimental(),
|
|
529
745
|
requestTimeout: import_koishi.Schema.number().default(120).description("请求 API 的超时时间,单位为秒。")
|
|
530
746
|
}).description("基础设置"),
|
|
@@ -544,5 +760,6 @@ var name = "chatluna-anuneko-api-adapter";
|
|
|
544
760
|
logger,
|
|
545
761
|
name,
|
|
546
762
|
reusable,
|
|
763
|
+
sessionManager,
|
|
547
764
|
usage
|
|
548
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'
|
|
@@ -82,6 +82,54 @@ export class AnunekoRequester extends ModelRequester {
|
|
|
82
82
|
return null
|
|
83
83
|
}
|
|
84
84
|
|
|
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
|
+
}
|
|
94
|
+
|
|
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
|
+
}
|
|
115
|
+
|
|
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)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return newSession
|
|
131
|
+
}
|
|
132
|
+
|
|
85
133
|
// 自动选择分支
|
|
86
134
|
private async sendChoice(msgId: string): Promise<void> {
|
|
87
135
|
const headers = this.buildHeaders()
|
|
@@ -127,10 +175,14 @@ export class AnunekoRequester extends ModelRequester {
|
|
|
127
175
|
modelName = 'Exotic Shorthair'
|
|
128
176
|
}
|
|
129
177
|
|
|
130
|
-
//
|
|
131
|
-
const
|
|
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)
|
|
132
184
|
if (!sessionId) {
|
|
133
|
-
const errorText = '
|
|
185
|
+
const errorText = '获取会话失败,请稍后再试。'
|
|
134
186
|
yield new ChatGenerationChunk({
|
|
135
187
|
text: errorText,
|
|
136
188
|
message: new AIMessageChunk({ content: errorText })
|
package/src/index.ts
CHANGED
|
@@ -7,9 +7,12 @@ 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
|
|
15
|
+
export let sessionManager: SessionManager | null = null
|
|
13
16
|
export const reusable = false
|
|
14
17
|
export const usage = `
|
|
15
18
|
<p><strong>零成本、快速体验Chatluna</strong>。</p>
|
|
@@ -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) => {
|
|
@@ -184,6 +203,43 @@ export function apply(ctx: Context, config: Config) {
|
|
|
184
203
|
• chatluna room list - 查看所有房间`
|
|
185
204
|
})
|
|
186
205
|
|
|
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
|
+
})
|
|
214
|
+
|
|
215
|
+
ctx.command('anuneko-session.clear', '清空所有 Anuneko 会话')
|
|
216
|
+
.action(async () => {
|
|
217
|
+
if (!sessionManager) {
|
|
218
|
+
return '❌ 会话管理器未初始化'
|
|
219
|
+
}
|
|
220
|
+
sessionManager.clearAll()
|
|
221
|
+
return '✅ 已清空所有 Anuneko 会话'
|
|
222
|
+
})
|
|
223
|
+
|
|
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': '每次对话都新建会话'
|
|
239
|
+
}
|
|
240
|
+
return `当前会话模式:${modeText[config.sessionMode] || config.sessionMode}`
|
|
241
|
+
})
|
|
242
|
+
|
|
187
243
|
ctx.on('ready', async () => {
|
|
188
244
|
if (config.platform == null || config.platform.length < 1) {
|
|
189
245
|
throw new ChatLunaError(
|
|
@@ -229,6 +285,7 @@ export interface Config extends ChatLunaPlugin.Config {
|
|
|
229
285
|
cookie?: string
|
|
230
286
|
loggerinfo: boolean
|
|
231
287
|
requestTimeout: number
|
|
288
|
+
sessionMode: 'shared' | 'per-user' | 'always-new'
|
|
232
289
|
}
|
|
233
290
|
|
|
234
291
|
export const Config: Schema<Config> = Schema.intersect([
|
|
@@ -241,6 +298,13 @@ export const Config: Schema<Config> = Schema.intersect([
|
|
|
241
298
|
cookie: Schema.string()
|
|
242
299
|
.role('textarea', { rows: [2, 4] })
|
|
243
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('会话管理模式'),
|
|
244
308
|
loggerinfo: Schema.boolean()
|
|
245
309
|
.default(false)
|
|
246
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
|
+
}
|