mioku-plugin-chat 2.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.
@@ -0,0 +1,180 @@
1
+ import type { AIInstance } from "mioku";
2
+ import type { MiokiContext } from "mioki";
3
+ import { logger } from "mioki";
4
+
5
+ /**
6
+ * 多模态内容管理器
7
+ * 负责处理图片、视频等多模态内容的描述和分析
8
+ */
9
+
10
+ /**
11
+ * 图片描述结果
12
+ */
13
+ export interface ImageDescriptionResult {
14
+ success: boolean;
15
+ description?: string;
16
+ error?: string;
17
+ }
18
+
19
+ /**
20
+ * 使用多模态工作模型描述图片
21
+ * @param ai AI 实例
22
+ * @param imageUrl 图片 URL
23
+ * @param model 多模态工作模型名称
24
+ * @param context 可选的上下文信息(例如用户的问题)
25
+ * @returns 图片描述结果
26
+ */
27
+ export async function describeImage(
28
+ ai: AIInstance,
29
+ imageUrl: string,
30
+ model: string,
31
+ context?: string,
32
+ ): Promise<ImageDescriptionResult> {
33
+ try {
34
+ // 检查是否为 GIF,如果是则提取三帧
35
+ const { isGifUrl, extractGifFrames } =
36
+ await import("./media/gif-extractor");
37
+ let imageUrls: string[] = [imageUrl];
38
+
39
+ if (await isGifUrl(imageUrl)) {
40
+ const result = await extractGifFrames(imageUrl);
41
+ if (result && result.frames.length > 0) {
42
+ imageUrls = result.frames;
43
+ } else {
44
+ logger.warn(
45
+ `[multimodal] Failed to extract GIF frames, using original URL`,
46
+ );
47
+ }
48
+ }
49
+
50
+ // 构建提示词
51
+ const systemPrompt = `You are an image analysis assistant. Your task is to provide a detailed, accurate description of the image.
52
+
53
+ Instructions:
54
+ - Describe what you see in the image clearly and objectively
55
+ - Include important details like objects, people, text, colors, composition, and atmosphere
56
+ - If there is text in the image, transcribe it accurately
57
+ - Keep your description concise but comprehensive (2-4 sentences)
58
+ - Use natural language that can be easily understood
59
+ ${imageUrls.length > 1 ? "\n- Note: You are viewing multiple frames from an animated image (GIF). Describe the overall motion and changes across frames." : ""}
60
+ ${context ? `\nUser context: ${context}` : ""}`;
61
+
62
+ const userPrompt = context
63
+ ? `Please describe this image in detail, paying special attention to anything relevant to: "${context}"`
64
+ : imageUrls.length > 1
65
+ ? `Please describe these ${imageUrls.length} frames from an animated image in detail.`
66
+ : "Please describe this image in detail.";
67
+
68
+ // 构建消息内容
69
+ const contentParts: any[] = [{ type: "text", text: userPrompt }];
70
+ for (const url of imageUrls) {
71
+ contentParts.push({
72
+ type: "image_url",
73
+ image_url: { url, detail: "auto" },
74
+ });
75
+ }
76
+
77
+ // 调用多模态模型
78
+ const response = await ai.complete({
79
+ model,
80
+ messages: [
81
+ { role: "system", content: systemPrompt },
82
+ {
83
+ role: "user",
84
+ content: contentParts,
85
+ },
86
+ ],
87
+ temperature: 0.3, // 使用较低的温度以获得更准确的描述
88
+ });
89
+
90
+ if (!response.content) {
91
+ logger.warn("[multimodal] No description returned from model");
92
+ return {
93
+ success: false,
94
+ error: "Model returned empty response",
95
+ };
96
+ }
97
+
98
+ logger.info(`[multimodal] image: ${response.content}`);
99
+
100
+ return {
101
+ success: true,
102
+ description: response.content,
103
+ };
104
+ } catch (err) {
105
+ logger.error(`[multimodal] Failed to describe image: ${err}`);
106
+ return {
107
+ success: false,
108
+ error: String(err),
109
+ };
110
+ }
111
+ }
112
+
113
+ /**
114
+ * 获取指定消息 ID 中的图片 URL
115
+ * @param ctx Mioki 上下文
116
+ * @param messageId 消息 ID
117
+ * @param e
118
+ * @returns 图片 URL 或 null
119
+ */
120
+ export async function getImageUrlByMessageId(
121
+ ctx: MiokiContext,
122
+ messageId: number,
123
+ e: any,
124
+ ): Promise<string | null> {
125
+ try {
126
+ // 通过 message_id 获取消息详情
127
+ const msg = await ctx.pickBot(e.self_id).getMsg(messageId);
128
+ if (!msg || !msg.message) {
129
+ return null;
130
+ }
131
+
132
+ // 查找图片段
133
+ const imageSeg = msg.message.find((s: any) => s.type === "image");
134
+ if (!imageSeg) {
135
+ return null;
136
+ }
137
+
138
+ // 提取图片 URL
139
+ return (imageSeg as any).url || (imageSeg as any).data?.url || null;
140
+ } catch (err) {
141
+ logger.error(
142
+ `[multimodal] Failed to get image URL from message ${messageId}: ${err}`,
143
+ );
144
+ return null;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * 获取引用消息中的图片 URL
150
+ * @param ctx Mioki 上下文
151
+ * @param event 消息事件
152
+ * @returns 图片 URL 或 null
153
+ */
154
+ export async function getQuoteImageUrl(
155
+ ctx: MiokiContext,
156
+ event: any,
157
+ ): Promise<string | null> {
158
+ if (!event.quote_id) {
159
+ return null;
160
+ }
161
+
162
+ try {
163
+ const quotedMsg = await ctx.getQuoteMsg(event);
164
+ if (!quotedMsg || !quotedMsg.message) {
165
+ return null;
166
+ }
167
+
168
+ // 查找图片段
169
+ const imageSeg = quotedMsg.message.find((s: any) => s.type === "image");
170
+ if (!imageSeg) {
171
+ return null;
172
+ }
173
+
174
+ // 提取图片 URL
175
+ return (imageSeg as any).url || (imageSeg as any).data?.url || null;
176
+ } catch (err) {
177
+ logger.error(`[multimodal] Failed to get quote image URL: ${err}`);
178
+ return null;
179
+ }
180
+ }