mioku-plugin-chat 2.6.0 → 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.
package/db.ts CHANGED
@@ -381,6 +381,11 @@ export async function initDatabase(): Promise<ChatDatabase> {
381
381
  VALUES ($hash, $url, $type, $description, $emotion, $character, $filePath, $createdAt)
382
382
  ON CONFLICT(hash) DO UPDATE SET
383
383
  url = excluded.url,
384
+ type = excluded.type,
385
+ description = excluded.description,
386
+ emotion = excluded.emotion,
387
+ character = excluded.character,
388
+ file_path = excluded.file_path,
384
389
  created_at = excluded.created_at
385
390
  `),
386
391
  getImageByHash: db.prepare(`SELECT * FROM images WHERE hash = $hash`),
@@ -710,9 +715,9 @@ export async function initDatabase(): Promise<ChatDatabase> {
710
715
  $url: image.url,
711
716
  $type: image.type,
712
717
  $description: image.description,
713
- $emotion: image.emotion ?? null,
714
- $character: image.character ?? null,
715
- $filePath: image.filePath ?? null,
718
+ $emotion: null,
719
+ $character: null,
720
+ $filePath: null,
716
721
  $createdAt: image.createdAt,
717
722
  });
718
723
  },
@@ -726,9 +731,6 @@ export async function initDatabase(): Promise<ChatDatabase> {
726
731
  url: row.url,
727
732
  type: row.type,
728
733
  description: row.description,
729
- emotion: row.emotion,
730
- character: row.character,
731
- filePath: row.file_path,
732
734
  createdAt: row.created_at,
733
735
  };
734
736
  },
@@ -742,9 +744,6 @@ export async function initDatabase(): Promise<ChatDatabase> {
742
744
  url: row.url,
743
745
  type: row.type,
744
746
  description: row.description,
745
- emotion: row.emotion,
746
- character: row.character,
747
- filePath: row.file_path,
748
747
  createdAt: row.created_at,
749
748
  };
750
749
  },
@@ -757,9 +756,6 @@ export async function initDatabase(): Promise<ChatDatabase> {
757
756
  url: row.url,
758
757
  type: row.type,
759
758
  description: row.description,
760
- emotion: row.emotion,
761
- character: row.character,
762
- filePath: row.file_path,
763
759
  createdAt: row.created_at,
764
760
  }));
765
761
  },
@@ -2,8 +2,8 @@ import type { MiokiContext } from "mioki";
2
2
  import type { AIService } from "mioku";
3
3
  import type { ChatPluginContext } from "../context";
4
4
  import type { ChatConfig, TargetMessage } from "../types";
5
+ import { finalizeChatTurn } from "../core/chat-turn";
5
6
  import { getBotRole } from "../utils";
6
- import { buildStructuredUserInputFromTarget } from "../manage/group-structured-history";
7
7
 
8
8
  export async function handleIdleCheckDebug(
9
9
  pluginCtx: ChatPluginContext,
@@ -16,7 +16,8 @@ export async function handleIdleCheckDebug(
16
16
  await e.reply("只有主人才能使用这个指令~");
17
17
  return;
18
18
  }
19
- const groupIdStr = e.message[0]?.text?.replace("/空闲检查", "")?.trim() || "";
19
+ const groupIdStr =
20
+ e.message[0]?.text?.replace("/空闲检查", "")?.trim() || "";
20
21
  const targetGroupId = parseInt(groupIdStr, 10);
21
22
  if (!targetGroupId) {
22
23
  await e.reply("请指定群号,如:/空闲检查 123456");
@@ -25,108 +26,106 @@ export async function handleIdleCheckDebug(
25
26
 
26
27
  const groupSessionId = `group:${targetGroupId}`;
27
28
  try {
28
- const now = Date.now();
29
- const botNickname =
30
- cfg.nicknames[0] || ctx.pickBot(e.self_id).nickname || "Bot";
31
- ctx.logger.info(`[Debug] Manual idle check: group ${targetGroupId}`);
32
-
33
- const { history } = await pluginCtx.getGroupHistoryMessages(
34
- targetGroupId,
35
- groupSessionId,
36
- ctx,
37
- cfg.historyCount,
38
- pluginCtx.db,
39
- e.self_id,
40
- pluginCtx.buildHistoryMediaOptions(pluginCtx.aiInstance, cfg),
41
- );
42
- const planResult = await pluginCtx.humanize.actionPlanner.plan(
29
+ await pluginCtx.sessionTurnScheduler.run(
43
30
  groupSessionId,
44
- botNickname,
45
- history,
46
- "[Check if you want to answer the call]",
47
- true,
48
- );
31
+ "idle-debug",
32
+ async () => {
33
+ const now = Date.now();
34
+ const botNickname =
35
+ cfg.nicknames[0] || ctx.pickBot(e.self_id).nickname || "Bot";
36
+ ctx.logger.info(`[Debug] Manual idle check: group ${targetGroupId}`);
49
37
 
50
- if (planResult.action !== "reply") {
51
- await e.reply(
52
- `[空闲检测] 群 ${targetGroupId}\n决定: ${planResult.action}\n原因: ${planResult.reason}`,
53
- );
54
- return;
55
- }
38
+ const { history } = await pluginCtx.getGroupHistoryMessages(
39
+ targetGroupId,
40
+ groupSessionId,
41
+ ctx,
42
+ cfg.historyCount,
43
+ pluginCtx.db,
44
+ e.self_id,
45
+ pluginCtx.buildHistoryMediaOptions(pluginCtx.aiInstance, cfg),
46
+ );
47
+ const planResult = await pluginCtx.humanize.actionPlanner.plan(
48
+ groupSessionId,
49
+ botNickname,
50
+ history,
51
+ "[Check if you want to answer the call]",
52
+ true,
53
+ );
56
54
 
57
- const targetMessage: TargetMessage = {
58
- userName: "系统",
59
- userId: 0,
60
- userRole: "member",
61
- content: "[No one in the group is talking? I'll answer!]",
62
- messageId: 0,
63
- timestamp: now,
64
- };
65
- const botRole = await getBotRole(targetGroupId, ctx, e.self_id);
66
- const toolCtx = pluginCtx.buildToolContext({
67
- ctx,
68
- event: null,
69
- groupSessionId,
70
- groupId: targetGroupId,
71
- userId: 0,
72
- config: cfg,
73
- aiService: ctx.services!.ai as AIService,
74
- db: pluginCtx.db,
75
- botRole,
76
- humanize: pluginCtx.humanize,
77
- targetMessage,
78
- selfId: e.self_id,
79
- });
55
+ if (planResult.action !== "reply") {
56
+ await e.reply(
57
+ `[空闲检测] 群 ${targetGroupId}\n决定: ${planResult.action}\n原因: ${planResult.reason}`,
58
+ );
59
+ return;
60
+ }
80
61
 
81
- const result = await pluginCtx.runWithRateLimitGuard(
82
- () =>
83
- pluginCtx.runChat(
84
- pluginCtx.aiInstance,
85
- toolCtx,
86
- history,
62
+ const targetMessage: TargetMessage = {
63
+ userName: "系统",
64
+ userId: 0,
65
+ userRole: "member",
66
+ content: "[No one in the group is talking? I'll answer!]",
67
+ messageId: 0,
68
+ timestamp: now,
69
+ };
70
+ const botRole = await getBotRole(targetGroupId, ctx, e.self_id);
71
+ const toolCtx = pluginCtx.buildToolContext({
72
+ ctx,
73
+ event: null,
74
+ groupSessionId,
75
+ groupId: targetGroupId,
76
+ userId: 0,
77
+ config: cfg,
78
+ aiService: ctx.services!.ai as AIService,
79
+ db: pluginCtx.db,
80
+ botRole,
81
+ humanize: pluginCtx.humanize,
87
82
  targetMessage,
88
- {
89
- config: cfg,
90
- botNickname,
91
- botRole: toolCtx.botRole,
92
- aiService: ctx.services!.ai as AIService,
93
- isGroup: true,
94
- plannerThoughts: `You stumbled upon some message in this group and decided to reply.\nQuote messages from group friends appropriately (using [reply:message ID] format).\nDon't mention your intentions like "I'm here to answer".`,
95
- replyContext: { type: "idle" },
96
- },
97
- pluginCtx.humanize,
98
- pluginCtx.skillManager,
99
- ),
100
- { groupId: targetGroupId, label: "idle-check" },
101
- );
102
- if (!result) {
103
- await e.reply(`[空闲检测] 群 ${targetGroupId} 因限流被跳过`);
104
- return;
105
- }
83
+ selfId: e.self_id,
84
+ });
106
85
 
