mioku-plugin-chat 2.10.1 → 3.0.1

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,5 +1,4 @@
1
1
  import type { AIInstance, SessionToolDefinition } from "mioku";
2
- import { logger } from "mioki";
3
2
  import type { AITool } from "mioku";
4
3
  import type {
5
4
  ToolContext,
@@ -19,6 +18,8 @@ import {
19
18
  } from "./external-skills";
20
19
  import {
21
20
  consumeCompleteStreamUnits,
21
+ isQuoteMarkerOnlyUnit,
22
+ mergeQuoteMarkerUnits,
22
23
  splitOutgoingUnits,
23
24
  } from "./media/markdown-message";
24
25
  import {
@@ -90,15 +91,16 @@ export async function runChat(
90
91
  const staticPrompt = buildStaticSystemPrompt(staticCtx);
91
92
  const dynamicUserContext = buildDynamicUserContext(dynamicCtx);
92
93
 
93
- logger.info(
94
+ const engineLog = toolCtx.ctx.logger;
95
+ engineLog.info(
94
96
  `[chat-engine] Session ${toolCtx.sessionId} | target: ${targetMessage.userName}(${targetMessage.userId}): "${targetMessage.content}"`,
95
97
  );
96
98
  if (toolCtx.config.debug) {
97
- logger.info("[chat-engine] === Static System Prompt ===");
98
- logger.info(staticPrompt);
99
- logger.info("[chat-engine] === Dynamic User Context ===");
100
- logger.info(dynamicUserContext);
101
- logger.info("[chat-engine] === End Prompts ===");
99
+ engineLog.info("[chat-engine] === Static System Prompt ===");
100
+ engineLog.info(staticPrompt);
101
+ engineLog.info("[chat-engine] === Dynamic User Context ===");
102
+ engineLog.info(dynamicUserContext);
103
+ engineLog.info("[chat-engine] === End Prompts ===");
102
104
  }
103
105
 
104
106
  const hasStructuredHistory =
@@ -181,6 +183,7 @@ export async function runChat(
181
183
  streamedMessages.push(text);
182
184
  };
183
185
 
186
+ let pendingQuoteMarkerLines: string[] = [];
184
187
  const flushStreamBuffer = async (force: boolean): Promise<void> => {
185
188
  while (true) {
186
189
  const { units, rest } = consumeCompleteStreamUnits(streamBuffer, force);
@@ -191,17 +194,29 @@ export async function runChat(
191
194
 
192
195
  streamBuffer = rest;
193
196
  for (const unit of units) {
197
+ if (!unit.trim()) {
198
+ continue;
199
+ }
200
+ if (isQuoteMarkerOnlyUnit(unit)) {
201
+ pendingQuoteMarkerLines.push(unit.trim());
202
+ continue;
203
+ }
204
+ const mergedUnit = pendingQuoteMarkerLines.length
205
+ ? [...pendingQuoteMarkerLines, unit].join("\n")
206
+ : unit;
207
+ pendingQuoteMarkerLines = [];
194
208
  const unitIndex = streamUnitIndex;
195
209
  streamUnitIndex += 1;
196
- if (unit.trim()) {
197
- await emitStreamSegment(unit, unitIndex);
198
- }
210
+ await emitStreamSegment(mergedUnit, unitIndex);
199
211
  }
200
212
 
201
213
  if (!force) {
202
214
  break;
203
215
  }
204
216
  }
217
+ if (force) {
218
+ pendingQuoteMarkerLines = [];
219
+ }
205
220
  };
206
221
 
207
222
  const runComplete = () =>
@@ -252,7 +267,9 @@ export async function runChat(
252
267
  targetMessage,
253
268
  })
254
269
  .catch((err) =>
255
- logger.warn(`[chat-engine] background emotion refresh failed: ${err}`),
270
+ toolCtx.ctx.logger.warn(
271
+ `[chat-engine] background emotion refresh failed: ${err}`,
272
+ ),
256
273
  );
257
274
 
258
275
  if (streamEnabled) {
@@ -261,9 +278,9 @@ export async function runChat(
261
278
  }
262
279
 
263
280
  if (toolCtx.config.debug) {
264
- logger.info("[chat-engine] === Raw AI Reply ===");
265
- logger.info(response.content || "(empty)");
266
- logger.info("[chat-engine] === End Raw AI Reply ===");
281
+ toolCtx.ctx.logger.info("[chat-engine] === Raw AI Reply ===");
282
+ toolCtx.ctx.logger.info(response.content || "(empty)");
283
+ toolCtx.ctx.logger.info("[chat-engine] === End Raw AI Reply ===");
267
284
  }
268
285
 
269
286
  const allToolCalls = response.allToolCalls || [];
@@ -275,16 +292,18 @@ export async function runChat(
275
292
  }
276
293
 
277
294
  if (toolCtx.config.debug && response.reasoning) {
278
- logger.info(`[chat-engine] AI reasoning: ${response.reasoning}`);
295
+ toolCtx.ctx.logger.info(
296
+ `[chat-engine] AI reasoning: ${response.reasoning}`,
297
+ );
279
298
  }
280
299
 
281
300
  if (toolCtx.config.debug && allToolCalls.length > 0) {
282
301
  for (const toolCall of allToolCalls) {
283
302
  const resultPreview = JSON.stringify(toolCall.result);
284
- logger.info(
303
+ toolCtx.ctx.logger.info(
285
304
  `[chat-engine] Tool call: ${toolCall.name}(${JSON.stringify(toolCall.arguments).substring(0, 100)})`,
286
305
  );
287
- logger.info(
306
+ toolCtx.ctx.logger.info(
288
307
  `[chat-engine] Tool result: ${toolCall.name} -> ${resultPreview ? resultPreview.substring(0, 300) : "undefined"}`,
289
308
  );
290
309
  }
@@ -298,7 +317,9 @@ export async function runChat(
298
317
  currentUserMessages,
299
318
  [],
300
319
  );
301
- logger.info(`[chat-engine] Session ${toolCtx.sessionId} ended by tool`);
320
+ toolCtx.ctx.logger.info(
321
+ `[chat-engine] Session ${toolCtx.sessionId} ended by tool`,
322
+ );
302
323
  toolCtx.aiService.finalizeUsage?.(usageId, {
303
324
  sentUserMessages: 0,
304
325
  sentAssistantMessages: 0,
@@ -358,9 +379,9 @@ export async function runChat(
358
379
  }
359
380
  }
360
381
 
361
- const finalMessages = splitOutgoingUnits(finalText).filter(
362
- (unit) => unit.trim() && unit.trim() !== "---",
363
- );
382
+ const finalMessages = mergeQuoteMarkerUnits(
383
+ splitOutgoingUnits(finalText),
384
+ ).filter((unit) => unit.trim() && unit.trim() !== "---");
364
385
  const sentAssistantMessages = streamEnabled
365
386
  ? streamedMessages.length
366
387
  : finalMessages.length;
@@ -369,7 +390,7 @@ export async function runChat(
369
390
  sentAssistantMessages,
370
391
  });
371
392
 
372
- logger.info(
393
+ toolCtx.ctx.logger.info(
373
394
  `[chat-engine] Session ${toolCtx.sessionId} done | ${finalMessages.length} msg(s), ${allToolCalls.length} tool call(s)`,
374
395
  );
375
396
 
@@ -955,7 +976,7 @@ ${failedSummary}
955
976
  return text;
956
977
  }
957
978
  } catch (err) {
958
- logger.warn(
979
+ toolCtx.ctx.logger.warn(
959
980
  `[chat-engine] Failed to generate tool-failure fallback reply: ${err}`,
960
981
  );
961
982
  }
package/core/chat-turn.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MiokiContext } from "mioki";
1
+ import type { MiokuContext } from "mioku";
2
2
  import type { AITool, ChatRuntimePromptInjection } from "mioku";
3
3
  import type { ChatPluginContext, ChatRuntimeState } from "../context";
4
4
  import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
@@ -41,14 +41,14 @@ interface ResolvedRuntimeContext {
41
41
  userRole: "owner" | "admin" | "member";
42
42
  userTitle?: string;
43
43
  groupName?: string;
44
- messageId?: number;
44
+ messageId?: string;
45
45
  }
46
46
 
47
47
  const NO_NEW_MESSAGE =
48
48
  "[No new user message in this turn. Reply naturally based on the runtime instruction and recent context.]";
49
49
 
50
50
  function buildRuntimeTargetMessageContent(
51
- ctx: MiokiContext,
51
+ ctx: MiokuContext,
52
52
  event: any,
53
53
  overrideContent?: string,
54
54
  ): string {
@@ -58,7 +58,7 @@ function buildRuntimeTargetMessageContent(
58
58
  }
59
59
 
60
60
  function resolveRuntimeContext(
61
- ctx: MiokiContext,
61
+ ctx: MiokuContext,
62
62
  options: ExecuteChatRuntimeRequestOptions,
63
63
  ): ResolvedRuntimeContext {
64
64
  if (options.event) {
@@ -147,13 +147,13 @@ export async function finalizeChatTurn(
147
147
  const { groupId, groupSessionId, userId, selfId, cfg, toolCtx, result, event } = args;
148
148
 
149
149
  if (groupId) {
150
- await pluginCtx.sendAIResponse(
150
+ const sentMessageIds = await pluginCtx.sendAIResponse(
151
151
  { ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices, audioService: pluginCtx.audioService },
152
152
  selfId,
153
153
  );
154
- await pluginCtx.sendEmoji(ctx, groupId, result.emojiPath, selfId);
154
+ await pluginCtx.sendEmoji(ctx, groupId, result.emojiPath, event?.bot);
155
155
  const now = Date.now();
156
- pluginCtx.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, pluginCtx.db, ctx, selfId);
156
+ pluginCtx.saveBotMessages(groupId, groupSessionId, result.messages, now, cfg, pluginCtx.db, ctx, event?.bot, sentMessageIds);
157
157
  if (args.isLive) {
158
158
  pluginCtx.idleCheckManager.recordBotMessages(groupSessionId, result.messages.length, selfId);
159
159
  }
@@ -170,9 +170,9 @@ export async function finalizeChatTurn(
170
170
  if (args.isLive) {
171
171
  await event.reply([emojiSegment]);
172
172
  } else {
173
- const bot = ctx.pickBot(selfId);
173
+ const bot = args.event?.bot;
174
174
  if (!bot) throw new Error(`bot ${selfId} not found`);
175
- await bot.sendPrivateMsg(userId, [emojiSegment]);
175
+ await bot.sendMessage({ type: "private", user_id: userId}, [emojiSegment]);
176
176
  }
177
177
  } catch (err) {
178
178
  ctx.logger.warn(
@@ -260,7 +260,7 @@ export async function processChat(
260
260
  }));
261
261
 
262
262
  const botNickname =
263
- cfg.nicknames[0] || ctx.pickBot(e.self_id).nickname || "Bot";
263
+ cfg.nicknames[0] || e.bot?.nickname || "Bot";
264
264
  const botRole = groupId ? await getBotRole(groupId, ctx, e.self_id) : "member";
265
265
  let groupName: string | undefined;
266
266
  let memberCount: number | undefined;
@@ -451,7 +451,7 @@ async function executeChatRuntimeRequestNow(
451
451
 
452
452
  const botRole = groupId ? await getBotRole(groupId, pluginCtx.ctx, selfId) : "member";
453
453
  const botNickname =
454
- cfg.nicknames[0] || pluginCtx.ctx.pickBot(selfId)?.nickname || "Bot";
454
+ cfg.nicknames[0] || event?.bot?.nickname || "Bot";
455
455
 
456
456
  let groupName: string | undefined;
457
457
  let memberCount: number | undefined;
@@ -1,5 +1,5 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import type { MiokiContext } from "mioki";
2
+ import type { MiokuContext } from "mioku";
3
3
  import type { AudioServiceApi, SupportedLang } from "mioku-service-audio";
4
4
 
5
5
  function detectAudioLanguage(text: string): SupportedLang {
@@ -33,7 +33,7 @@ export async function synthesizeAudioSource(
33
33
  }
34
34
 
35
35
  export async function sendVoice(
36
- ctx: MiokiContext,
36
+ ctx: MiokuContext,
37
37
  e: any,
38
38
  filePath: string,
39
39
  logTag = "[audio]",
@@ -42,7 +42,7 @@ export async function sendVoice(
42
42
  ? filePath
43
43
  : `file://${filePath}`;
44
44
  try {
45
- await e.reply([ctx.segment.record(fileUrl)]);
45
+ await e.reply([ctx.segment.raw("record", { file: fileUrl })]);
46
46
  return;
47
47
  } catch (err: any) {
48
48
  const msg = String(err?.message ?? err);
@@ -59,6 +59,6 @@ export async function sendVoice(
59
59
  ctx.logger.info(
60
60
  `${logTag} file:// 发送失败 (${msg}),回退 base64 (${buf.length} bytes)`,
61
61
  );
62
- await e.reply([ctx.segment.record(b64)]);
62
+ await e.reply([ctx.segment.raw("record", { file: b64 })]);
63
63
  }
64
64
  }
@@ -1,4 +1,4 @@
1
- import { logger } from "mioki";
1
+ import { logger } from "mioku";
2
2
  import sharp from "sharp";
3
3
  import { QQ_IMAGE_FETCH_HEADERS } from "./image-compress";
4
4
 
@@ -57,7 +57,7 @@ export async function extractGifFrames(
57
57
  })
58
58
  .png()
59
59
  .toBuffer();
60
- frames.push(`data:image/png;base64,${firstFrame.toString("base64")}`);
60
+ frames.push(`data:image/png);base64,${firstFrame.toString("base64")}`);
61
61
 
62
62
  // 提取中间帧
63
63
  if (middleIndex !== firstIndex && middleIndex !== lastIndex) {
@@ -67,7 +67,7 @@ export async function extractGifFrames(
67
67
  })
68
68
  .png()
69
69
  .toBuffer();
70
- frames.push(`data:image/png;base64,${middleFrame.toString("base64")}`);
70
+ frames.push(`data:image/png);base64,${middleFrame.toString("base64")}`);
71
71
  }
72
72
 
73
73
  // 提取末尾帧
@@ -78,7 +78,7 @@ export async function extractGifFrames(
78
78
  })
79
79
  .png()
80
80
  .toBuffer();
81
- frames.push(`data:image/png;base64,${lastFrame.toString("base64")}`);
81
+ frames.push(`data:image/png);base64,${lastFrame.toString("base64")}`);
82
82
  }
83
83
 
84
84
  logger.info(
@@ -4,7 +4,7 @@ import * as fs from "fs/promises";
4
4
  import * as os from "os";
5
5
  import * as path from "path";
6
6
  import { promisify } from "util";
7
- import { logger } from "mioki";
7
+ import { Bot, ForwardNode, logger } from "mioku";
8
8
  import type { AIInstance } from "mioku";
9
9
  import type { MediaSummaryKind, MediaSummaryRecord } from "../../types";
10
10
 
@@ -69,9 +69,7 @@ export interface HistoryMediaProcessingOptions {
69
69
  warn(message: string): void;
70
70
  error(message: string): void;
71
71
  };
72
- bot?: {
73
- api<T = any>(action: string, params?: Record<string, any>): Promise<T>;
74
- };
72
+ bot?: Bot;
75
73
  groupId?: number;
76
74
  runAIRequest?<T>(request: () => Promise<T>): Promise<T | null>;
77
75
  }
@@ -82,7 +80,7 @@ export interface GroupNoticeHistorySummary {
82
80
  userRole: string;
83
81
  content: string;
84
82
  timestamp: number;
85
- messageId: number;
83
+ messageId: string;
86
84
  }
87
85
 
88
86
  function runHistoryMediaAIRequest<T>(
@@ -252,7 +250,7 @@ export async function summarizeHistoryForward(
252
250
  return "[forward]";
253
251
  }
254
252
 
255
- const forwardResult = await options.bot.api("get_forward_msg", { id });
253
+ const forwardResult = await options.bot.getForwardMessage(id);
256
254
  const text = extractForwardText(forwardResult);
257
255
  const source = `forward:${id}`;
258
256
  if (!text) {
@@ -293,7 +291,7 @@ export async function getCachedHistoryForwardTag(
293
291
  if (!id) return "[forward]";
294
292
  if (!options.bot) return "[forward]";
295
293
  try {
296
- const forwardResult = await options.bot.api("get_forward_msg", { id });
294
+ const forwardResult = await options.bot.getForwardMessage(id);
297
295
  const text = extractForwardText(forwardResult);
298
296
  const summary = getCachedSummaryByHash(
299
297
  "forward",
@@ -395,7 +393,7 @@ export async function summarizeGroupNotice(
395
393
  userRole: "member",
396
394
  content,
397
395
  timestamp: normalizeNoticeTimestamp(notice),
398
- messageId: 0,
396
+ messageId: String((notice as { message_id?: unknown }).message_id ?? ""),
399
397
  };
400
398
  }
401
399
 
@@ -558,7 +556,7 @@ async function extractVideoFrames(
558
556
  { timeout: 30_000 },
559
557
  );
560
558
  const buffer = await fs.readFile(outputPath);
561
- frames.push(`data:image/jpeg;base64,${buffer.toString("base64")}`);
559
+ frames.push(`data:image/jpeg);base64,${buffer.toString("base64")}`);
562
560
  }
563
561
 
564
562
  return frames;
@@ -645,16 +643,13 @@ function buildFrameTimestamps(duration: number, frameCount: number): number[] {
645
643
  });
646
644
  }
647
645
 
648
- function extractForwardText(result: any): string {
649
- const messages =
650
- result?.messages || result?.data?.messages || result?.data || [];
651
- const nodes = Array.isArray(messages) ? messages : [];
646
+ function extractForwardText(result: ForwardNode[]): string {
647
+ const nodes = Array.isArray(result) ? result : [];
652
648
  const lines: string[] = [];
653
649
 
654
650
  for (const node of nodes) {
655
- const senderName =
656
- node.sender?.nickname || node.sender?.card || node.nickname || "unknown";
657
- const content = extractMessageSegmentsText(node.message || node.content);
651
+ const senderName = node.nickname || String(node.user_id || "unknown");
652
+ const content = extractMessageSegmentsText(node.message);
658
653
  if (content) {
659
654
  lines.push(`${senderName}: ${content}`);
660
655
  }
@@ -1,6 +1,6 @@
1
1
  import * as crypto from "crypto";
2
2
  import type { AIInstance } from "mioku";
3
- import { logger } from "mioki";
3
+ import { logger } from "mioku";
4
4
  import type { ChatDatabase } from "../../db";
5
5
  import type { ImageRecord } from "../../types";
6
6
  import {
@@ -1,5 +1,5 @@
1
1
  import sharp from "sharp";
2
- import { logger } from "mioki";
2
+ import { logger } from "mioku";
3
3
 
4
4
  const IMAGE_MAX_BYTES = 1 * 1024 * 1024;
5
5
  const COMPRESS_MAX_WIDTH = 1280;
@@ -1,6 +1,28 @@
1
1
  export const MARKDOWN_OPEN_TAG = "<MARKDOWN>";
2
2
  export const MARKDOWN_CLOSE_TAG = "</MARKDOWN>";
3
3
 
4
+ const QUOTE_MARKER_ONLY = /^(?:\s*\[reply:[^\]\n]+\])+\s*$/i;
5
+
6
+ export function isQuoteMarkerOnlyUnit(unit: string): boolean {
7
+ return QUOTE_MARKER_ONLY.test(String(unit || "").trim());
8
+ }
9
+
10
+ export function mergeQuoteMarkerUnits(units: string[]): string[] {
11
+ const result: string[] = [];
12
+ let pending: string[] = [];
13
+
14
+ for (const unit of units) {
15
+ if (isQuoteMarkerOnlyUnit(unit)) {
16
+ pending.push(unit.trim());
17
+ continue;
18
+ }
19
+ result.push(pending.length > 0 ? [...pending, unit].join("\n") : unit);
20
+ pending = [];
21
+ }
22
+
23
+ return result;
24
+ }
25
+
4
26
  export function splitOutgoingUnits(text: string): string[] {
5
27
  const normalized = String(text || "").replace(/\r/g, "");
6
28
  const result: string[] = [];
@@ -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,
130
- messageId: number,
129
+ ctx: MiokuContext,
130
+ messageId: string | 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
  }
@@ -150,7 +153,7 @@ export async function getMediaByMessageId(
150
153
 
151
154
  return null;
152
155
  } catch (err) {
153
- logger.error(
156
+ ctx.logger.error(
154
157
  `[multimodal] Failed to get media from message ${messageId}: ${err}`,
155
158
  );
156
159
  return null;
@@ -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
  }
@@ -186,7 +191,7 @@ export async function getQuoteImageUrl(
186
191
  // 提取图片 URL
187
192
  return (imageSeg as any).url || (imageSeg as any).data?.url || null;
188
193
  } catch (err) {
189
- logger.error(`[multimodal] Failed to get quote image URL: ${err}`);
194
+ ctx.logger.error(`[multimodal] Failed to get quote image URL: ${err}`);
190
195
  return null;
191
196
  }
192
197
  }
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,
@@ -45,7 +45,7 @@ export interface DynamicPromptContext {
45
45
  reviewMessages?: {
46
46
  contents: string[];
47
47
  userNames: string[];
48
- messageIds: number[];
48
+ messageIds: string[];
49
49
  };
50
50
  currentEmotion?: string;
51
51
  expressionContext?: string;
@@ -276,7 +276,7 @@ function buildResponseFormatSection(
276
276
  - **SPECIAL ACTIONS in your text (auto-parsed and removed from message):**
277
277
  - Use [at:123456] in your text to @ someone (123456 is the QQ number)
278
278
  - Use [poke:123456] in your text to poke someone. IMPORTANT: when you plan to poke a user, DON't describe your poke actions.
279
- - Use [reply:123456] at the START of a line to quote-reply that message (123456 is message_id)
279
+ - Use [reply:message_id] at the START of a line to quote-reply that message (copy the message_id EXACTLY as shown after # in the chat history; it may contain letters, e.g. [reply:P2rW...pMB])
280
280
  - **You can use MULTIPLE [reply:xxx] markers in different lines to quote multiple messages!**`);
281
281
 
282
282
  if (ctx.config.audio?.enabled) {
@@ -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
 
@@ -579,13 +579,15 @@ function buildChatHistorySection(ctx: DynamicPromptContext): string {
579
579
  for (const msg of chatHistory) {
580
580
  const time = new Date(msg.timestamp);
581
581
  const timeStr = `${String(time.getMonth() + 1).padStart(2, "0")}-${String(time.getDate()).padStart(2, "0")} ${String(time.getHours()).padStart(2, "0")}:${String(time.getMinutes()).padStart(2, "0")}`;
582
+ const msgIdStr = msg.messageId ? `#${msg.messageId}` : "";
582
583
 
583
584
  if (msg.role === "assistant") {
585
+ const contentStr = msgIdStr ? `${msg.content} ${msgIdStr}` : msg.content;
584
586
  if (currentAssistantBlock && currentAssistantBlock.timeStr === timeStr) {
585
- currentAssistantBlock.contents.push(msg.content);
587
+ currentAssistantBlock.contents.push(contentStr);
586
588
  } else {
587
589
  flushAssistant();
588
- currentAssistantBlock = { timeStr, contents: [msg.content] };
590
+ currentAssistantBlock = { timeStr, contents: [contentStr] };
589
591
  }
590
592
  continue;
591
593
  }
@@ -600,16 +602,16 @@ function buildChatHistorySection(ctx: DynamicPromptContext): string {
600
602
  : "Member";
601
603
  const titleStr = msg.userTitle ? `, ${msg.userTitle}` : "";
602
604
  const qqStr = msg.userId ? `${msg.userId}` : "";
603
- const msgIdStr = msg.messageId ? ` #${msg.messageId}` : "";
604
605
  mergedLines.push(
605
- `[${timeStr}] ${name}(${qqStr}, ${roleLabel}${titleStr})${msgIdStr}): ${msg.content}`,
606
+ `[${timeStr}] ${name}(${qqStr}, ${roleLabel}${titleStr})${msgIdStr ? ` ${msgIdStr}` : ""}: ${msg.content}`,
606
607
  );
607
608
  }
608
609
  flushAssistant();
609
610
 
610
611
  return `## Recent Context
611
612
  ${mergedLines.join("\n")}
612
- -- DON'T repeat yourself or bring up old topics - focus on what's being said right now. --`;
613
+ -- DON'T repeat yourself or bring up old topics - focus on what's being said right now. --
614
+ -- Use [reply:message_id] with the #id shown after a message to quote-reply it. --`;
613
615
  }
614
616
 
615
617
  function buildTargetMessageSection(