mioku-plugin-chat 2.5.2 → 2.7.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.
@@ -2,6 +2,8 @@ import type { AIInstance } from "mioku";
2
2
  import { logger } from "mioki";
3
3
  import { extractJsonObject } from "../utils/json";
4
4
  import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
5
+ import { extractGroupIdFromSession } from "../utils/group-config";
6
+ import type { ChatConfigProvider } from "./index";
5
7
 
6
8
  export interface EmotionState {
7
9
  current: string;
@@ -18,41 +20,43 @@ export interface EmotionAnalysisInput {
18
20
 
19
21
  export class EmotionAgent {
20
22
  private readonly ai: AIInstance;
21
- private readonly config: ChatConfig;
23
+ private readonly getConfig: ChatConfigProvider;
22
24
  private readonly states = new Map<string, EmotionState>();
23
25
 
24
- constructor(ai: AIInstance, config: ChatConfig) {
26
+ constructor(ai: AIInstance, configProvider: ChatConfigProvider) {
25
27
  this.ai = ai;
26
- this.config = config;
28
+ this.getConfig = configProvider;
27
29
  }
28
30
 
29
31
  getCurrent(sessionId: string): EmotionState {
30
32
  const existing = this.states.get(sessionId);
31
33
  if (existing) return existing;
32
34
 
33
- const current = this.getDefaultEmotion();
35
+ const current = this.getDefaultEmotion(extractGroupIdFromSession(sessionId));
34
36
  const state = { current, updatedAt: 0 };
35
37
  this.states.set(sessionId, state);
36
38
  return state;
37
39
  }
38
40
 
39
- getAvailableEmotions(): string[] {
40
- const emotions = Object.keys(this.config.emotion?.emotions || {})
41
+ getAvailableEmotions(groupId?: number): string[] {
42
+ const cfg = this.getConfig(groupId);
43
+ const emotions = Object.keys(cfg.emotion?.emotions || {})
41
44
  .map((name) => name.trim().toLowerCase())
42
45
  .filter(Boolean);
43
46
  return Array.from(new Set(["default", ...emotions]));
44
47
  }
45
48
 
46
- getReferenceExamples(emotion: string): string[] {
49
+ getReferenceExamples(emotion: string, groupId?: number): string[] {
50
+ const cfg = this.getConfig(groupId);
47
51
  const normalized = this.normalizeEmotionName(emotion);
48
- const emotions = this.config.emotion?.emotions || {};
52
+ const emotions = cfg.emotion?.emotions || {};
49
53
  const examples = this.normalizeExamples(emotions[normalized]?.examples);
50
54
  if (examples.length > 0) return examples;
51
- return this.normalizeExamples(emotions[this.getDefaultEmotion()]?.examples);
55
+ return this.normalizeExamples(emotions[this.getDefaultEmotion(groupId)]?.examples);
52
56
  }
53
57
 
54
- setEmotion(sessionId: string, emotion: string): EmotionState {
55
- const current = this.resolveEmotion(emotion);
58
+ setEmotion(sessionId: string, emotion: string, groupId?: number): EmotionState {
59
+ const current = this.resolveEmotion(emotion, groupId);
56
60
  const state = { current, updatedAt: Date.now() };
57
61
  this.states.set(sessionId, state);
58
62
  return state;
@@ -69,28 +73,30 @@ export class EmotionAgent {
69
73
  }
70
74
 
71
75
  async refreshIfNeeded(input: EmotionAnalysisInput): Promise<EmotionState> {
76
+ const groupId = extractGroupIdFromSession(input.sessionId);
77
+ const cfg = this.getConfig(groupId);
72
78
  const current = this.getCurrent(input.sessionId);
73
- const intervalMs = Number(this.config.emotion?.updateIntervalMs ?? 60 * 60_000);
79
+ const intervalMs = Number(cfg.emotion?.updateIntervalMs ?? 60 * 60_000);
74
80
  const shouldRefresh =
75
81
  Boolean(input.force) || current.updatedAt <= 0 || Date.now() - current.updatedAt >= intervalMs;
76
82
 
77
83
  if (!shouldRefresh) return current;
78
84
 
79
85
  try {
80
- const nextEmotion = await this.analyzeEmotion(input);
81
- return this.setEmotion(input.sessionId, nextEmotion);
86
+ const nextEmotion = await this.analyzeEmotion(input, cfg);
87
+ return this.setEmotion(input.sessionId, nextEmotion, groupId);
82
88
  } catch (err) {
83
89
  logger.warn(`[emotion-agent] emotion analysis failed: ${err}`);
84
90
  if (current.updatedAt <= 0) {
85
- return this.setEmotion(input.sessionId, this.getDefaultEmotion());
91
+ return this.setEmotion(input.sessionId, this.getDefaultEmotion(groupId), groupId);
86
92
  }
87
93
  return current;
88
94
  }
89
95
  }
90
96
 
91
- private async analyzeEmotion(input: EmotionAnalysisInput): Promise<string> {
92
- const availableEmotions = this.getAvailableEmotions();
93
- const model = this.config.workingModel || this.config.model;
97
+ private async analyzeEmotion(input: EmotionAnalysisInput, cfg: ChatConfig = this.getConfig(extractGroupIdFromSession(input.sessionId))): Promise<string> {
98
+ const availableEmotions = this.getAvailableEmotions(extractGroupIdFromSession(input.sessionId));
99
+ const model = cfg.workingModel || cfg.model;
94
100
  const systemPrompt = `You are an emotion state selector for a chat bot.
95
101
 
96
102
  Task:
@@ -134,20 +140,21 @@ ${input.targetMessage.userName}: ${input.targetMessage.content}`;
134
140
 
135
141
  const content = response.content || "";
136
142
  const parsed = extractJsonObject(content);
137
- if (parsed === undefined) return this.getDefaultEmotion();
138
- return this.resolveEmotion(parsed?.emotion);
143
+ if (parsed === undefined) return this.getDefaultEmotion(extractGroupIdFromSession(input.sessionId));
144
+ return this.resolveEmotion(parsed?.emotion, extractGroupIdFromSession(input.sessionId));
139
145
  }
140
146
 
141
- private resolveEmotion(emotion: unknown): string {
147
+ private resolveEmotion(emotion: unknown, groupId?: number): string {
142
148
  const normalized = this.normalizeEmotionName(emotion);
143
- const available = this.getAvailableEmotions();
149
+ const available = this.getAvailableEmotions(groupId);
144
150
  if (available.includes(normalized)) return normalized;
145
- return this.getDefaultEmotion();
151
+ return this.getDefaultEmotion(groupId);
146
152
  }
147
153
 
148
- private getDefaultEmotion(): string {
149
- const configured = this.normalizeEmotionName(this.config.emotion?.defaultEmotion);
150
- const available = this.getAvailableEmotions();
154
+ private getDefaultEmotion(groupId?: number): string {
155
+ const cfg = this.getConfig(groupId);
156
+ const configured = this.normalizeEmotionName(cfg.emotion?.defaultEmotion);
157
+ const available = this.getAvailableEmotions(groupId);
151
158
  return available.includes(configured) ? configured : "default";
152
159
  }
153
160
 
@@ -2,23 +2,25 @@ import type { AIInstance } from "mioku";
2
2
  import { logger } from "mioki";
3
3
  import { extractJsonObject } from "../utils/json";
4
4
  import type { ChatDatabase } from "../db";
5
- import type { ChatConfig, ChatMessage } from "../types";
5
+ import type { ChatMessage } from "../types";
6
+ import type { ChatConfigProvider } from "./index";
6
7
 
7
8
  export class ExpressionLearner {
8
9
  private ai: AIInstance;
9
- private config: ChatConfig;
10
+ private getConfig: ChatConfigProvider;
10
11
  private db: ChatDatabase;
11
12
  private pendingMessagesByUser: Map<number, ChatMessage[]> = new Map();
12
13
  private learningUsers: Set<number> = new Set();
13
14
 
14
- constructor(ai: AIInstance, config: ChatConfig, db: ChatDatabase) {
15
+ constructor(ai: AIInstance, configProvider: ChatConfigProvider, db: ChatDatabase) {
15
16
  this.ai = ai;
16
- this.config = config;
17
+ this.getConfig = configProvider;
17
18
  this.db = db;
18
19
  }
19
20
 
20
21
  async onMessage(message: ChatMessage): Promise<void> {
21
- if (!this.config.expression?.enabled) return;
22
+ const cfg = this.getConfig(message.groupId);
23
+ if (!cfg.expression?.enabled) return;
22
24
  if (message.role !== "user") return;
23
25
  if (!message.content || message.content.length < 4) return;
24
26
  if (!message.userId) return;
@@ -27,13 +29,18 @@ export class ExpressionLearner {
27
29
  pending.push(message);
28
30
  this.pendingMessagesByUser.set(message.userId, pending);
29
31
 
30
- await this.tryLearn(message.userId);
32
+ await this.tryLearn(message.userId, cfg);
31
33
  }
32
34
 
33
- getExpressionContextForUser(userId: number, userName: string): string {
34
- if (!this.config.expression?.enabled) return "";
35
+ getExpressionContextForUser(
36
+ userId: number,
37
+ userName: string,
38
+ groupId?: number,
39
+ ): string {
40
+ const cfg = this.getConfig(groupId);
41
+ if (!cfg.expression?.enabled) return "";
35
42
 
36
- const sampleSize = this.config.expression?.sampleSize ?? 8;
43
+ const sampleSize = cfg.expression?.sampleSize ?? 8;
37
44
  const expressions = this.db.getExpressionsByUser(userId, sampleSize);
38
45
  if (expressions.length === 0) {
39
46
  logger.info(
@@ -55,12 +62,12 @@ export class ExpressionLearner {
55
62
  return context;
56
63
  }
57
64
 
58
- private async tryLearn(userId: number): Promise<void> {
65
+ private async tryLearn(userId: number, cfg = this.getConfig()): Promise<void> {
59
66
  if (this.learningUsers.has(userId)) return;
60
67
 
61
68
  const threshold = Math.max(
62
69
  1,
63
- this.config.expression?.learnAfterMessages ?? 100,
70
+ cfg.expression?.learnAfterMessages ?? 100,
64
71
  );
65
72
  const pending = this.pendingMessagesByUser.get(userId) ?? [];
66
73
  logger.debug(
@@ -76,7 +83,7 @@ export class ExpressionLearner {
76
83
 
77
84
  const batch = current.slice(0, threshold);
78
85
  this.pendingMessagesByUser.set(userId, current.slice(threshold));
79
- await this.learnForUser(userId, batch);
86
+ await this.learnForUser(userId, batch, cfg);
80
87
  }
81
88
  } catch (err) {
82
89
  logger.warn(
@@ -90,10 +97,11 @@ export class ExpressionLearner {
90
97
  private async learnForUser(
91
98
  userId: number,
92
99
  messages: ChatMessage[],
100
+ cfg = this.getConfig(),
93
101
  ): Promise<void> {
94
102
  if (messages.length === 0) return;
95
103
 
96
- const maxHabits = Math.max(1, this.config.expression?.sampleSize ?? 8);
104
+ const maxHabits = Math.max(1, cfg.expression?.sampleSize ?? 8);
97
105
  const userName = messages[messages.length - 1].userName || `User${userId}`;
98
106
  const msgTexts = messages.map((m) => m.content).join("\n");
99
107
 
@@ -128,7 +136,7 @@ Output strictly in JSON:
128
136
 
129
137
  If nothing reliable can be extracted, keep stable previous habits when possible. If still nothing, output {"expressions":[]}.`,
130
138
  messages: [],
131
- model: this.config.workingModel || this.config.model,
139
+ model: cfg.workingModel || cfg.model,
132
140
  temperature: 0.2,
133
141
  max_tokens: 600,
134
142
  });
@@ -154,7 +162,7 @@ If nothing reliable can be extracted, keep stable previous habits when possible.
154
162
 
155
163
  const keepCount = Math.max(
156
164
  1,
157
- this.config.retention?.expressionKeepPerUser ?? maxHabits,
165
+ cfg.retention?.expressionKeepPerUser ?? maxHabits,
158
166
  );
159
167
  const pruned = this.db.pruneExpressionsBySession(
160
168
  `user:${userId}`,
@@ -170,4 +178,4 @@ If nothing reliable can be extracted, keep stable previous habits when possible.
170
178
  `[ExpressionLearner] Updated ${normalized.length} habits for ${userName} (${userId}): ${JSON.stringify(normalized)}`,
171
179
  );
172
180
  }
173
- }
181
+ }
package/humanize/index.ts CHANGED
@@ -16,6 +16,8 @@ import { TopicTracker } from "./topic";
16
16
  import { ActionPlanner } from "./planner";
17
17
  import { ExpressionLearner } from "./expression";
18
18
 
19
+ export type ChatConfigProvider = (groupId?: number) => ChatConfig;
20
+
19
21
  export class HumanizeEngine {
20
22
  readonly memoryRetrieval: MemoryRetrieval;
21
23
  readonly topicTracker: TopicTracker;
@@ -25,17 +27,16 @@ export class HumanizeEngine {
25
27
  readonly expressionLearner: ExpressionLearner;
26
28
 
27
29
  constructor(
28
- mainAI: AIInstance,
29
30
  workAI: AIInstance,
30
- config: ChatConfig,
31
31
  db: ChatDatabase,
32
+ configProvider: ChatConfigProvider,
32
33
  ) {
33
- this.memoryRetrieval = new MemoryRetrieval(workAI, config, db);
34
- this.topicTracker = new TopicTracker(workAI, config, db);
35
- this.actionPlanner = new ActionPlanner(workAI, config);
36
- this.emotionAgent = new EmotionAgent(workAI, config);
37
- this.emojiAgent = new EmojiAgent(workAI, config, db);
38
- this.expressionLearner = new ExpressionLearner(workAI, config, db);
34
+ this.memoryRetrieval = new MemoryRetrieval(workAI, configProvider, db);
35
+ this.topicTracker = new TopicTracker(workAI, configProvider, db);
36
+ this.actionPlanner = new ActionPlanner(workAI, configProvider);
37
+ this.emotionAgent = new EmotionAgent(workAI, configProvider);
38
+ this.emojiAgent = new EmojiAgent(workAI, configProvider);
39
+ this.expressionLearner = new ExpressionLearner(workAI, configProvider, db);
39
40
  }
40
41
 
41
42
  async init(): Promise<void> {
@@ -1,7 +1,9 @@
1
1
  import type { AIInstance } from "mioku";
2
2
  import { logger } from "mioki";
3
3
  import type { ChatDatabase } from "../db";
4
- import type { ChatConfig, ChatMessage } from "../types";
4
+ import type { ChatMessage } from "../types";
5
+ import { extractGroupIdFromSession } from "../utils/group-config";
6
+ import type { ChatConfigProvider } from "./index";
5
7
 
6
8
  export interface MemoryUserHistoryChunk {
7
9
  userId: number;
@@ -18,17 +20,18 @@ export interface MemoryRecallInput {
18
20
 
19
21
  export class MemoryRetrieval {
20
22
  private ai: AIInstance;
21
- private config: ChatConfig;
23
+ private getConfig: ChatConfigProvider;
22
24
  private db: ChatDatabase;
23
25
 
24
- constructor(ai: AIInstance, config: ChatConfig, db: ChatDatabase) {
26
+ constructor(ai: AIInstance, configProvider: ChatConfigProvider, db: ChatDatabase) {
25
27
  this.ai = ai;
26
- this.config = config;
28
+ this.getConfig = configProvider;
27
29
  this.db = db;
28
30
  }
29
31
 
30
32
  async retrieveByQuestion(input: MemoryRecallInput): Promise<string | null> {
31
- if (!this.config.memory?.enabled) return null;
33
+ const cfg = this.getConfig(extractGroupIdFromSession(input.sessionId));
34
+ if (!cfg.memory?.enabled) return null;
32
35
 
33
36
  const question = String(input.question || "").trim();
34
37
  if (!question) return null;
@@ -68,7 +71,7 @@ Output requirements:
68
71
  - If useful evidence exists, summarize key facts with timestamps.
69
72
  - If no useful memory exists, output exactly: NO_USEFUL_MEMORY_FOUND`,
70
73
  messages: [],
71
- model: this.config.workingModel || this.config.model,
74
+ model: cfg.workingModel || cfg.model,
72
75
  temperature: 0.2,
73
76
  max_tokens: 700,
74
77
  });
@@ -128,4 +131,4 @@ Output requirements:
128
131
  private formatTimestamp(timestamp: number): string {
129
132
  return new Date(timestamp).toLocaleString("zh-CN", { hour12: false });
130
133
  }
131
- }
134
+ }
@@ -1,24 +1,25 @@
1
1
  import type { AIInstance } from "mioku";
2
2
  import { logger } from "mioki";
3
3
  import type {
4
- ChatConfig,
5
4
  ChatMessage,
6
5
  PlannerAction,
7
6
  PlannerResult,
8
7
  } from "../types";
8
+ import { extractGroupIdFromSession } from "../utils/group-config";
9
+ import type { ChatConfigProvider } from "./index";
9
10
 
10
11
  export class ActionPlanner {
11
12
  private ai: AIInstance;
12
- private config: ChatConfig;
13
+ private getConfig: ChatConfigProvider;
13
14
  private actionHistory: Map<
14
15
  string,
15
16
  { action: PlannerAction; time: number }[]
16
17
  > = new Map();
17
18
  private pendingPlans: Map<string, Promise<PlannerResult>> = new Map();
18
19
 
19
- constructor(ai: AIInstance, config: ChatConfig) {
20
+ constructor(ai: AIInstance, configProvider: ChatConfigProvider) {
20
21
  this.ai = ai;
21
- this.config = config;
22
+ this.getConfig = configProvider;
22
23
  }
23
24
 
24
25
  async plan(
@@ -28,18 +29,18 @@ export class ActionPlanner {
28
29
  lastTriggerMessage: string,
29
30
  isIdleCheck: boolean = false,
30
31
  ): Promise<PlannerResult> {
31
- if (!this.config.planner?.enabled) {
32
+ const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
33
+ if (!cfg.planner?.enabled) {
32
34
  return { action: "reply", reason: "planner disabled" };
33
35
  }
34
36
 
35
- // Guard: if a plan call is already in-flight for this session, wait for it
36
37
  const existing = this.pendingPlans.get(sessionId);
37
38
  if (existing) {
38
39
  logger.info(`[ActionPlanner] Session ${sessionId} has a plan already in-flight, waiting...`);
39
40
  return existing;
40
41
  }
41
42
 
42
- const planPromise = this.doPlan(sessionId, botName, recentHistory, lastTriggerMessage, isIdleCheck);
43
+ const planPromise = this.doPlan(sessionId, botName, recentHistory, lastTriggerMessage, isIdleCheck, cfg);
43
44
  this.pendingPlans.set(sessionId, planPromise);
44
45
 
45
46
  try {
@@ -55,8 +56,9 @@ export class ActionPlanner {
55
56
  recentHistory: ChatMessage[],
56
57
  lastTriggerMessage: string,
57
58
  isIdleCheck: boolean,
59
+ cfg = this.getConfig(extractGroupIdFromSession(sessionId)),
58
60
  ): Promise<PlannerResult> {
59
- if (!this.config.planner?.enabled) {
61
+ if (!cfg.planner?.enabled) {
60
62
  return { action: "reply", reason: "planner disabled" };
61
63
  }
62
64
 
@@ -81,7 +83,6 @@ export class ActionPlanner {
81
83
  let prompt: string;
82
84
 
83
85
  if (isIdleCheck) {
84
- // 空闲检测模式:没有明确触发消息,观察群友聊天并决定是否要融入对话
85
86
  prompt = `It is ${timeStr}. Your name is ${botName}.
86
87
 
87
88
  Here is the recent chat content (from oldest to most recent):
@@ -120,7 +121,6 @@ OR for complete:
120
121
 
121
122
  DO NOT include any explanation, markdown formatting, or additional text. Only output the JSON.`;
122
123
  } else {
123
- // 正常模式:由触发消息驱动的 planner
124
124
  prompt = `It is ${timeStr}. Your name is ${botName}.
125
125
 
126
126
  Here is the chat content:
@@ -169,12 +169,11 @@ DO NOT include any explanation, markdown formatting, or additional text. Only ou
169
169
  const content = await this.ai.generateText({
170
170
  prompt,
171
171
  messages: [],
172
- model: this.config.workingModel || this.config.model,
172
+ model: cfg.workingModel || cfg.model,
173
173
  temperature: isIdleCheck ? 0.3 : 0.2,
174
174
  max_tokens: isIdleCheck ? 300 : 500,
175
175
  });
176
176
 
177
- // 如果返回内容为空,使用默认值
178
177
  if (!content || !content.trim()) {
179
178
  logger.warn(
180
179
  `[ActionPlanner] Empty response from AI, using default reply`,
@@ -182,7 +181,6 @@ DO NOT include any explanation, markdown formatting, or additional text. Only ou
182
181
  return { action: "reply", reason: "empty response" };
183
182
  }
184
183
 
185
- // 尝试提取 JSON 块
186
184
  let jsonStr = "";
187
185
  const jsonMatch = content.match(/\{[\s\S]*\}/);
188
186
  if (jsonMatch) {
@@ -196,14 +194,11 @@ DO NOT include any explanation, markdown formatting, or additional text. Only ou
196
194
  return { action: "reply", reason: "parse failed" };
197
195
  }
198
196
 
199
- // 尝试解析 JSON
200
197
  let parsed: any;
201
198
  try {
202
199
  parsed = JSON.parse(jsonStr);
203
200
  } catch {
204
- // 尝试修复常见的 JSON 错误
205
201
  try {
206
- // 移除可能存在的尾随逗号
207
202
  jsonStr = jsonStr.replace(/,\s*}/g, "}").replace(/,\s*]/g, "]");
208
203
  parsed = JSON.parse(jsonStr);
209
204
  } catch (e) {
package/humanize/topic.ts CHANGED
@@ -2,25 +2,28 @@ import type { AIInstance } from "mioku";
2
2
  import { logger } from "mioki";
3
3
  import { extractJsonObject } from "../utils/json";
4
4
  import type { ChatDatabase } from "../db";
5
- import type { ChatConfig } from "../types";
5
+ import type { ChatMessage } from "../types";
6
+ import { extractGroupIdFromSession } from "../utils/group-config";
7
+ import type { ChatConfigProvider } from "./index";
6
8
 
7
9
  export class TopicTracker {
8
10
  private ai: AIInstance;
9
- private config: ChatConfig;
11
+ private getConfig: ChatConfigProvider;
10
12
  private db: ChatDatabase;
11
13
  private lastAnalyzedWindowEnd: Map<string, number> = new Map();
12
14
 
13
- constructor(ai: AIInstance, config: ChatConfig, db: ChatDatabase) {
15
+ constructor(ai: AIInstance, configProvider: ChatConfigProvider, db: ChatDatabase) {
14
16
  this.ai = ai;
15
- this.config = config;
17
+ this.getConfig = configProvider;
16
18
  this.db = db;
17
19
  }
18
20
 
19
21
  async onMessage(sessionId: string): Promise<void> {
20
- if (!this.config.topic?.enabled) return;
22
+ const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
23
+ if (!cfg.topic?.enabled) return;
21
24
  if (!sessionId.startsWith("group:")) return;
22
25
 
23
- const windowMs = this.getWindowMs();
26
+ const windowMs = this.getWindowMs(cfg);
24
27
  const completedWindowEnd = Math.floor(Date.now() / windowMs) * windowMs;
25
28
  if (completedWindowEnd <= 0) return;
26
29
 
@@ -32,18 +35,19 @@ export class TopicTracker {
32
35
  this.lastAnalyzedWindowEnd.set(sessionId, completedWindowEnd);
33
36
  const windowStartAt = completedWindowEnd - windowMs;
34
37
 
35
- this.analyzeWindow(sessionId, windowStartAt, completedWindowEnd).catch(
38
+ this.analyzeWindow(sessionId, windowStartAt, completedWindowEnd, cfg).catch(
36
39
  (err) => logger.warn(`[TopicTracker] Analysis failed: ${err}`),
37
40
  );
38
41
  }
39
42
 
40
43
  getTopicContext(sessionId: string, historyStartAt?: number): string {
41
- if (!this.config.topic?.enabled) return "";
44
+ const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
45
+ if (!cfg.topic?.enabled) return "";
42
46
  if (!sessionId.startsWith("group:")) return "";
43
47
  if (!historyStartAt) return "";
44
48
 
45
- const windowMs = this.getWindowMs();
46
- const windowCount = this.getHistoryWindowCount();
49
+ const windowMs = this.getWindowMs(cfg);
50
+ const windowCount = this.getHistoryWindowCount(cfg);
47
51
  const firstWindowEnd = Math.floor(historyStartAt / windowMs) * windowMs;
48
52
  if (firstWindowEnd <= 0) return "";
49
53
 
@@ -79,6 +83,7 @@ export class TopicTracker {
79
83
  sessionId: string,
80
84
  windowStartAt: number,
81
85
  windowEndAt: number,
86
+ cfg = this.getConfig(extractGroupIdFromSession(sessionId)),
82
87
  ): Promise<void> {
83
88
  if (windowEndAt <= windowStartAt) return;
84
89
 
@@ -119,7 +124,7 @@ Output strictly in JSON format:
119
124
  "summary": "one concise abstract summary sentence"
120
125
  }`,
121
126
  messages: [],
122
- model: this.config.workingModel || this.config.model,
127
+ model: cfg.workingModel || cfg.model,
123
128
  temperature: 0.2,
124
129
  max_tokens: 300,
125
130
  });
@@ -163,14 +168,14 @@ Output strictly in JSON format:
163
168
  }
164
169
  }
165
170
 
166
- private getWindowMs(): number {
167
- const hours = Number(this.config.topic?.windowHours);
171
+ private getWindowMs(cfg = this.getConfig()): number {
172
+ const hours = Number(cfg.topic?.windowHours);
168
173
  const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 5;
169
174
  return Math.max(1, Math.floor(safeHours)) * 3600_000;
170
175
  }
171
176
 
172
- private getHistoryWindowCount(): number {
173
- const count = Number(this.config.topic?.historyWindowCount);
177
+ private getHistoryWindowCount(cfg = this.getConfig()): number {
178
+ const count = Number(cfg.topic?.historyWindowCount);
174
179
  if (!Number.isFinite(count) || count <= 0) return 3;
175
180
  return Math.max(1, Math.floor(count));
176
181
  }
@@ -202,3 +207,5 @@ Output strictly in JSON format:
202
207
  return `${year}-${month}-${day} ${hour}:${minute}`;
203
208
  }
204
209
  }
210
+
211
+ export type { ChatMessage };