mioku-plugin-chat 2.5.2 → 2.6.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.
@@ -0,0 +1,5 @@
1
+ import type { ChatGroupsFile } from "../types";
2
+
3
+ export const DEFAULT_GROUPS_CONFIG: ChatGroupsFile = {
4
+ groups: {},
5
+ };
package/context.ts CHANGED
@@ -2,7 +2,7 @@ import type { MiokiContext } from "mioki";
2
2
  import type { AIInstance, AIService } from "mioku";
3
3
  import type { ChatConfig } from "./types";
4
4
  import type { ChatDatabase } from "./db";
5
- import type { HumanizeEngine } from "./humanize";
5
+ import type { HumanizeEngine, ChatConfigProvider } from "./humanize";
6
6
  import type { SessionManager } from "./manage/session";
7
7
  import type { SkillSessionManager } from "./manage/skill-session";
8
8
  import type { RateLimiter } from "./manage/rate-limiter";
@@ -39,7 +39,9 @@ import type { ChatMessage } from "./types";
39
39
  export interface ChatPluginContext {
40
40
  // Core context
41
41
  ctx: MiokiContext;
42
- config: ChatConfig;
42
+ defaultConfig: ChatConfig;
43
+ configProvider: ChatConfigProvider;
44
+ getConfig: (groupId?: number) => Promise<ChatConfig>;
43
45
  db: ChatDatabase;
44
46
  aiInstance: AIInstance;
45
47
  workAIInstance: AIInstance;
@@ -98,7 +100,7 @@ export interface ChatRuntimeState {
98
100
  * Closure state shared with the extracted message/poke handlers.
99
101
  */
100
102
  export interface ChatHandlerState {
101
- getConfig: () => Promise<ChatConfig>;
103
+ getConfig: (groupId?: number) => Promise<ChatConfig>;
102
104
  matchMessageCommands?: (
103
105
  text: string,
104
106
  ) => Array<{ plugin: string; command: string }>;
package/core/chat-turn.ts CHANGED
@@ -190,11 +190,12 @@ export async function processChat(
190
190
  pluginCtx: ChatPluginContext,
191
191
  runtimeState: ChatRuntimeState,
192
192
  ): Promise<void> {
193
- const { ctx, config: cfg } = pluginCtx;
193
+ const { ctx } = pluginCtx;
194
194
  const isGroup = e.message_type === "group";
195
195
  const groupId: number | undefined = isGroup ? e.group_id : undefined;
196
196
  const userId: number = e.user_id || e.sender?.user_id;
197
197
  const selfId = e.self_id;
198
+ const cfg = await pluginCtx.getConfig(groupId);
198
199
 
199
200
  const personalSessionId = `personal:${userId}`;
200
201
  const groupSessionId = groupId ? `group:${groupId}` : personalSessionId;
package/core/tools/web.ts CHANGED
@@ -287,7 +287,7 @@ export function createRecallMemoryTool(toolCtx: ToolContext): AITool {
287
287
  messages: toolCtx.db.getMessagesByUser(userId, toolCtx.sessionId, userHistoryLimit),
288
288
  }));
289
289
 
290
- const retriever = new MemoryRetrieval(ai, toolCtx.config, toolCtx.db);
290
+ const retriever = new MemoryRetrieval(ai, () => toolCtx.config, toolCtx.db);
291
291
  const answer = await retriever.retrieveByQuestion({
292
292
  sessionId: toolCtx.sessionId,
293
293
  question,
@@ -33,13 +33,13 @@ export function createMessageHandler(
33
33
  const { getConfig, matchMessageCommands, runtimeState } = state;
34
34
 
35
35
  return async (e: any) => {
36
- const cfg = await getConfig();
36
+ const isGroup = e.message_type === "group";
37
+ const groupId: number | undefined = isGroup ? e.group_id : undefined;
38
+ const cfg = await getConfig(groupId);
37
39
  if (!cfg.apiKey) return;
38
40
  if (!e?.message || !Array.isArray(e.message)) return;
39
41
 
40
42
  const text = ctx.text(e) || "";
41
- const isGroup = e.message_type === "group";
42
- const groupId: number | undefined = isGroup ? e.group_id : undefined;
43
43
  const userId: number = e.user_id || e.sender?.user_id;
44
44
 
45
45
  if (userId === e.self_id) return;
package/handlers/poke.ts CHANGED
@@ -13,9 +13,9 @@ export function createPokeHandler(
13
13
 
14
14
  return async (e: any) => {
15
15
  if (e.target_id !== e.self_id) return;
16
- const cfg = await getConfig();
17
- if (!cfg.apiKey) return;
18
16
  const groupId = e.group_id;
17
+ const cfg = groupId ? await getConfig(groupId) : await getConfig();
18
+ if (!cfg.apiKey) return;
19
19
  if (!groupId || !isGroupAllowed(groupId, cfg)) return;
20
20
 
21
21
  const lastPoke = pokeCooldowns.get(groupId) ?? 0;
@@ -5,6 +5,8 @@ import { logger } from "mioki";
5
5
  import * as fs from "fs/promises";
6
6
  import { existsSync, readdirSync } from "fs";
7
7
  import * as path from "path";
8
+ import { extractGroupIdFromSession } from "../utils/group-config";
9
+ import type { ChatConfigProvider } from "./index";
8
10
 
9
11
  export interface EmojiPickResult {
10
12
  success: boolean;
@@ -33,13 +35,13 @@ const AVAILABLE_EMOTIONS = [
33
35
 
34
36
  export class EmojiAgent {
35
37
  private ai: AIInstance;
36
- private config: ChatConfig;
38
+ private getConfig: ChatConfigProvider;
37
39
  private db: ChatDatabase;
38
40
  private readonly memeBaseDir: string;
39
41
 
40
- constructor(ai: AIInstance, config: ChatConfig, db: ChatDatabase) {
42
+ constructor(ai: AIInstance, configProvider: ChatConfigProvider, db: ChatDatabase) {
41
43
  this.ai = ai;
42
- this.config = config;
44
+ this.getConfig = configProvider;
43
45
  this.db = db;
44
46
  this.memeBaseDir = path.join(process.cwd(), "data", "chat", "meme");
45
47
  }
@@ -77,6 +79,14 @@ export class EmojiAgent {
77
79
  aiResponseText: string,
78
80
  sessionId: string,
79
81
  ): Promise<EmojiPickResult> {
82
+ const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
83
+ if (!cfg.emoji?.enabled) {
84
+ return {
85
+ success: false,
86
+ error: "emoji disabled",
87
+ };
88
+ }
89
+
80
90
  const intents = this.parseAllMemeIntents(aiResponseText);
81
91
  if (intents.length === 0) {
82
92
  return {
@@ -88,7 +98,7 @@ export class EmojiAgent {
88
98
  const chatHistory = this.db.getMessages(sessionId, 20);
89
99
 
90
100
  // 获取配置中的角色列表
91
- const configChars = this.config.emoji?.characters || [];
101
+ const configChars = cfg.emoji?.characters || [];
92
102
 
93
103
  // 决定使用哪些角色
94
104
  let targetCharacters: string[];
@@ -112,6 +122,7 @@ export class EmojiAgent {
112
122
  targetCharacters,
113
123
  intent.emotion,
114
124
  chatHistory,
125
+ cfg,
115
126
  );
116
127
 
117
128
  if (!emojiResult.success || !emojiResult.emojiPath) {
@@ -135,6 +146,7 @@ export class EmojiAgent {
135
146
  characters: string[],
136
147
  emotion: string,
137
148
  chatHistory: ChatMessage[],
149
+ cfg: ChatConfig = this.getConfig(),
138
150
  ): Promise<{
139
151
  success: boolean;
140
152
  emojiPath?: string;
@@ -228,7 +240,7 @@ export class EmojiAgent {
228
240
  }
229
241
 
230
242
  // 检查是否使用 AI 选择
231
- const useAI = this.config.emoji?.useAISelection;
243
+ const useAI = cfg.emoji?.useAISelection;
232
244
  if (!useAI) {
233
245
  // 不使用 AI,直接随机选择
234
246
  logger.info(`[emoji-agent] useAISelection=false, random pick`);
@@ -236,7 +248,7 @@ export class EmojiAgent {
236
248
  }
237
249
 
238
250
  // 使用 AI 选择最合适的表情包
239
- return this.selectByAI(allEmojis, normalizedEmotion, chatHistory);
251
+ return this.selectByAI(allEmojis, normalizedEmotion, chatHistory, cfg);
240
252
  } catch (err) {
241
253
  logger.error(`[emoji-agent] Failed to pick emoji: ${err}`);
242
254
  return {
@@ -274,13 +286,14 @@ export class EmojiAgent {
274
286
  emojis: { path: string; character: string; file: string }[],
275
287
  emotion: string,
276
288
  chatHistory: ChatMessage[],
289
+ cfg: ChatConfig = this.getConfig(),
277
290
  ): Promise<{
278
291
  success: boolean;
279
292
  emojiPath?: string;
280
293
  description?: string;
281
294
  error?: string;
282
295
  }> {
283
- const model = this.config.workingModel || this.config.model;
296
+ const model = cfg.workingModel || cfg.model;
284
297
 
285
298
  const systemPrompt = `You are an emoji/sticker selection assistant. Your task is to select the most appropriate emoji/sticker from a given list based on the chat context.
286
299
 
@@ -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;
@@ -27,18 +29,18 @@ export class HumanizeEngine {
27
29
  constructor(
28
30
  mainAI: AIInstance,
29
31
  workAI: AIInstance,
30
- config: ChatConfig,
31
32
  db: ChatDatabase,
33
+ configProvider: ChatConfigProvider,
32
34
  ) {
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);
35
+ this.memoryRetrieval = new MemoryRetrieval(workAI, configProvider, db);
36
+ this.topicTracker = new TopicTracker(workAI, configProvider, db);
37
+ this.actionPlanner = new ActionPlanner(workAI, configProvider);
38
+ this.emotionAgent = new EmotionAgent(workAI, configProvider);
39
+ this.emojiAgent = new EmojiAgent(workAI, configProvider, db);
40
+ this.expressionLearner = new ExpressionLearner(workAI, configProvider, db);
39
41
  }
40
42
 
41
43
  async init(): Promise<void> {
42
44
  // emojiAgent 不需要初始化,它直接从文件系统读取
43
45
  }
44
- }
46
+ }
@@ -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) {