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 +5 -5
- package/core/base.ts +20 -20
- package/core/chat-engine.ts +14 -10
- package/core/chat-turn.ts +109 -37
- package/core/media/history-media.ts +14 -14
- package/core/media/image-compress.ts +38 -27
- package/core/media/segment.ts +2 -2
- package/core/prompt.ts +1 -1
- package/core/tools/info.ts +14 -7
- package/core/tools/web.ts +6 -9
- package/db.ts +13 -13
- package/handlers/idle-debug.ts +7 -7
- package/handlers/message.ts +6 -8
- package/handlers/poke.ts +15 -14
- package/humanize/emotion-agent.ts +5 -5
- package/humanize/expression.ts +6 -6
- package/humanize/index.ts +1 -1
- package/humanize/memory.ts +1 -1
- package/index.ts +52 -28
- package/manage/cooldown.ts +8 -8
- package/manage/group-structured-history.ts +2 -2
- package/manage/idle-check.ts +12 -11
- package/manage/queue-processor.ts +9 -8
- package/manage/rate-limit-guard.ts +2 -2
- package/manage/rate-limiter.ts +21 -21
- package/manage/session.ts +1 -1
- package/manage/types.ts +16 -16
- package/package.json +1 -1
- package/runtime/chat-runtime.ts +4 -4
- package/types.ts +11 -11
- package/utils/config.ts +4 -4
- package/utils/group-config.ts +3 -4
- package/utils/index.ts +2 -0
- package/utils/message.ts +112 -47
- package/utils/model.ts +31 -0
- package/utils/queue.ts +12 -11
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?:
|
|
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:
|
|
79
|
-
selfId:
|
|
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?:
|
|
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<
|
|
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:
|
|
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:
|
|
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:
|
|
256
|
-
userId:
|
|
255
|
+
groupId: string | undefined,
|
|
256
|
+
userId: string,
|
|
257
257
|
text: string,
|
|
258
258
|
config: ChatConfig,
|
|
259
|
-
selfId:
|
|
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:
|
|
559
|
-
userId:
|
|
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:
|
|
587
|
-
userId:
|
|
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:
|
|
671
|
+
groupId: string,
|
|
672
672
|
groupSessionId: string,
|
|
673
673
|
ctx: MiokuContext,
|
|
674
674
|
historyCount: number,
|
|
675
675
|
db: ChatDatabase,
|
|
676
|
-
selfId:
|
|
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:
|
|
714
|
-
selfId:
|
|
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?:
|
|
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:
|
|
778
|
+
selfId: string;
|
|
779
779
|
groupSessionId: string;
|
|
780
|
-
groupId?:
|
|
781
|
-
userId:
|
|
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:
|
|
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 ??
|
|
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:
|
|
907
|
+
groupId: string,
|
|
908
908
|
emojiPath: string | null | undefined,
|
|
909
909
|
bot: Bot | undefined,
|
|
910
910
|
): Promise<void> {
|
package/core/chat-engine.ts
CHANGED
|
@@ -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 =
|
|
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:
|
|
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
|
|
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("
|
|
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:
|
|
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
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
|
|
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 {
|
|
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?:
|
|
18
|
-
groupId?:
|
|
19
|
-
userId?:
|
|
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?:
|
|
32
|
-
userId:
|
|
33
|
-
selfId:
|
|
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:
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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 =
|
|
94
|
-
const userId =
|
|
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:
|
|
169
|
+
groupId: optionGroupId,
|
|
114
170
|
userId,
|
|
115
|
-
selfId
|
|
116
|
-
sessionId:
|
|
117
|
-
? `group:${
|
|
171
|
+
selfId,
|
|
172
|
+
sessionId: optionGroupId
|
|
173
|
+
? `group:${optionGroupId}`
|
|
118
174
|
: `personal:${userId}`,
|
|
119
175
|
personalSessionId:
|
|
120
|
-
|
|
121
|
-
senderName:
|
|
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?:
|
|
192
|
+
groupId?: string;
|
|
137
193
|
groupSessionId: string;
|
|
138
|
-
userId:
|
|
139
|
-
selfId:
|
|
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:
|
|
251
|
-
const userId:
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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?:
|
|
73
|
+
groupId?: string;
|
|
74
74
|
runAIRequest?<T>(request: () => Promise<T>): Promise<T | null>;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export interface GroupNoticeHistorySummary {
|
|
78
|
-
userId:
|
|
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
|
|
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:
|
|
704
|
+
userId: string;
|
|
705
705
|
userName: string;
|
|
706
706
|
} {
|
|
707
|
-
const userId =
|
|
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
|
-
);
|
|
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
|
|
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
|
-
|
|
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)
|
|
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,
|
|
48
|
-
return
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
}
|
package/core/media/segment.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function buildHistoryMediaProcessingOptions(
|
|
|
28
28
|
saveMediaSummary(summary: MediaSummaryRecord): void;
|
|
29
29
|
},
|
|
30
30
|
bot: Bot,
|
|
31
|
-
groupId:
|
|
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:
|
|
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
|
|
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!**`);
|