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.
@@ -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 switchModel;
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 const reusable = true;
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 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");
@@ -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(userId, modelName) {
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 switchModel(userId, chatId, modelName) {
156
- const headers = this.buildHeaders();
157
- const data = { chat_id: chatId, model: modelName };
158
- try {
159
- logInfo("Switching model to:", modelName);
160
- const signal = AbortSignal.timeout(this._pluginConfig.requestTimeout * 1e3);
161
- const response = await fetch("https://anuneko.com/api/v1/user/select_model", {
162
- method: "POST",
163
- headers,
164
- body: JSON.stringify(data),
165
- signal
166
- });
167
- if (response.ok) {
168
- this.modelMap.set(userId, modelName);
169
- logInfo("Model switched successfully");
170
- return true;
171
- }
172
- } catch (error) {
173
- this.logger.error("Failed to switch model:", error);
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
- return false;
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
- let sessionId = this.sessionMap.get(sessionKey);
214
- const currentModel = this.modelMap.get(sessionKey);
215
- if (!sessionId || currentModel !== modelName) {
216
- sessionId = await this.createNewSession(sessionKey, modelName);
217
- if (!sessionId) {
218
- const errorText = "创建会话失败,请稍后再试。";
219
- yield new import_outputs.ChatGenerationChunk({
220
- text: errorText,
221
- message: new import_messages.AIMessageChunk({ content: errorText })
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 reusable = true;
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, import_logger3.createLogger)(ctx, "chatluna-anuneko-api-adapter");
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", "清理当前频道的 anuneko 对话记录").action(async ({ session }) => {
535
- try {
536
- if (!anunekoClient) {
537
- return "❌ anuneko 客户端未初始化";
538
- }
539
- const sessionKey = session.channelId || session.userId;
540
- const requester = anunekoClient._requester;
541
- if (requester && typeof requester.clearSession === "function") {
542
- const cleared = requester.clearSession(sessionKey);
543
- if (cleared) {
544
- return "✅ 已清理当前频道的对话记录,下次对话将创建新会话";
545
- } else {
546
- return " 当前频道还没有对话记录";
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
@@ -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.0.4",
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'
@@ -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(userId: string, modelName: string): Promise<string | null> {
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 switchModel(userId: string, chatId: string, modelName: string): Promise<boolean> {
117
- const headers = this.buildHeaders()
118
- const data = { chat_id: chatId, model: modelName }
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
- try {
121
- logInfo('Switching model to:', modelName)
122
- const signal = AbortSignal.timeout(this._pluginConfig.requestTimeout * 1000)
123
- const response = await fetch('https://anuneko.com/api/v1/user/select_model', {
124
- method: 'POST',
125
- headers,
126
- body: JSON.stringify(data),
127
- signal
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
- if (response.ok) {
131
- this.modelMap.set(userId, modelName)
132
- logInfo('Model switched successfully')
133
- return true
134
- }
135
- } catch (error) {
136
- this.logger.error('Failed to switch model:', error)
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 false
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
- ) as KoishiHumanMessage[]
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
- let sessionId = this.sessionMap.get(sessionKey)
193
- const currentModel = this.modelMap.get(sessionKey)
194
-
195
- // 如果没有会话或模型不匹配,创建新会话
196
- if (!sessionId || currentModel !== modelName) {
197
- sessionId = await this.createNewSession(sessionKey, modelName)
198
- if (!sessionId) {
199
- const errorText = '创建会话失败,请稍后再试。'
200
- yield new ChatGenerationChunk({
201
- text: errorText,
202
- message: new AIMessageChunk({ content: errorText })
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 const reusable = true
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', '清理当前频道的 anuneko 对话记录')
198
+ ctx.command('anuneko-clean', '清理当前频道的对话记录')
180
199
  .action(async ({ session }) => {
181
- try {
182
- if (!anunekoClient) {
183
- return '❌ anuneko 客户端未初始化'
184
- }
200
+ return `对话历史由 ChatLuna 管理,请使用以下命令清理:
201
+ chatluna room clear - 清空当前房间的对话历史
202
+ chatluna room delete <房间名称> - 删除指定房间
203
+ • chatluna room list - 查看所有房间`
204
+ })
185
205
 
186
- // 使用 channelId 作为会话标识
187
- const sessionKey = session.channelId || session.userId
188
- const requester = anunekoClient._requester
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
- if (requester && typeof requester.clearSession === 'function') {
191
- const cleared = requester.clearSession(sessionKey)
192
- if (cleared) {
193
- return ' 已清理当前频道的对话记录,下次对话将创建新会话'
194
- } else {
195
- return '✅ 当前频道还没有对话记录'
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
- return ' 无法访问会话管理器'
200
- } catch (error) {
201
- logger.error('清理失败:', error)
202
- return `❌ 清理失败: ${error.message}`
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('日志调试模式')
@@ -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
+ }