mioku-plugin-chat 3.0.6 → 3.1.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.
package/context.ts CHANGED
@@ -38,7 +38,7 @@ export interface ChatPluginContext {
38
38
  ctx: MiokuContext;
39
39
  defaultConfig: ChatConfig;
40
40
  configProvider: ChatConfigProvider;
41
- getConfig: (groupId?: number) => Promise<ChatConfig>;
41
+ getConfig: (groupId?: string) => Promise<ChatConfig>;
42
42
  db: ChatDatabase;
43
43
  aiInstance: AIInstance;
44
44
  workAIInstance: AIInstance;
@@ -75,8 +75,8 @@ export interface ChatPluginContext {
75
75
  runChat: RunChat;
76
76
  startCooldownTimer: (
77
77
  groupSessionId: string,
78
- groupId: number,
79
- selfId: number,
78
+ groupId: string,
79
+ selfId: string,
80
80
  ) => void;
81
81
  recordGroupMessageForLearning: (
82
82
  userMsg: ChatMessage,
@@ -91,12 +91,12 @@ export interface ChatRuntimeState {
91
91
  }
92
92
 
93
93
  export interface ChatHandlerState {
94
- getConfig: (groupId?: number) => Promise<ChatConfig>;
94
+ getConfig: (groupId?: string) => Promise<ChatConfig>;
95
95
  matchMessageCommands?: (
96
96
  text: string,
97
97
  ) => Array<{ plugin: string; command: string }>;
98
98
  runtimeState: ChatRuntimeState;
99
- pokeCooldowns: Map<number, number>;
99
+ pokeCooldowns: Map<string, number>;
100
100
  }
101
101
 
102
102
  export interface ChatRuntime {
package/core/base.ts CHANGED
@@ -25,7 +25,7 @@ import {
25
25
 
26
26
  export interface SendAIResponseOptions {
27
27
  ctx: MiokuContext;
28
- groupId: number;
28
+ groupId: string;
29
29
  messages: string[];
30
30
  config: ChatConfig;
31
31
  sentIndices?: Set<number>;
@@ -78,7 +78,7 @@ async function waitTypingDelay(
78
78
 
79
79
  export async function sendAIResponse(
80
80
  options: SendAIResponseOptions,
81
- selfId: number,
81
+ selfId: string,
82
82
  ): Promise<Array<string | undefined>> {
83
83
  const {
84
84
  ctx,
@@ -252,11 +252,11 @@ export async function sendAIResponse(
252
252
 
253
253
  export async function sendMessage(
254
254
  ctx: MiokuContext,
255
- groupId: number | undefined,
256
- userId: number,
255
+ groupId: string | undefined,
256
+ userId: string,
257
257
  text: string,
258
258
  config: ChatConfig,
259
- selfId: number,
259
+ selfId: string,
260
260
  audioService?: AudioServiceApi,
261
261
  ): Promise<void> {
262
262
  const typingDelayEnabled = config.enableTypingDelay ?? false;
@@ -555,8 +555,8 @@ async function buildMarkdownImage(
555
555
 
556
556
  async function dispatchSegments(
557
557
  bot: any,
558
- groupId: number | undefined,
559
- userId: number | undefined,
558
+ groupId: string | undefined,
559
+ userId: string | undefined,
560
560
  buildSegments: (imageSource?: string) => any[],
561
561
  fallbackImagePath?: string,
562
562
  ): Promise<string | undefined> {
@@ -583,8 +583,8 @@ async function dispatchSegments(
583
583
 
584
584
  async function sendByTarget(
585
585
  bot: Bot,
586
- groupId: number | undefined,
587
- userId: number | undefined,
586
+ groupId: string | undefined,
587
+ userId: string | undefined,
588
588
  segments: readonly any[],
589
589
  ): Promise<import("mioku").SentMessage | undefined> {
590
590
  if (groupId) {
@@ -668,12 +668,12 @@ export interface GroupHistoryResult {
668
668
  }
669
669
 
670
670
  export async function getGroupHistoryMessages(
671
- groupId: number,
671
+ groupId: string,
672
672
  groupSessionId: string,
673
673
  ctx: MiokuContext,
674
674
  historyCount: number,
675
675
  db: ChatDatabase,
676
- selfId: number,
676
+ selfId: string,
677
677
  mediaOptions?: {
678
678
  ai?: AIInstance;
679
679
  workingModel?: string;
@@ -710,8 +710,8 @@ export interface GroupInfoResult {
710
710
 
711
711
  export async function getGroupInfoData(
712
712
  ctx: MiokuContext,
713
- groupId: number,
714
- selfId: number,
713
+ groupId: string,
714
+ selfId: string,
715
715
  fallbackGroupName?: string,
716
716
  ): Promise<GroupInfoResult> {
717
717
  let groupName: string | undefined;
@@ -746,7 +746,7 @@ export async function getHumanizeContexts(
746
746
  groupSessionId: string,
747
747
  userName: string,
748
748
  history: ChatMessage[],
749
- triggerUserId?: number,
749
+ triggerUserId?: string,
750
750
  ): Promise<HumanizeContextsResult> {
751
751
  const historyStartAt = history.length > 0 ? history[0].timestamp : undefined;
752
752
 
@@ -775,10 +775,10 @@ export async function getHumanizeContexts(
775
775
  export interface BuildToolContextOptions {
776
776
  ctx: MiokuContext;
777
777
  event: any;
778
- selfId: number;
778
+ selfId: string;
779
779
  groupSessionId: string;
780
- groupId?: number;
781
- userId: number;
780
+ groupId?: string;
781
+ userId: string;
782
782
  config: ChatConfig;
783
783
  aiService: AIService;
784
784
  db: ChatDatabase;
@@ -867,7 +867,7 @@ export function buildToolContext(
867
867
  }
868
868
 
869
869
  export function saveBotMessages(
870
- groupId: number,
870
+ groupId: string,
871
871
  groupSessionId: string,
872
872
  messages: string[],
873
873
  timestamp: number,
@@ -891,7 +891,7 @@ export function saveBotMessages(
891
891
  sessionId: groupSessionId,
892
892
  role: "assistant",
893
893
  content: msg ?? "",
894
- userId: selfId ?? 0,
894
+ userId: String(selfId ?? ""),
895
895
  userName: botNickname ?? "Miku",
896
896
  userRole: "member",
897
897
  groupId,
@@ -904,7 +904,7 @@ export function saveBotMessages(
904
904
 
905
905
  export async function sendEmoji(
906
906
  ctx: MiokuContext,
907
- groupId: number,
907
+ groupId: string,
908
908
  emojiPath: string | null | undefined,
909
909
  bot: Bot | undefined,
910
910
  ): Promise<void> {
@@ -29,6 +29,7 @@ import {
29
29
  type StructuredUserInput,
30
30
  } from "../manage/group-structured-history";
31
31
  import { prepareImageUrlsForModel } from "./media/image-compress";
32
+ import { mainModelSupportsVision } from "../utils/model";
32
33
 
33
34
  interface StructuredHistoryRunContext {
34
35
  manager: GroupStructuredHistoryManager;
@@ -116,7 +117,10 @@ export async function runChat(
116
117
  const currentUserMessages = hasStructuredHistory
117
118
  ? buildStructuredUserMessages(structuredHistory!.currentUserInputs)
118
119
  : [];
119
- const directImageUrls = toolCtx.config.isMultimodal
120
+ const directImageUrls = mainModelSupportsVision(
121
+ toolCtx.aiService,
122
+ toolCtx.config.model,
123
+ )
120
124
  ? toolCtx.pendingImageUrls
121
125
  : undefined;
122
126
  const webSearchState = { count: 0 };
@@ -142,7 +146,7 @@ export async function runChat(
142
146
  const usageContext = {
143
147
  usageId,
144
148
  source: "chat",
145
- botId: getEventNumber(toolCtx.event, "self_id"),
149
+ botId: getEventId(toolCtx.event, "self_id"),
146
150
  groupId: toolCtx.groupId,
147
151
  groupName: getEventString(toolCtx.event, "group_name"),
148
152
  userId: toolCtx.userId,
@@ -762,7 +766,7 @@ function sanitizeBrackets(text: string): string {
762
766
  let working = text.replace(FUNCTIONAL_BRACKET_PREFIX, (match) => {
763
767
  const idx = placeholders.length;
764
768
  placeholders.push(match);
765
- return `${idx}`;
769
+ return `\u0000${idx}\u0000`;
766
770
  });
767
771
 
768
772
  let result = "";
@@ -772,8 +776,8 @@ function sanitizeBrackets(text: string): string {
772
776
  while (i < working.length) {
773
777
  const ch = working[i];
774
778
 
775
- if (ch === "") {
776
- const endIdx = working.indexOf("", i + 1);
779
+ if (ch === "\u0000") {
780
+ const endIdx = working.indexOf("\u0000", i + 1);
777
781
  if (endIdx > i) {
778
782
  const idx = parseInt(working.slice(i + 1, endIdx), 10);
779
783
  if (!Number.isNaN(idx) && placeholders[idx] !== undefined) {
@@ -960,7 +964,7 @@ ${failedSummary}
960
964
  ],
961
965
  usageContext: {
962
966
  source: "chat.tool-failure",
963
- botId: getEventNumber(toolCtx.event, "self_id"),
967
+ botId: getEventId(toolCtx.event, "self_id"),
964
968
  groupId: toolCtx.groupId,
965
969
  groupName: getEventString(toolCtx.event, "group_name"),
966
970
  userId: toolCtx.userId,
@@ -984,12 +988,12 @@ ${failedSummary}
984
988
  return "我刚刚查这条信息时出了点问题,你可以换个关键词再试试,或者给我更具体一点的线索。";
985
989
  }
986
990
 
987
- function getEventNumber(event: unknown, key: string): number | undefined {
991
+ function getEventId(event: unknown, key: string): string | undefined {
988
992
  if (!event || typeof event !== "object") return undefined;
989
993
  const value = (event as Record<string, unknown>)[key];
990
- return typeof value === "number" && Number.isFinite(value)
991
- ? value
992
- : undefined;
994
+ if (value == null) return undefined;
995
+ const text = String(value).trim();
996
+ return text.length > 0 ? text : undefined;
993
997
  }
994
998
 
995
999
  function getEventString(event: unknown, key: string): string | undefined {
package/core/chat-turn.ts CHANGED
@@ -2,7 +2,12 @@ import type { MiokuContext } from "mioku";
2
2
  import type { AITool, Bot, ChatRuntimePromptInjection } from "mioku";
3
3
  import type { ChatPluginContext, ChatRuntimeState } from "../context";
4
4
  import type { ChatConfig, ChatMessage, TargetMessage } from "../types";
5
- import { getGroupHistory, getBotRole, getQuotedContent } from "../utils";
5
+ import {
6
+ getGroupHistory,
7
+ getBotRole,
8
+ getQuotedContent,
9
+ mainModelSupportsVision,
10
+ } from "../utils";
6
11
  import { buildStructuredUserInputFromTarget } from "../manage/group-structured-history";
7
12
 
8
13
  export type RuntimeReplyContextType =
@@ -14,9 +19,9 @@ export type RuntimeReplyContextType =
14
19
 
15
20
  export interface ExecuteChatRuntimeRequestOptions {
16
21
  event?: any;
17
- selfId?: number;
18
- groupId?: number;
19
- userId?: number;
22
+ selfId?: string;
23
+ groupId?: string;
24
+ userId?: string;
20
25
  config: ChatConfig;
21
26
  targetMessageContent?: string;
22
27
  promptInjections?: ChatRuntimePromptInjection[];
@@ -28,9 +33,9 @@ export interface ExecuteChatRuntimeRequestOptions {
28
33
  interface ResolvedRuntimeContext {
29
34
  event: any;
30
35
  isGroup: boolean;
31
- groupId?: number;
32
- userId: number;
33
- selfId: number;
36
+ groupId?: string;
37
+ userId: string;
38
+ selfId: string;
34
39
  sessionId: string;
35
40
  personalSessionId?: string;
36
41
  senderName: string;
@@ -43,6 +48,53 @@ interface ResolvedRuntimeContext {
43
48
  const NO_NEW_MESSAGE =
44
49
  "[No new user message in this turn. Reply naturally based on the runtime instruction and recent context.]";
45
50
 
51
+ /**
52
+ * 主模型不支持视觉时,引用消息里的图片交给视觉工作模型描述(结果按内容哈希入库复用)。
53
+ * 主模型支持视觉时返回 undefined:图片会直接附到请求里,不必重复描述。
54
+ */
55
+ async function describeQuotedImage(
56
+ pluginCtx: ChatPluginContext,
57
+ cfg: ChatConfig,
58
+ imageUrl: string | undefined,
59
+ userId: string,
60
+ groupId: string | undefined,
61
+ ): Promise<string | undefined> {
62
+ if (
63
+ !imageUrl ||
64
+ mainModelSupportsVision(pluginCtx.aiService, cfg.model)
65
+ ) {
66
+ return undefined;
67
+ }
68
+ const visionAI =
69
+ pluginCtx.visionAIInstance ?? pluginCtx.aiService.getDefault();
70
+ if (!visionAI) return undefined;
71
+
72
+ try {
73
+ const { processImage } = await import("./media/image-analyzer");
74
+ const record = await processImage(
75
+ visionAI,
76
+ imageUrl,
77
+ cfg.multimodalWorkingModel,
78
+ pluginCtx.db,
79
+ {
80
+ runAIRequest: (request) =>
81
+ pluginCtx.runWithRateLimitGuard(request, {
82
+ userId,
83
+ groupId,
84
+ label: "quoted-image",
85
+ skipRetryOnRateLimit: true,
86
+ }),
87
+ },
88
+ );
89
+ return record?.description?.trim() || undefined;
90
+ } catch (err) {
91
+ pluginCtx.ctx.logger.error(
92
+ `[chat-turn] Failed to describe quoted image: ${err}`,
93
+ );
94
+ return undefined;
95
+ }
96
+ }
97
+
46
98
  function buildRuntimeTargetMessageContent(
47
99
  ctx: MiokuContext,
48
100
  event: any,
@@ -61,8 +113,12 @@ function resolveRuntimeContext(
61
113
  if (options.event) {
62
114
  const event = options.event;
63
115
  const isGroup = event.message_type === "group";
64
- const groupId: number | undefined = isGroup ? event.group_id : undefined;
65
- const userId: number = event.user_id || event.sender?.user_id || 0;
116
+ const groupId: string | undefined = isGroup
117
+ ? String(event.group_id ?? "").trim() || undefined
118
+ : undefined;
119
+ const userId: string = String(
120
+ event.user_id ?? event.sender?.user_id ?? "",
121
+ ).trim();
66
122
  return {
67
123
  event,
68
124
  isGroup,
@@ -80,18 +136,18 @@ function resolveRuntimeContext(
80
136
  };
81
137
  }
82
138
 
83
- if (typeof options.selfId !== "number") {
139
+ const selfId = String(options.selfId ?? "").trim();
140
+ if (!selfId) {
84
141
  throw new Error("Chat runtime requires either event or selfId");
85
142
  }
86
- if (
87
- typeof options.groupId !== "number" &&
88
- typeof options.userId !== "number"
89
- ) {
143
+ const optionGroupId = String(options.groupId ?? "").trim() || undefined;
144
+ const optionUserId = String(options.userId ?? "").trim();
145
+ if (!optionGroupId && !optionUserId) {
90
146
  throw new Error("Chat runtime requires groupId or userId");
91
147
  }
92
148
 
93
- const isGroup = typeof options.groupId === "number";
94
- const userId = options.userId ?? 0;
149
+ const isGroup = Boolean(optionGroupId);
150
+ const userId = optionUserId;
95
151
  const event = {
96
152
  self_id: options.selfId,
97
153
  message_type: isGroup ? "group" : "private",
@@ -110,15 +166,15 @@ function resolveRuntimeContext(
110
166
  return {
111
167
  event,
112
168
  isGroup,
113
- groupId: options.groupId,
169
+ groupId: optionGroupId,
114
170
  userId,
115
- selfId: options.selfId,
116
- sessionId: options.groupId
117
- ? `group:${options.groupId}`
171
+ selfId,
172
+ sessionId: optionGroupId
173
+ ? `group:${optionGroupId}`
118
174
  : `personal:${userId}`,
119
175
  personalSessionId:
120
- options.groupId && userId ? `personal:${userId}` : undefined,
121
- senderName: options.groupId ? "system" : String(userId),
176
+ optionGroupId && userId ? `personal:${userId}` : undefined,
177
+ senderName: optionGroupId ? "system" : String(userId),
122
178
  userRole: "member",
123
179
  userTitle: undefined,
124
180
  groupName: undefined,
@@ -133,10 +189,10 @@ export async function finalizeChatTurn(
133
189
  event: any;
134
190
  cfg: ChatConfig;
135
191
  result: { messages: string[]; emojiPath?: string | null };
136
- groupId?: number;
192
+ groupId?: string;
137
193
  groupSessionId: string;
138
- userId: number;
139
- selfId: number;
194
+ userId: string;
195
+ selfId: string;
140
196
  toolCtx: { sentMessageIndices?: Set<number> };
141
197
  send: boolean;
142
198
  isLive: boolean;
@@ -247,8 +303,8 @@ export async function processChat(
247
303
  ): Promise<void> {
248
304
  const { ctx } = pluginCtx;
249
305
  const isGroup = e.message_type === "group";
250
- const groupId: number | undefined = isGroup ? e.group_id : undefined;
251
- const userId: number = e.user_id || e.sender?.user_id;
306
+ const groupId: string | undefined = isGroup ? e.group_id : undefined;
307
+ const userId: string = e.user_id || e.sender?.user_id;
252
308
  const selfId = options.replyBot ? Number(options.replyBot.bot_id) : e.self_id;
253
309
  const cfg = await pluginCtx.getConfig(groupId);
254
310
 
@@ -285,16 +341,14 @@ export async function processChat(
285
341
  }
286
342
  if (quotedInfo?.imageUrl) imageUrls.push(quotedInfo.imageUrl);
287
343
 
288
- let messageContent = ctx.text(e) || "";
289
- let extraContext = "";
290
- if (quotedInfo) {
291
- const parts = [
292
- `[Quoted message #${quotedInfo.messageId} from ${quotedInfo.senderName}: ${quotedInfo.content}]`,
293
- ];
294
- if (quotedInfo.imageUrl) parts.push("[Quoted message contains an image]");
295
- extraContext = parts.join(" ");
296
- }
297
- if (extraContext) messageContent = extraContext + " " + messageContent;
344
+ // 与历史拉取并行,省掉视觉描述带来的额外等待
345
+ const quotedImageNotePromise = describeQuotedImage(
346
+ pluginCtx,
347
+ cfg,
348
+ quotedInfo?.imageUrl,
349
+ userId,
350
+ groupId,
351
+ );
298
352
 
299
353
  const rawHistory = groupId
300
354
  ? await getGroupHistory(
@@ -346,6 +400,24 @@ export async function processChat(
346
400
  userId,
347
401
  );
348
402
 
403
+ const quotedImageNote = await quotedImageNotePromise;
404
+ let messageContent = ctx.text(e) || "";
405
+ if (quotedInfo) {
406
+ const quotedText =
407
+ quotedInfo.content || (quotedInfo.imageUrl ? "[image]" : "");
408
+ const parts = [
409
+ `[Quoted message #${quotedInfo.messageId} from ${quotedInfo.senderName}: ${quotedText}]`,
410
+ ];
411
+ if (quotedInfo.imageUrl) {
412
+ parts.push(
413
+ quotedImageNote
414
+ ? `[Quoted message image: ${quotedImageNote}]`
415
+ : "[Quoted message contains an image]",
416
+ );
417
+ }
418
+ messageContent = `${parts.join(" ")} ${messageContent}`;
419
+ }
420
+
349
421
  const targetMessage: TargetMessage = {
350
422
  userName: senderName,
351
423
  userId,
@@ -70,12 +70,12 @@ export interface HistoryMediaProcessingOptions {
70
70
  error(message: string): void;
71
71
  };
72
72
  bot?: Bot;
73
- groupId?: number;
73
+ groupId?: string;
74
74
  runAIRequest?<T>(request: () => Promise<T>): Promise<T | null>;
75
75
  }
76
76
 
77
77
  export interface GroupNoticeHistorySummary {
78
- userId: number;
78
+ userId: string;
79
79
  userName: string;
80
80
  userRole: string;
81
81
  content: string;
@@ -556,7 +556,7 @@ async function extractVideoFrames(
556
556
  { timeout: 30_000 },
557
557
  );
558
558
  const buffer = await fs.readFile(outputPath);
559
- frames.push(`data:image/jpeg);base64,${buffer.toString("base64")}`);
559
+ frames.push(`data:image/jpeg;base64,${buffer.toString("base64")}`);
560
560
  }
561
561
 
562
562
  return frames;
@@ -701,18 +701,18 @@ function extractNoticeText(notice: any): string {
701
701
  }
702
702
 
703
703
  function extractNoticeSender(notice: any): {
704
- userId: number;
704
+ userId: string;
705
705
  userName: string;
706
706
  } {
707
- const userId = Number(
708
- notice?.user_id ||
709
- notice?.sender_id ||
710
- notice?.operator_id ||
711
- notice?.poster_id ||
712
- notice?.publisher_id ||
713
- notice?.sender?.user_id ||
714
- 0,
715
- );
707
+ const userId = String(
708
+ notice?.user_id ??
709
+ notice?.sender_id ??
710
+ notice?.operator_id ??
711
+ notice?.poster_id ??
712
+ notice?.publisher_id ??
713
+ notice?.sender?.user_id ??
714
+ "",
715
+ ).trim();
716
716
  const userName =
717
717
  notice?.sender?.card ||
718
718
  notice?.sender?.nickname ||
@@ -722,7 +722,7 @@ function extractNoticeSender(notice: any): {
722
722
  notice?.operator_name ||
723
723
  (userId ? String(userId) : "unknown");
724
724
  return {
725
- userId: Number.isFinite(userId) ? userId : 0,
725
+ userId,
726
726
  userName: String(userName),
727
727
  };
728
728
  }
@@ -5,6 +5,13 @@ const IMAGE_MAX_BYTES = 1 * 1024 * 1024;
5
5
  const COMPRESS_MAX_WIDTH = 1280;
6
6
  const COMPRESS_JPEG_QUALITY = 80;
7
7
 
8
+ const INLINE_MIME_TYPES = new Set([
9
+ "image/jpeg",
10
+ "image/png",
11
+ "image/webp",
12
+ "image/gif",
13
+ ]);
14
+
8
15
  export const QQ_IMAGE_FETCH_HEADERS = {
9
16
  "User-Agent":
10
17
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
@@ -13,26 +20,24 @@ export const QQ_IMAGE_FETCH_HEADERS = {
13
20
 
14
21
  const FETCH_HEADERS = QQ_IMAGE_FETCH_HEADERS;
15
22
 
16
- /**
17
- * 准备发给模型的图片 URL:体积超过 1MB 时压缩为 JPEG data URL,否则原样返回。
18
- * data URL(如 GIF 抽帧结果)与非 http(s) 链接直接放行。
19
- */
20
23
  export async function prepareImageUrlForModel(url: string): Promise<string> {
21
24
  if (url.startsWith("data:")) return url;
22
25
  if (!/^https?:\/\//i.test(url)) return url;
23
26
 
24
- const size = await probeImageSize(url);
25
- if (size !== null && size <= IMAGE_MAX_BYTES) return url;
26
-
27
27
  let buffer: Buffer;
28
+ let mimeType: string;
28
29
  try {
29
- buffer = await downloadImageBuffer(url);
30
+ const downloaded = await downloadImage(url);
31
+ buffer = downloaded.buffer;
32
+ mimeType = downloaded.mimeType;
30
33
  } catch (err) {
31
34
  logger.warn(`[image-compress] download failed, using original: ${err}`);
32
35
  return url;
33
36
  }
34
37
 
35
- if (buffer.length <= IMAGE_MAX_BYTES) return url;
38
+ if (buffer.length <= IMAGE_MAX_BYTES && INLINE_MIME_TYPES.has(mimeType)) {
39
+ return `data:${mimeType};base64,${buffer.toString("base64")}`;
40
+ }
36
41
 
37
42
  try {
38
43
  const compressed = await sharp(buffer)
@@ -44,8 +49,8 @@ export async function prepareImageUrlForModel(url: string): Promise<string> {
44
49
  );
45
50
  return `data:image/jpeg;base64,${compressed.toString("base64")}`;
46
51
  } catch (err) {
47
- logger.warn(`[image-compress] compress failed, using original: ${err}`);
48
- return url;
52
+ logger.warn(`[image-compress] compress failed, inlining original: ${err}`);
53
+ return `data:${mimeType};base64,${buffer.toString("base64")}`;
49
54
  }
50
55
  }
51
56
 
@@ -55,23 +60,29 @@ export async function prepareImageUrlsForModel(
55
60
  return Promise.all(urls.map(prepareImageUrlForModel));
56
61
  }
57
62
 
58
- async function probeImageSize(url: string): Promise<number | null> {
59
- try {
60
- const head = await fetch(url, { method: "HEAD", headers: FETCH_HEADERS });
61
- if (!head.ok) return null;
62
- const len = head.headers.get("content-length");
63
- if (!len) return null;
64
- const n = Number(len);
65
- return Number.isFinite(n) ? n : null;
66
- } catch {
67
- return null;
68
- }
69
- }
70
-
71
- async function downloadImageBuffer(url: string): Promise<Buffer> {
63
+ async function downloadImage(
64
+ url: string,
65
+ ): Promise<{ buffer: Buffer; mimeType: string }> {
72
66
  const response = await fetch(url, { headers: FETCH_HEADERS });
73
67
  if (!response.ok) {
74
- throw new Error(`download failed: ${response.status} ${response.statusText}`);
68
+ throw new Error(
69
+ `download failed: ${response.status} ${response.statusText}`,
70
+ );
71
+ }
72
+
73
+ const mimeType = normalizeImageMimeType(
74
+ response.headers.get("content-type") || "",
75
+ );
76
+ if (!mimeType) {
77
+ throw new Error(
78
+ `not an image response: ${response.headers.get("content-type") || "unknown"}`,
79
+ );
75
80
  }
76
- return Buffer.from(await response.arrayBuffer());
81
+
82
+ return { buffer: Buffer.from(await response.arrayBuffer()), mimeType };
83
+ }
84
+
85
+ function normalizeImageMimeType(contentType: string): string | null {
86
+ const mimeType = contentType.split(";")[0]?.trim().toLowerCase() || "";
87
+ return mimeType.startsWith("image/") ? mimeType : null;
77
88
  }
@@ -28,7 +28,7 @@ export function buildHistoryMediaProcessingOptions(
28
28
  saveMediaSummary(summary: MediaSummaryRecord): void;
29
29
  },
30
30
  bot: Bot,
31
- groupId: number,
31
+ groupId: string,
32
32
  log: HistoryMediaProcessingOptions["logger"],
33
33
  runAIRequest?: <T>(request: () => Promise<T>) => Promise<T | null>,
34
34
  ): HistoryMediaProcessingOptions {
@@ -81,7 +81,7 @@ export function getCardData(seg: unknown): string | null {
81
81
 
82
82
  export function isMediaAnalysisBlocked(
83
83
  config: ChatConfig,
84
- userId: number,
84
+ userId: string,
85
85
  ): boolean {
86
86
  return Boolean(config.mediaAnalysisBlacklistUsers?.includes(userId));
87
87
  }
package/core/prompt.ts CHANGED
@@ -274,7 +274,7 @@ function buildResponseFormatSection(
274
274
  - For action markers like [] , put them on their own line when they are meant to be a separate action.
275
275
 
276
276
  - **SPECIAL ACTIONS in your text (auto-parsed and removed from message):**
277
- - Use [at:123456] in your text to @ someone (123456 is the QQ number)
277
+ - Use [at:<user_id>] in your text to @ someone (<user_id> is the platform user id shown in history)
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
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!**`);