mioku-plugin-chat 2.10.0 → 3.0.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.
@@ -1,4 +1,4 @@
1
- import type { AIInstance } from "mioku";
1
+ import type { AIInstance, Bot } from "mioku";
2
2
  import type { ChatConfig, MediaSummaryRecord } from "../../types";
3
3
  import type { HistoryMediaOptions } from "../../manage/types";
4
4
  import {
@@ -27,9 +27,7 @@ export function buildHistoryMediaProcessingOptions(
27
27
  getMediaSummary(key: string): MediaSummaryRecord | null;
28
28
  saveMediaSummary(summary: MediaSummaryRecord): void;
29
29
  },
30
- bot: {
31
- api<T = unknown>(action: string, params?: Record<string, unknown>): Promise<T>;
32
- },
30
+ bot: Bot,
33
31
  groupId: number,
34
32
  log: HistoryMediaProcessingOptions["logger"],
35
33
  runAIRequest?: <T>(request: () => Promise<T>) => Promise<T | null>,
@@ -53,17 +51,12 @@ export function getSegmentUrl(seg: {
53
51
  }
54
52
 
55
53
  export async function getVideoSourceCandidatesFromMessage(
56
- bot: {
57
- api(action: string, params?: Record<string, unknown>): Promise<unknown>;
58
- },
54
+ bot: Bot,
59
55
  messageId: number | string | undefined,
60
56
  ): Promise<string[]> {
61
- if (messageId == null) return [];
62
- const result = (await bot.api("get_msg", { message_id: messageId })) as {
63
- message?: unknown[];
64
- data?: { message?: unknown[] };
65
- };
66
- const segments = result?.message || result?.data?.message || [];
57
+ if (messageId == null || !bot) return [];
58
+ const result = await bot.getMessage(messageId);
59
+ const segments = result?.message ?? [];
67
60
  if (!Array.isArray(segments)) return [];
68
61
  const candidates: string[] = [];
69
62
  for (const seg of segments as MediaMessageSegment[]) {
@@ -86,6 +79,9 @@ export function getCardData(seg: unknown): string | null {
86
79
  return typeof data === "string" ? data : JSON.stringify(data);
87
80
  }
88
81
 
89
- export function isMediaAnalysisBlocked(config: ChatConfig, userId: number): boolean {
82
+ export function isMediaAnalysisBlocked(
83
+ config: ChatConfig,
84
+ userId: number,
85
+ ): boolean {
90
86
  return Boolean(config.mediaAnalysisBlacklistUsers?.includes(userId));
91
87
  }
@@ -1,6 +1,6 @@
1
- import type { AIInstance } from "mioku";
2
- import type { MiokiContext } from "mioki";
3
- import { logger } from "mioki";
1
+ import type { AIInstance, Bot } from "mioku";
2
+ import type { MiokuContext } from "mioku";
3
+ import { logger } from "mioku";
4
4
  import { prepareImageUrlsForModel } from "./media/image-compress";
5
5
 
6
6
  /**
@@ -126,23 +126,26 @@ export type MediaByMessageId =
126
126
  * @returns 媒体信息或 null
127
127
  */
128
128
  export async function getMediaByMessageId(
129
- ctx: MiokiContext,
129
+ ctx: MiokuContext,
130
130
  messageId: number,
131
131
  e: any,
132
132
  ): Promise<MediaByMessageId> {
133
133
  try {
134
- const msg = await ctx.pickBot(e.self_id).getMsg(messageId);
134
+ const bot: Bot = e?.bot ?? ctx.pickBot(e.self_id ?? "");
135
+ if (!bot) return null;
136
+ const msg = await bot.getMessage(messageId);
135
137
  if (!msg || !msg.message) {
136
138
  return null;
137
139
  }
138
140
 
139
- for (const seg of msg.message as any[]) {
141
+ for (const seg of Array.from(msg.message)) {
140
142
  if (seg.type === "image") {
141
- const url = seg.url || seg.data?.url;
143
+ const url = String(seg.data?.url ?? seg.data?.file ?? "");
142
144
  if (url) return { kind: "image", url };
143
145
  }
144
146
  if (seg.type === "video") {
145
- const { getSegmentSourceCandidates } = await import("./media/history-media");
147
+ const { getSegmentSourceCandidates } =
148
+ await import("./media/history-media");
146
149
  const sources = getSegmentSourceCandidates(seg);
147
150
  if (sources.length > 0) return { kind: "video", sources };
148
151
  }
@@ -164,7 +167,7 @@ export async function getMediaByMessageId(
164
167
  * @returns 图片 URL 或 null
165
168
  */
166
169
  export async function getQuoteImageUrl(
167
- ctx: MiokiContext,
170
+ ctx: MiokuContext,
168
171
  event: any,
169
172
  ): Promise<string | null> {
170
173
  if (!event.quote_id) {
@@ -172,7 +175,9 @@ export async function getQuoteImageUrl(
172
175
  }
173
176
 
174
177
  try {
175
- const quotedMsg = await ctx.getQuoteMsg(event);
178
+ const bot = event?.bot;
179
+ if (!bot) return null;
180
+ const quotedMsg = await bot.getMessage(event.quote_id);
176
181
  if (!quotedMsg || !quotedMsg.message) {
177
182
  return null;
178
183
  }
package/core/prompt.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
2
- import { logger } from "mioki";
2
+ import { logger } from "mioku";
3
3
  import type {
4
4
  SkillPermissionRole,
5
5
  AIService,
@@ -293,7 +293,7 @@ ${AUDIO_MODE_LINE[audioStrength]}`);
293
293
  ### Optional Markdown Screenshot Format
294
294
  - You MAY optionally send one rendered Markdown screenshot by wrapping content with exact tags: <MARKDOWN> ... </MARKDOWN>
295
295
  - Put the Markdown block on its own message whenever possible.
296
- - It is forbidden to use Markdown syntax or formulas in plain text; they must be rendered using <MARKDOWN> blocks.
296
+ - It is forbidden to use Markdown syntax or formulas in plain text); they must be rendered using <MARKDOWN> blocks.
297
297
  ${MARKDOWN_MODE_LINE[markdownStrength]}
298
298
  - Inside <MARKDOWN>...</MARKDOWN>, there is NO length limit. If the user needs detail, explain clearly and thoroughly instead of over-compressing.
299
299
  `);
@@ -304,7 +304,7 @@ ${MARKDOWN_MODE_LINE[markdownStrength]}
304
304
  lines.push(`
305
305
  ### Tool Calling Format
306
306
  - When you decide to use a tool, you MUST use the structured tool_calls mechanism provided by the API
307
- - web_search and web_read_page are limited per conversation; do not retry excessively`);
307
+ - web_search and web_read_page are limited per conversation); do not retry excessively`);
308
308
 
309
309
  appendEmojiSection(lines, ctx, emojiStrength);
310
310
 
@@ -1,4 +1,4 @@
1
- import { logger } from "mioki";
1
+ import {logger} from "mioku";
2
2
  import type { AITool } from "mioku";
3
3
  import { TOOL_RESULT_FOLLOWUP_KEY } from "mioku";
4
4
  import type { ToolContext } from "../../types";
@@ -73,9 +73,10 @@ export function createInfoTools(toolCtx: ToolContext): AITool[] {
73
73
  },
74
74
  handler: async (args) => {
75
75
  try {
76
- const info = await toolCtx.ctx
77
- .pickBot(toolCtx.event.self_id)
78
- .getGroupMemberInfo(toolCtx.groupId!, args.user_id);
76
+ const bot = toolCtx.event?.bot;
77
+ if (!bot) return { error: `Failed to get member info: bot not found` };
78
+ const info = await bot.getMemberInfo(toolCtx.groupId!, args.user_id);
79
+ if (!info) return { error: `Failed to get member info: member not found` };
79
80
  return {
80
81
  nickname: info.nickname,
81
82
  card: info.card,
@@ -107,9 +108,9 @@ export function createInfoTools(toolCtx: ToolContext): AITool[] {
107
108
  },
108
109
  handler: async (args) => {
109
110
  try {
110
- const list = await toolCtx.ctx
111
- .pickBot(toolCtx.event.self_id)
112
- .getGroupMemberList(toolCtx.groupId!);
111
+ const bot = toolCtx.event?.bot;
112
+ if (!bot) return { error: `Failed to get member list: bot not found` };
113
+ const list = await bot.getGroupMembers(toolCtx.groupId!);
113
114
  const members = (list as any[]).map((m) => ({
114
115
  user_id: m.user_id,
115
116
  nickname: m.card || m.nickname,
package/core/tools/web.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { logger } from "mioki";
1
+ import { HistoryMessage, logger } from "mioku";
2
2
  import type { AITool } from "mioku";
3
3
  import type { ChatMessage, ToolContext } from "../../types";
4
4
  import { searchWebWithSearxng } from "../web/searxng";
@@ -47,7 +47,8 @@ function extractGroupHistoryText(raw: any): string {
47
47
  continue;
48
48
  }
49
49
  if (seg?.type === "at") {
50
- const target = seg?.qq || seg?.data?.qq || seg?.data?.id || seg?.data?.user_id;
50
+ const target =
51
+ seg?.qq || seg?.data?.qq || seg?.data?.id || seg?.data?.user_id;
51
52
  if (target === "all" || target === "everyone") parts.push("@全体成员");
52
53
  else if (target) parts.push(`@${target}`);
53
54
  continue;
@@ -68,15 +69,12 @@ async function fetchGroupHistoryByMessageIdPaging(
68
69
  ): Promise<ChatMessage[]> {
69
70
  if (!toolCtx.groupId || limit <= 0) return [];
70
71
 
71
- const selfId = Number(toolCtx.event?.self_id || 0);
72
- if (!selfId) return [];
73
-
74
- const bot = toolCtx.ctx.pickBot(selfId);
72
+ const bot = toolCtx.event?.bot;
75
73
  if (!bot) return [];
76
74
 
77
75
  const collected: ChatMessage[] = [];
78
76
  const seenMessageIds = new Set<string>();
79
- let cursorMessageId = 0;
77
+ let cursorMessageId = "";
80
78
  const maxPages = Math.max(1, Math.ceil(limit / 200) + 5);
81
79
  let page = 0;
82
80
 
@@ -84,73 +82,75 @@ async function fetchGroupHistoryByMessageIdPaging(
84
82
  const remaining = limit - collected.length;
85
83
  const pageSize = Math.min(200, remaining);
86
84
 
87
- let response: any;
85
+ let history: HistoryMessage[];
88
86
  try {
89
- response = await (bot as any).api("get_group_msg_history", {
90
- group_id: String(toolCtx.groupId),
91
- message_seq: String(cursorMessageId),
92
- count: pageSize,
93
- reverse_order: false,
94
- disable_get_url: true,
95
- parse_mult_msg: false,
96
- quick_reply: false,
97
- });
87
+ history = await bot.getHistory(
88
+ { type: "group", group_id: toolCtx.groupId },
89
+ cursorMessageId || undefined,
90
+ pageSize,
91
+ );
98
92
  } catch (err) {
99
93
  const errText = String(err);
100
94
  if (
101
- cursorMessageId > 0 &&
102
- (errText.includes("不存在") || errText.toLowerCase().includes("not exist"))
95
+ cursorMessageId !== "" &&
96
+ (errText.includes("不存在") ||
97
+ errText.toLowerCase().includes("not exist"))
103
98
  ) {
104
- logger.info(`[recall_memory] get_group_msg_history stop at cursor ${cursorMessageId}: ${errText}`);
99
+ logger.info(
100
+ `[recall_memory] get_group_msg_history stop at cursor ${cursorMessageId}: ${errText}`,
101
+ );
105
102
  } else {
106
- logger.warn(`[recall_memory] get_group_msg_history failed at cursor ${cursorMessageId}: ${errText}`);
103
+ logger.warn(
104
+ `[recall_memory] get_group_msg_history failed at cursor ${cursorMessageId}: ${errText}`,
105
+ );
107
106
  }
108
107
  break;
109
108
  }
110
109
 
111
- const rawMessages = response?.messages || response?.data?.messages || [];
112
- if (!Array.isArray(rawMessages) || rawMessages.length === 0) break;
110
+ if (!Array.isArray(history) || history.length === 0) break;
113
111
 
114
- let oldestMessageId: number | null = null;
112
+ let oldestMessageId: string | null = null;
115
113
  let newAdded = 0;
116
114
 
117
- for (const raw of rawMessages) {
118
- const messageId = Number(raw?.message_id || raw?.message_seq || 0);
115
+ for (const raw of history) {
116
+ const messageId = String(raw.message_id || "");
119
117
  const key =
120
- messageId > 0
118
+ messageId !== ""
121
119
  ? `mid:${messageId}`
122
- : `${String(raw?.user_id || "unknown")}:${String(raw?.time || "0")}:${String(raw?.raw_message || "")}`;
120
+ : `${String(raw.user_id || "unknown")}:${String(raw.time || "0")}:${extractGroupHistoryText(raw)}`;
123
121
  if (seenMessageIds.has(key)) continue;
124
122
  seenMessageIds.add(key);
125
123
 
126
124
  const content = extractGroupHistoryText(raw);
127
125
  if (!content.trim()) continue;
128
126
 
129
- const ts = typeof raw?.time === "number" ? raw.time * 1000 : Date.now();
127
+ const ts = raw.time ?? Date.now();
130
128
  collected.push({
131
129
  sessionId: toolCtx.sessionId,
132
- role: String(raw?.user_id) === String(selfId) ? "assistant" : "user",
130
+ role: String(raw.user_id) === String(bot.bot_id) ? "assistant" : "user",
133
131
  content,
134
- userId: typeof raw?.user_id === "number" ? raw.user_id : Number(raw?.user_id),
135
- userName: raw?.sender?.card || raw?.sender?.nickname || String(raw?.user_id || "unknown"),
136
- userRole: raw?.sender?.role || "member",
132
+ userId: Number(raw.user_id || 0),
133
+ userName: raw.nickname || String(raw.user_id || "unknown"),
134
+ userRole: "member",
137
135
  groupId: toolCtx.groupId,
138
136
  timestamp: ts,
139
- messageId: messageId > 0 ? messageId : undefined,
137
+ messageId: messageId !== "" ? Number(messageId) : undefined,
140
138
  });
141
139
  newAdded += 1;
142
140
 
143
- if (messageId > 0 && (oldestMessageId === null || messageId < oldestMessageId)) {
141
+ if (
142
+ messageId !== "" &&
143
+ (oldestMessageId === null || messageId < oldestMessageId)
144
+ ) {
144
145
  oldestMessageId = messageId;
145
146
  }
146
147
  }
147
148
 
148
- if (newAdded === 0 || oldestMessageId === null || oldestMessageId <= 1) break;
149
+ if (newAdded === 0 || oldestMessageId === null || oldestMessageId === "")
150
+ break;
149
151
 
150
- // NapCat message_seq expects an existing id; don't subtract 1 (IDs may be non-contiguous).
151
- const nextCursor = oldestMessageId;
152
- if (nextCursor === cursorMessageId) break;
153
- cursorMessageId = nextCursor;
152
+ if (oldestMessageId === cursorMessageId) break;
153
+ cursorMessageId = oldestMessageId;
154
154
  page += 1;
155
155
  }
156
156
 
@@ -182,7 +182,8 @@ export function createWebSearchTool(toolCtx: ToolContext): AITool {
182
182
  },
183
183
  limit: {
184
184
  type: "number",
185
- description: "Max number of results to return. Will be clamped by config maxLimit.",
185
+ description:
186
+ "Max number of results to return. Will be clamped by config maxLimit.",
186
187
  },
187
188
  time_range: {
188
189
  type: "string",
@@ -192,17 +193,20 @@ export function createWebSearchTool(toolCtx: ToolContext): AITool {
192
193
  categories: {
193
194
  type: "array",
194
195
  items: { type: "string" },
195
- description: 'Optional categories, e.g. ["general"], ["news"], ["science"]',
196
+ description:
197
+ 'Optional categories, e.g. ["general"], ["news"], ["science"]',
196
198
  },
197
199
  engines: {
198
200
  type: "array",
199
201
  items: { type: "string" },
200
- description: 'Optional engines, e.g. ["google"], ["bing"], ["duckduckgo"]',
202
+ description:
203
+ 'Optional engines, e.g. ["google"], ["bing"], ["duckduckgo"]',
201
204
  },
202
205
  },
203
206
  required: [],
204
207
  },
205
- handler: async (args) => searchWebWithSearxng(toolCtx.config.searxng, args || {}),
208
+ handler: async (args) =>
209
+ searchWebWithSearxng(toolCtx.config.searxng, args || {}),
206
210
  };
207
211
  }
208
212
 
@@ -217,7 +221,10 @@ export function createWebReadPageTool(toolCtx: ToolContext): AITool {
217
221
  parameters: {
218
222
  type: "object",
219
223
  properties: {
220
- url: { type: "string", description: "The http/https URL of the webpage to read" },
224
+ url: {
225
+ type: "string",
226
+ description: "The http/https URL of the webpage to read",
227
+ },
221
228
  render_js: {
222
229
  type: "boolean",
223
230
  description:
@@ -234,7 +241,8 @@ export function createWebReadPageTool(toolCtx: ToolContext): AITool {
234
241
  handler: async (args) => {
235
242
  try {
236
243
  const ai = toolCtx.config.webReader.useWorkingModel
237
- ? (toolCtx.aiService.getInstanceByRole?.("working") ?? toolCtx.aiService.getDefault())
244
+ ? (toolCtx.aiService.getInstanceByRole?.("working") ??
245
+ toolCtx.aiService.getDefault())
238
246
  : undefined;
239
247
  if (toolCtx.config.webReader.useWorkingModel && !ai) {
240
248
  return { success: false, error: "AI instance not available" };
@@ -275,19 +283,41 @@ export function createRecallMemoryTool(toolCtx: ToolContext): AITool {
275
283
  const question = String(args?.question || "").trim();
276
284
  if (!question) return { success: false, error: "question is required" };
277
285
 
278
- const ai = toolCtx.aiService.getInstanceByRole?.("working") ?? toolCtx.aiService.getDefault();
286
+ const ai =
287
+ toolCtx.aiService.getInstanceByRole?.("working") ??
288
+ toolCtx.aiService.getDefault();
279
289
  if (!ai) return { success: false, error: "AI instance not available" };
280
290
 
281
- const groupHistoryLimit = resolveGroupRecallLimit(toolCtx.config.memory?.groupHistoryLimit);
282
- const userHistoryLimit = resolveUserHistoryLimit(toolCtx.config.memory?.userHistoryLimit);
283
- const groupHistoryMessages = await fetchGroupHistoryByMessageIdPaging(toolCtx, groupHistoryLimit);
284
- const targetUserIds = extractTargetUserIdsFromQuestion(question, toolCtx.userId);
285
- const userHistories: MemoryUserHistoryChunk[] = targetUserIds.map((userId) => ({
286
- userId,
287
- messages: toolCtx.db.getMessagesByUser(userId, toolCtx.sessionId, userHistoryLimit),
288
- }));
291
+ const groupHistoryLimit = resolveGroupRecallLimit(
292
+ toolCtx.config.memory?.groupHistoryLimit,
293
+ );
294
+ const userHistoryLimit = resolveUserHistoryLimit(
295
+ toolCtx.config.memory?.userHistoryLimit,
296
+ );
297
+ const groupHistoryMessages = await fetchGroupHistoryByMessageIdPaging(
298
+ toolCtx,
299
+ groupHistoryLimit,
300
+ );
301
+ const targetUserIds = extractTargetUserIdsFromQuestion(
302
+ question,
303
+ toolCtx.userId,
304
+ );
305
+ const userHistories: MemoryUserHistoryChunk[] = targetUserIds.map(
306
+ (userId) => ({
307
+ userId,
308
+ messages: toolCtx.db.getMessagesByUser(
309
+ userId,
310
+ toolCtx.sessionId,
311
+ userHistoryLimit,
312
+ ),
313
+ }),
314
+ );
289
315
 
290
- const retriever = new MemoryRetrieval(ai, () => toolCtx.config, toolCtx.db);
316
+ const retriever = new MemoryRetrieval(
317
+ ai,
318
+ () => toolCtx.config,
319
+ toolCtx.db,
320
+ );
291
321
  const answer = await retriever.retrieveByQuestion({
292
322
  sessionId: toolCtx.sessionId,
293
323
  question,
package/core/tools.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { logger } from "mioki";
1
+ import { logger } from "mioku";
2
2
  import type { AITool } from "mioku";
3
3
  import type { ToolContext } from "../types";
4
4
  import type { SkillSessionManager } from "../manage/skill-session";
@@ -1,4 +1,4 @@
1
- import { logger } from "mioki";
1
+ import { logger } from "mioku";
2
2
  import type { SearxngConfig } from "../../types";
3
3
 
4
4
  type TimeRange = "day" | "month" | "year";
@@ -1,4 +1,4 @@
1
- import { logger } from "mioki";
1
+ import { logger } from "mioku";
2
2
  import puppeteer from "puppeteer";
3
3
  import type { AIInstance } from "mioku";
4
4
  import type { WebReaderConfig } from "../../types";
@@ -266,7 +266,7 @@ function pickBestParagraphs(text: string, maxChars: number): string {
266
266
 
267
267
  const scored = paragraphs.map((paragraph) => {
268
268
  const value = paragraph.text;
269
- const punctuationCount = (value.match(/[,。!?;:,.!?;:]/g) || [])
269
+ const punctuationCount = (value.match(/[,。!?;:,.!?);:]/g) || [])
270
270
  .length;
271
271
  const sentenceCount = (value.match(/[。!?.!?]/g) || []).length;
272
272
  const linkLikeCount = (value.match(/https?:\/\/|www\./g) || []).length;
@@ -1,4 +1,4 @@
1
- import type { MiokiContext } from "mioki";
1
+ import type { MiokuContext } from "mioku";
2
2
  import type { AIService } from "mioku";
3
3
  import type { ChatPluginContext } from "../context";
4
4
  import type { ChatConfig, TargetMessage } from "../types";
@@ -10,7 +10,7 @@ export async function handleIdleCheckDebug(
10
10
  e: any,
11
11
  cfg: ChatConfig,
12
12
  ): Promise<void> {
13
- const ctx: MiokiContext = pluginCtx.ctx;
13
+ const ctx: MiokuContext = pluginCtx.ctx;
14
14
  const isOwner = ctx.isOwner?.(e) ?? false;
15
15
  if (!isOwner) {
16
16
  await e.reply("只有主人才能使用这个指令~");
@@ -32,7 +32,7 @@ export async function handleIdleCheckDebug(
32
32
  async () => {
33
33
  const now = Date.now();
34
34
  const botNickname =
35
- cfg.nicknames[0] || ctx.pickBot(e.self_id).nickname || "Bot";
35
+ cfg.nicknames[0] || e.bot?.nickname || "Bot";
36
36
  ctx.logger.info(`[Debug] Manual idle check: group ${targetGroupId}`);
37
37
 
38
38
  const { history } = await pluginCtx.getGroupHistoryMessages(
@@ -1,8 +1,5 @@
1
1
  import type { ChatPluginContext, ChatHandlerState } from "../context";
2
- import type {
3
- GroupMessageEvent,
4
- PrivateMessageEvent,
5
- } from "napcat-sdk";
2
+ import type { MessageEvent } from "mioku";
6
3
  import {
7
4
  isGroupAllowed,
8
5
  shouldTrigger,
@@ -37,17 +34,19 @@ export function createMessageHandler(
37
34
  const { ctx } = pluginCtx;
38
35
  const { getConfig, matchMessageCommands, runtimeState } = state;
39
36
 
40
- return async (e: GroupMessageEvent | PrivateMessageEvent) => {
37
+ return async (e: MessageEvent) => {
41
38
  const isGroup = e.message_type === "group";
42
- const groupId: number | undefined = isGroup ? e.group_id : undefined;
39
+ const groupId: number | undefined = isGroup
40
+ ? Number(e.group_id)
41
+ : undefined;
43
42
  const cfg = await getConfig(groupId);
44
43
  if (!cfg.model && !cfg.apiKey) return;
45
44
  if (!e?.message || !Array.isArray(e.message)) return;
46
45
 
47
46
  const text = ctx.text(e) || "";
48
- const userId: number = e.user_id || e.sender?.user_id;
47
+ const userId: number = Number(e.user_id || e.sender?.user_id || 0);
49
48
 
50
- if (userId === e.self_id) return;
49
+ if (userId === Number(e.self_id || 0)) return;
51
50
 
52
51
  if (matchMessageCommands && matchMessageCommands(text).length > 0) return;
53
52
 
@@ -78,9 +77,15 @@ export function createMessageHandler(
78
77
  if (groupId && !isGroupAllowed(groupId, cfg)) return;
79
78
 
80
79
  // 媒体分析
81
- if (isGroup && groupId && e.message && !isMediaAnalysisBlocked(cfg, userId)) {
82
- const visionAI = pluginCtx.visionAIInstance ?? pluginCtx.aiService.getDefault();
83
- const bot = ctx.pickBot(e.self_id) as any;
80
+ if (
81
+ isGroup &&
82
+ groupId &&
83
+ e.message &&
84
+ !isMediaAnalysisBlocked(cfg, userId)
85
+ ) {
86
+ const visionAI =
87
+ pluginCtx.visionAIInstance ?? pluginCtx.aiService.getDefault();
88
+ const bot = e.bot;
84
89
  const mediaOptions = visionAI
85
90
  ? buildHistoryMediaProcessingOptions(
86
91
  visionAI,
@@ -109,15 +114,21 @@ export function createMessageHandler(
109
114
  if (seg.type === "image") {
110
115
  const imageUrl = getSegmentUrl(seg);
111
116
  if (imageUrl) {
112
- processImage(visionAI, imageUrl, cfg.multimodalWorkingModel, pluginCtx.db, {
113
- runAIRequest: (request) =>
114
- pluginCtx.runWithRateLimitGuard(request, {
115
- userId,
116
- groupId,
117
- label: "image-analysis",
118
- skipRetryOnRateLimit: true,
119
- }),
120
- }).catch((err) =>
117
+ processImage(
118
+ visionAI,
119
+ imageUrl,
120
+ cfg.multimodalWorkingModel,
121
+ pluginCtx.db,
122
+ {
123
+ runAIRequest: (request) =>
124
+ pluginCtx.runWithRateLimitGuard(request, {
125
+ userId,
126
+ groupId,
127
+ label: "image-analysis",
128
+ skipRetryOnRateLimit: true,
129
+ }),
130
+ },
131
+ ).catch((err) =>
121
132
  ctx.logger.error(`[image-analyzer] Failed: ${err}`),
122
133
  );
123
134
  }
@@ -162,7 +173,9 @@ export function createMessageHandler(
162
173
  }
163
174
  }
164
175
  }
165
- if (e.sub_type === "notice") {
176
+ if (
177
+ (e.raw as { sub_type?: string } | undefined)?.sub_type === "notice"
178
+ ) {
166
179
  summarizeGroupNotice(e, mediaOptions)
167
180
  .then((noticeMessage) => {
168
181
  if (!noticeMessage) return;
@@ -192,8 +205,7 @@ export function createMessageHandler(
192
205
  const learnSessionId = `group:${groupId}`;
193
206
  const learnMsg = buildChatMessageFromEvent(e, text, true, groupId);
194
207
  const hasText = !!learnMsg.content?.trim();
195
- const hasMultimodal =
196
- !!cfg.isMultimodal && !!cfg.multimodalWorkingModel;
208
+ const hasMultimodal = !!cfg.isMultimodal && !!cfg.multimodalWorkingModel;
197
209
  if (hasText || hasMultimodal) {
198
210
  pluginCtx
199
211
  .recordGroupMessageForLearning(learnMsg, learnSessionId)
@@ -235,7 +247,11 @@ export function createMessageHandler(
235
247
  if (pluginCtx.queueProcessor.isInDynamicDelay(groupSessionId)) {
236
248
  if (atBot && !runtimeState.isRateLimitBlocked()) {
237
249
  pluginCtx.rateLimiter.recordInteraction(groupId, userId);
238
- pluginCtx.queueProcessor.collectDynamicDelayMessage(groupSessionId, e, text);
250
+ pluginCtx.queueProcessor.collectDynamicDelayMessage(
251
+ groupSessionId,
252
+ e,
253
+ text,
254
+ );
239
255
  }
240
256
  return;
241
257
  }
@@ -259,7 +275,7 @@ export function createMessageHandler(
259
275
  groupSessionId,
260
276
  groupId,
261
277
  delayInfo.delayMs,
262
- e.self_id,
278
+ Number(e.self_id || 0),
263
279
  );
264
280
  return;
265
281
  }
@@ -278,11 +294,10 @@ export function createMessageHandler(
278
294
  ctx,
279
295
  cfg.historyCount,
280
296
  pluginCtx.db,
281
- e.self_id,
297
+ Number(e.self_id || 0),
282
298
  pluginCtx.buildHistoryMediaOptions(pluginCtx.aiInstance, cfg),
283
299
  );
284
- const botNickname =
285
- cfg.nicknames[0] || ctx.pickBot(e.self_id).nickname || "Bot";
300
+ const botNickname = cfg.nicknames[0] || e.bot?.nickname || "Bot";
286
301
  const planResult = await pluginCtx.humanize.actionPlanner.plan(
287
302
  groupSessionId,
288
303
  botNickname,
@@ -304,7 +319,7 @@ export function createMessageHandler(
304
319
  pluginCtx.rateLimiter.recordInteraction(groupId, userId);
305
320
  pluginCtx.queueProcessor.scheduleQueuedMessages(
306
321
  groupSessionId,
307
- e.self_id,
322
+ Number(e.self_id || 0),
308
323
  );
309
324
  }
310
325
  return;