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.
@@ -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 import_logger3 = require("koishi-plugin-chatluna/utils/logger");
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 sessionId = await this.createNewSession(modelName);
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, import_logger3.createLogger)(ctx, "chatluna-anuneko-api-adapter");
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-chatluna-anuneko-api-adapter",
3
3
  "description": "anuneko API adapter for ChatLuna, using pearktrue API.",
4
- "version": "1.1.0",
4
+ "version": "1.2.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -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 sessionId = await this.createNewSession(modelName)
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('日志调试模式')
@@ -1,4 +1,6 @@
1
1
  platform: Platform name
2
2
  xToken: x-token for anuneko API
3
3
  cookie: Cookie for anuneko API (optional)
4
+ sessionMode: Session management mode
4
5
  loggerinfo: Logger debug mode
6
+ requestTimeout: Request timeout
@@ -1,4 +1,6 @@
1
1
  platform: 平台名称
2
2
  xToken: anuneko API 的 x-token
3
3
  cookie: anuneko API 的 Cookie(可选)
4
+ sessionMode: 会话管理模式
4
5
  loggerinfo: 日志调试模式
6
+ requestTimeout: 请求超时时间
@@ -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
+ }