107
- await pluginCtx.sendAIResponse(
108
- {
109
- ctx,
110
- groupId: targetGroupId,
111
- messages: result.messages,
112
- config: cfg,
113
- sentIndices: toolCtx.sentMessageIndices,
86
+ const result = await pluginCtx.runWithRateLimitGuard(
87
+ () =>
88
+ pluginCtx.runChat(
89
+ pluginCtx.aiInstance,
90
+ toolCtx,
91
+ history,
92
+ targetMessage,
93
+ {
94
+ config: cfg,
95
+ botNickname,
96
+ botRole: toolCtx.botRole,
97
+ aiService: ctx.services!.ai as AIService,
98
+ isGroup: true,
99
+ plannerThoughts:
100
+ 'You stumbled upon some message in this group and decided to reply.\nQuote messages from group friends appropriately (using [reply:message ID] format).\nDon\'t mention your intentions like "I\'m here to answer".',
101
+ replyContext: { type: "idle" },
102
+ },
103
+ pluginCtx.humanize,
104
+ pluginCtx.skillManager,
105
+ ),
106
+ { groupId: targetGroupId, label: "idle-check" },
107
+ );
108
+ if (!result) {
109
+ await e.reply(`[空闲检测] 群 ${targetGroupId} 因限流被跳过`);
110
+ return;
111
+ }
112
+
113
+ await finalizeChatTurn(pluginCtx, {
114
+ event: null,
115
+ cfg,
116
+ result,
117
+ groupId: targetGroupId,
118
+ groupSessionId,
119
+ userId: 0,
120
+ selfId: e.self_id,
121
+ toolCtx,
122
+ send: true,
123
+ isLive: true,
124
+ });
125
+ await e.reply(
126
+ `[空闲检测] 群 ${targetGroupId} 已发送回复: ${planResult.reason}`,
127
+ );
114
128
  },
115
- e.self_id,
116
- );
117
- const now2 = Date.now();
118
- pluginCtx.saveBotMessages(
119
- targetGroupId,
120
- groupSessionId,
121
- result.messages,
122
- now2,
123
- cfg,
124
- pluginCtx.db,
125
- ctx,
126
- e.self_id,
127
- );
128
- await e.reply(
129
- `[空闲检测] 群 ${targetGroupId} 已发送回复: ${planResult.reason}`,
130
129
  );
131
130
  } catch (err) {
132
131
  ctx.logger.error(`[Debug] Idle check failed: ${err}`);
@@ -104,10 +104,6 @@ export function createMessageHandler(
104
104
  groupId,
105
105
  label: "image-analysis",
106
106
  }),
107
- allowedCharacters:
108
- cfg.emoji?.characters && cfg.emoji.characters.length > 0
109
- ? cfg.emoji.characters
110
- : undefined,
111
107
  }).catch((err) =>
112
108
  ctx.logger.error(`[image-analyzer] Failed: ${err}`),
113
109
  );
@@ -232,24 +228,7 @@ export function createMessageHandler(
232
228
  }
233
229
  }
234
230
 
235
- // 检查是否在处理中
236
- const processingSet = runtimeState.processingSet;
237
- if (isGroup && groupId && groupSessionId) {
238
- if (processingSet.has(groupSessionId)) {
239
- if ((atBot || mentionedNickname) && !runtimeState.isRateLimitBlocked()) {
240
- pluginCtx.queueManager.enqueue(groupSessionId, e, cfg);
241
- pluginCtx.rateLimiter.recordInteraction(groupId, userId);
242
- }
243
- return;
244
- }
245
- processingSet.add(groupSessionId);
246
- } else {
247
- const triggerKey = `personal:${userId}`;
248
- if (processingSet.has(triggerKey)) return;
249
- processingSet.add(triggerKey);
250
- }
251
-
252
- try {
231
+ const runTriggeredMessage = async (): Promise<void> => {
253
232
  if (atBot) {
254
233
  if (!pluginCtx.rateLimiter.canProcess(userId, groupId, text)) return;
255
234
 
@@ -279,9 +258,10 @@ export function createMessageHandler(
279
258
  }
280
259
 
281
260
  if (quotedBot || mentionedNickname) {
261
+ if (!groupId || !groupSessionId) return;
282
262
  const { history } = await pluginCtx.getGroupHistoryMessages(
283
- groupId!,
284
- groupSessionId!,
263
+ groupId,
264
+ groupSessionId,
285
265
  ctx,
286
266
  cfg.historyCount,
287
267
  pluginCtx.db,
@@ -291,7 +271,7 @@ export function createMessageHandler(
291
271
  const botNickname =
292
272
  cfg.nicknames[0] || ctx.pickBot(e.self_id).nickname || "Bot";
293
273
  const planResult = await pluginCtx.humanize.actionPlanner.plan(
294
- groupSessionId!,
274
+ groupSessionId,
295
275
  botNickname,
296
276
  history,
297
277
  text,
@@ -300,18 +280,39 @@ export function createMessageHandler(
300
280
  if (!pluginCtx.rateLimiter.canProcess(userId, groupId, text)) return;
301
281
  pluginCtx.rateLimiter.record(userId, groupId, text);
302
282
  await processChat(e, pluginCtx, runtimeState);
283
+ }
284
+ };
285
+
286
+ if (isGroup && groupId && groupSessionId) {
287
+ if (!atBot && !quotedBot && !mentionedNickname) return;
288
+ if (pluginCtx.sessionTurnScheduler.isBusy(groupSessionId)) {
289
+ if (!runtimeState.isRateLimitBlocked()) {
290
+ pluginCtx.queueManager.enqueue(groupSessionId, e, cfg);
291
+ pluginCtx.rateLimiter.recordInteraction(groupId, userId);
292
+ pluginCtx.queueProcessor.scheduleQueuedMessages(
293
+ groupSessionId,
294
+ e.self_id,
295
+ );
296
+ }
303
297
  return;
304
298
  }
299
+
300
+ await pluginCtx.sessionTurnScheduler.run(
301
+ groupSessionId,
302
+ "message",
303
+ runTriggeredMessage,
304
+ );
305
+ return;
306
+ }
307
+
308
+ const triggerKey = `personal:${userId}`;
309
+ const processingSet = runtimeState.processingSet;
310
+ if (processingSet.has(triggerKey)) return;
311
+ processingSet.add(triggerKey);
312
+ try {
313
+ await runTriggeredMessage();
305
314
  } finally {
306
- if (isGroup && groupId && groupSessionId) {
307
- processingSet.delete(groupSessionId);
308
- await pluginCtx.queueProcessor.processQueuedMessages(
309
- groupSessionId,
310
- e.self_id,
311
- );
312
- } else {
313
- processingSet.delete(`personal:${userId}`);
314
- }
315
+ processingSet.delete(triggerKey);
315
316
  }
316
317
  };
317
318
  }
package/handlers/poke.ts CHANGED
@@ -2,6 +2,7 @@ import type { ChatPluginContext, ChatHandlerState } from "../context";
2
2
  import type { TargetMessage } from "../types";
3
3
  import { getBotRole, isGroupAllowed } from "../utils";
4
4
  import { buildStructuredUserInputFromTarget } from "../manage/group-structured-history";
5
+ import { finalizeChatTurn } from "../core/chat-turn";
5
6
  import { POKE_COOLDOWN_MS } from "./message";
6
7
 
7
8
  export function createPokeHandler(
@@ -25,141 +26,118 @@ export function createPokeHandler(
25
26
  const groupSessionId = `group:${groupId}`;
26
27
  if (runtimeState.isRateLimitBlocked()) return;
27
28
 
28
- const processingSet = runtimeState.processingSet;
29
- if (processingSet.has(groupSessionId)) {
30
- if (!runtimeState.isRateLimitBlocked()) {
31
- pluginCtx.queueManager.enqueue(groupSessionId, e, cfg);
32
- }
33
- return;
34
- }
35
-
36
- processingSet.add(groupSessionId);
37
- pluginCtx.sessionManager.getOrCreate(groupSessionId, "group", groupId);
38
-
39
29
  try {
40
- const userId = e.user_id || e.operator_id;
41
- const botRole = await getBotRole(groupId, ctx, e.self_id);
42
- const botNickname =
43
- cfg.nicknames[0] || ctx.pickBot(e.self_id).nickname || "Bot";
44
-
45
- let senderName = String(userId);
46
- try {
47
- const memberInfo = await ctx
48
- .pickBot(e.self_id)
49
- .getGroupMemberInfo(groupId, userId);
50
- senderName =
51
- (memberInfo as any).card ||
52
- (memberInfo as any).nickname ||
53
- String(userId);
54
- } catch {}
30
+ await pluginCtx.sessionTurnScheduler.run(
31
+ groupSessionId,
32
+ "poke",
33
+ async () => {
34
+ pluginCtx.sessionManager.getOrCreate(
35
+ groupSessionId,
36
+ "group",
37
+ groupId,
38
+ );
39
+ const userId = e.user_id || e.operator_id;
40
+ const botRole = await getBotRole(groupId, ctx, e.self_id);
41
+ const botNickname =
42
+ cfg.nicknames[0] || ctx.pickBot(e.self_id).nickname || "Bot";
55
43
 
56
- const targetMessage: TargetMessage = {
57
- userName: senderName,
58
- userId,
59
- userRole: "member",
60
- content: `[${senderName} poked you]`,
61
- timestamp: Date.now(),
62
- };
44
+ let senderName = String(userId);
45
+ try {
46
+ const memberInfo = await ctx
47
+ .pickBot(e.self_id)
48
+ .getGroupMemberInfo(groupId, userId);
49
+ senderName =
50
+ (memberInfo as any).card ||
51
+ (memberInfo as any).nickname ||
52
+ String(userId);
53
+ } catch {}
63
54
 
64
- const { history } = await pluginCtx.getGroupHistoryMessages(
65
- groupId,
66
- groupSessionId,
67
- ctx,
68
- cfg.historyCount,
69
- pluginCtx.db,
70
- e.self_id,
71
- pluginCtx.buildHistoryMediaOptions(pluginCtx.aiInstance, cfg),
72
- );
73
- const { groupName, memberCount } = await pluginCtx.getGroupInfoData(
74
- ctx,
75
- groupId,
76
- e.self_id,
77
- );
55
+ const targetMessage: TargetMessage = {
56
+ userName: senderName,
57
+ userId,
58
+ userRole: "member",
59
+ content: `[${senderName} poked you]`,
60
+ timestamp: Date.now(),
61
+ };
78
62
 
79
- const toolCtx = pluginCtx.buildToolContext({
80
- ctx,
81
- event: e,
82
- groupSessionId,
83
- groupId,
84
- userId,
85
- config: cfg,
86
- aiService: pluginCtx.aiService,
87
- db: pluginCtx.db,
88
- botRole,
89
- humanize: pluginCtx.humanize,
90
- targetMessage,
91
- selfId: e.self_id,
92
- });
63
+ const { history } = await pluginCtx.getGroupHistoryMessages(
64
+ groupId,
65
+ groupSessionId,
66
+ ctx,
67
+ cfg.historyCount,
68
+ pluginCtx.db,
69
+ e.self_id,
70
+ pluginCtx.buildHistoryMediaOptions(pluginCtx.aiInstance, cfg),
71
+ );
72
+ const { groupName, memberCount } =
73
+ await pluginCtx.getGroupInfoData(ctx, groupId, e.self_id);
93
74
 
94
- const result = await pluginCtx.runWithRateLimitGuard(
95
- () =>
96
- pluginCtx.runChat(
97
- pluginCtx.aiInstance,
98
- toolCtx,
99
- history,
75
+ const toolCtx = pluginCtx.buildToolContext({
76
+ ctx,
77
+ event: e,
78
+ groupSessionId,
79
+ groupId,
80
+ userId,
81
+ config: cfg,
82
+ aiService: pluginCtx.aiService,
83
+ db: pluginCtx.db,
84
+ botRole,
85
+ humanize: pluginCtx.humanize,
100
86
  targetMessage,
101
- {
102
- config: cfg,
103
- groupName,
104
- memberCount,
105
- botNickname,
106
- botRole,
107
- aiService: pluginCtx.aiService,
108
- isGroup: true,
109
- replyContext: {
110
- type: "poked",
111
- targetUser: targetMessage.userName,
112
- targetMessage: targetMessage.content,
113
- },
114
- },
115
- pluginCtx.humanize,
116
- pluginCtx.skillManager,
117
- {
118
- manager: pluginCtx.groupStructuredHistory,
119
- ttlMs: cfg.groupStructuredHistoryTtlMs,
120
- currentUserInputs: [
121
- buildStructuredUserInputFromTarget(targetMessage),
122
- ],
123
- },
124
- ),
125
- { userId, groupId, label: "poke" },
126
- );
127
- if (!result) return;
87
+ selfId: e.self_id,
88
+ });
128
89
 
129
- await pluginCtx.sendAIResponse(
130
- {
131
- ctx,
132
- groupId,
133
- messages: result.messages,
134
- config: cfg,
135
- sentIndices: toolCtx.sentMessageIndices,
136
- },
137
- e.self_id,
138
- );
90
+ const result = await pluginCtx.runWithRateLimitGuard(
91
+ () =>
92
+ pluginCtx.runChat(
93
+ pluginCtx.aiInstance,
94
+ toolCtx,
95
+ history,
96
+ targetMessage,
97
+ {
98
+ config: cfg,
99
+ groupName,
100
+ memberCount,
101
+ botNickname,
102
+ botRole,
103
+ aiService: pluginCtx.aiService,
104
+ isGroup: true,
105
+ replyContext: {
106
+ type: "poked",
107
+ targetUser: targetMessage.userName,
108
+ targetMessage: targetMessage.content,
109
+ },
110
+ },
111
+ pluginCtx.humanize,
112
+ pluginCtx.skillManager,
113
+ {
114
+ manager: pluginCtx.groupStructuredHistory,
115
+ ttlMs: cfg.groupStructuredHistoryTtlMs,
116
+ currentUserInputs: [
117
+ buildStructuredUserInputFromTarget(targetMessage),
118
+ ],
119
+ },
120
+ ),
121
+ { userId, groupId, label: "poke" },
122
+ );
123
+ if (!result) return;
139
124
 
140
- const now = Date.now();
141
- pluginCtx.saveBotMessages(
142
- groupId,
143
- groupSessionId,
144
- result.messages,
145
- now,
146
- cfg,
147
- pluginCtx.db,
148
- ctx,
149
- e.self_id,
150
- );
151
- pluginCtx.idleCheckManager.recordBotMessages(
152
- groupSessionId,
153
- result.messages.length,
154
- e.self_id,
125
+ await finalizeChatTurn(pluginCtx, {
126
+ event: e,
127
+ cfg,
128
+ result,
129
+ groupId,
130
+ groupSessionId,
131
+ userId,
132
+ selfId: e.self_id,
133
+ toolCtx,
134
+ send: true,
135
+ isLive: true,
136
+ });
137
+ },
155
138
  );
156
-
157
- await pluginCtx.sendEmoji(ctx, groupId, result.emojiPath, e.self_id);
158
- pluginCtx.sessionManager.touch(groupSessionId);
159
139
  } catch (err) {
160
140
  ctx.logger.error(`Poke processing failed: ${err}`);
161
- } finally {
162
- processingSet.delete(groupSessionId);
163
141
  }
164
142
  };
165
143
  }