@lobehub/chat 1.128.0 → 1.128.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.
Files changed (52) hide show
  1. package/.github/workflows/test.yml +8 -1
  2. package/CHANGELOG.md +25 -0
  3. package/changelog/v1.json +9 -0
  4. package/next.config.ts +8 -1
  5. package/package.json +71 -69
  6. package/packages/context-engine/ARCHITECTURE.md +425 -0
  7. package/packages/context-engine/package.json +40 -0
  8. package/packages/context-engine/src/base/BaseProcessor.ts +87 -0
  9. package/packages/context-engine/src/base/BaseProvider.ts +22 -0
  10. package/packages/context-engine/src/index.ts +32 -0
  11. package/packages/context-engine/src/pipeline.ts +219 -0
  12. package/packages/context-engine/src/processors/HistoryTruncate.ts +76 -0
  13. package/packages/context-engine/src/processors/InputTemplate.ts +83 -0
  14. package/packages/context-engine/src/processors/MessageCleanup.ts +87 -0
  15. package/packages/context-engine/src/processors/MessageContent.ts +298 -0
  16. package/packages/context-engine/src/processors/PlaceholderVariables.ts +196 -0
  17. package/packages/context-engine/src/processors/ToolCall.ts +186 -0
  18. package/packages/context-engine/src/processors/ToolMessageReorder.ts +113 -0
  19. package/packages/context-engine/src/processors/__tests__/HistoryTruncate.test.ts +175 -0
  20. package/packages/context-engine/src/processors/__tests__/InputTemplate.test.ts +243 -0
  21. package/packages/context-engine/src/processors/__tests__/MessageContent.test.ts +394 -0
  22. package/packages/context-engine/src/processors/__tests__/PlaceholderVariables.test.ts +334 -0
  23. package/packages/context-engine/src/processors/__tests__/ToolMessageReorder.test.ts +186 -0
  24. package/packages/context-engine/src/processors/index.ts +15 -0
  25. package/packages/context-engine/src/providers/HistorySummary.ts +102 -0
  26. package/packages/context-engine/src/providers/InboxGuide.ts +102 -0
  27. package/packages/context-engine/src/providers/SystemRoleInjector.ts +64 -0
  28. package/packages/context-engine/src/providers/ToolSystemRole.ts +118 -0
  29. package/packages/context-engine/src/providers/__tests__/HistorySummaryProvider.test.ts +112 -0
  30. package/packages/context-engine/src/providers/__tests__/InboxGuideProvider.test.ts +121 -0
  31. package/packages/context-engine/src/providers/__tests__/SystemRoleInjector.test.ts +200 -0
  32. package/packages/context-engine/src/providers/__tests__/ToolSystemRoleProvider.test.ts +140 -0
  33. package/packages/context-engine/src/providers/index.ts +11 -0
  34. package/packages/context-engine/src/types.ts +201 -0
  35. package/packages/context-engine/vitest.config.mts +10 -0
  36. package/packages/database/package.json +1 -1
  37. package/packages/prompts/src/prompts/systemRole/index.ts +1 -1
  38. package/packages/utils/src/index.ts +2 -0
  39. package/packages/utils/src/uriParser.test.ts +29 -0
  40. package/packages/utils/src/uriParser.ts +24 -0
  41. package/src/services/{__tests__ → chat}/chat.test.ts +22 -1032
  42. package/src/services/chat/clientModelRuntime.test.ts +385 -0
  43. package/src/services/chat/clientModelRuntime.ts +34 -0
  44. package/src/services/chat/contextEngineering.test.ts +848 -0
  45. package/src/services/chat/contextEngineering.ts +123 -0
  46. package/src/services/chat/helper.ts +61 -0
  47. package/src/services/{chat.ts → chat/index.ts} +24 -366
  48. package/src/services/chat/types.ts +9 -0
  49. package/src/services/models.ts +1 -1
  50. package/src/store/aiInfra/slices/aiModel/selectors.ts +2 -2
  51. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +1 -40
  52. /package/src/services/{__tests__ → chat}/__snapshots__/chat.test.ts.snap +0 -0
@@ -0,0 +1,298 @@
1
+ import { filesPrompts } from '@lobechat/prompts';
2
+ import { imageUrlToBase64, isLocalUrl, parseDataUri } from '@lobechat/utils';
3
+ import debug from 'debug';
4
+
5
+ import { BaseProcessor } from '../base/BaseProcessor';
6
+ import type { PipelineContext, ProcessorOptions } from '../types';
7
+
8
+ const log = debug('context-engine:processor:MessageContentProcessor');
9
+
10
+ export interface FileContextConfig {
11
+ /** Whether to enable file context injection */
12
+ enabled?: boolean;
13
+ /** Whether to include file URLs in file context prompts */
14
+ includeFileUrl?: boolean;
15
+ }
16
+
17
+ export interface MessageContentConfig {
18
+ /** File context configuration */
19
+ fileContext?: FileContextConfig;
20
+ /** Function to check if vision is supported */
21
+ isCanUseVision?: (model: string, provider: string) => boolean | undefined;
22
+ /** Model name */
23
+ model: string;
24
+ /** Provider name */
25
+ provider: string;
26
+ }
27
+
28
+ export interface UserMessageContentPart {
29
+ image_url?: {
30
+ detail?: string;
31
+ url: string;
32
+ };
33
+ signature?: string;
34
+ text?: string;
35
+ thinking?: string;
36
+ type: 'text' | 'image_url' | 'thinking';
37
+ }
38
+
39
+ /**
40
+ * Message Content Processor
41
+ * Responsible for handling content format conversion of user and assistant messages
42
+ */
43
+ export class MessageContentProcessor extends BaseProcessor {
44
+ readonly name = 'MessageContentProcessor';
45
+
46
+ constructor(
47
+ private config: MessageContentConfig,
48
+ options: ProcessorOptions = {},
49
+ ) {
50
+ super(options);
51
+ }
52
+
53
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
54
+ const clonedContext = this.cloneContext(context);
55
+
56
+ let processedCount = 0;
57
+ let userMessagesProcessed = 0;
58
+ let assistantMessagesProcessed = 0;
59
+
60
+ // 处理每条消息的内容
61
+ for (let i = 0; i < clonedContext.messages.length; i++) {
62
+ const message = clonedContext.messages[i];
63
+
64
+ try {
65
+ let updatedMessage = message;
66
+
67
+ if (message.role === 'user') {
68
+ updatedMessage = await this.processUserMessage(message);
69
+ if (updatedMessage !== message) {
70
+ userMessagesProcessed++;
71
+ processedCount++;
72
+ }
73
+ } else if (message.role === 'assistant') {
74
+ updatedMessage = await this.processAssistantMessage(message);
75
+ if (updatedMessage !== message) {
76
+ assistantMessagesProcessed++;
77
+ processedCount++;
78
+ }
79
+ }
80
+
81
+ if (updatedMessage !== message) {
82
+ clonedContext.messages[i] = updatedMessage;
83
+ log(`Processed message content ${message.id}, role: ${message.role}`);
84
+ }
85
+ } catch (error) {
86
+ log.extend('error')(`Error processing message ${message.id} content: ${error}`);
87
+ // 继续处理其他消息
88
+ }
89
+ }
90
+
91
+ // 更新元数据
92
+ clonedContext.metadata.messageContentProcessed = processedCount;
93
+ clonedContext.metadata.userMessagesProcessed = userMessagesProcessed;
94
+ clonedContext.metadata.assistantMessagesProcessed = assistantMessagesProcessed;
95
+
96
+ log(
97
+ `Message content processing completed, processed ${processedCount} messages (user: ${userMessagesProcessed}, assistant: ${assistantMessagesProcessed})`,
98
+ );
99
+
100
+ return this.markAsExecuted(clonedContext);
101
+ }
102
+
103
+ /**
104
+ * Process user message content
105
+ */
106
+ private async processUserMessage(message: any): Promise<any> {
107
+ // Check if images or files need processing
108
+ const hasImages = message.imageList && message.imageList.length > 0;
109
+ const hasFiles = message.fileList && message.fileList.length > 0;
110
+
111
+ // If no images and files, return plain text content directly
112
+ if (!hasImages && !hasFiles) {
113
+ return {
114
+ ...message,
115
+ content: message.content,
116
+ };
117
+ }
118
+
119
+ const contentParts: UserMessageContentPart[] = [];
120
+
121
+ // Add text content
122
+ let textContent = message.content || '';
123
+
124
+ // Add file context (if file context is enabled and has files or images)
125
+ if ((hasFiles || hasImages) && this.config.fileContext?.enabled) {
126
+ const filesContext = filesPrompts({
127
+ addUrl: this.config.fileContext.includeFileUrl ?? true,
128
+ fileList: message.fileList,
129
+ imageList: message.imageList,
130
+ });
131
+
132
+ if (filesContext) {
133
+ textContent = (textContent + '\n\n' + filesContext).trim();
134
+ }
135
+ }
136
+
137
+ // Add text part
138
+ if (textContent) {
139
+ contentParts.push({
140
+ text: textContent,
141
+ type: 'text',
142
+ });
143
+ }
144
+
145
+ // Process image content
146
+ if (hasImages && this.config.isCanUseVision?.(this.config.model, this.config.provider)) {
147
+ const imageContentParts = await this.processImageList(message.imageList || []);
148
+ contentParts.push(...imageContentParts);
149
+ }
150
+
151
+ // 明确返回的字段,只保留必要的消息字段
152
+ const hasFileContext = (hasFiles || hasImages) && this.config.fileContext?.enabled;
153
+ const hasVisionContent =
154
+ hasImages && this.config.isCanUseVision?.(this.config.model, this.config.provider);
155
+
156
+ // 如果只有文本内容且没有添加文件上下文也没有视觉内容,返回纯文本
157
+ if (
158
+ contentParts.length === 1 &&
159
+ contentParts[0].type === 'text' &&
160
+ !hasFileContext &&
161
+ !hasVisionContent
162
+ ) {
163
+ return {
164
+ content: contentParts[0].text,
165
+ createdAt: message.createdAt,
166
+ id: message.id,
167
+ meta: message.meta,
168
+ role: message.role,
169
+ updatedAt: message.updatedAt,
170
+ // 保留其他可能需要的字段,但移除已处理的文件相关字段
171
+ ...(message.tools && { tools: message.tools }),
172
+ ...(message.tool_calls && { tool_calls: message.tool_calls }),
173
+ ...(message.tool_call_id && { tool_call_id: message.tool_call_id }),
174
+ ...(message.name && { name: message.name }),
175
+ };
176
+ }
177
+
178
+ // 返回结构化内容
179
+ return {
180
+ content: contentParts,
181
+ createdAt: message.createdAt,
182
+ id: message.id,
183
+ meta: message.meta,
184
+ role: message.role,
185
+ updatedAt: message.updatedAt,
186
+ // 保留其他可能需要的字段,但移除已处理的文件相关字段
187
+ ...(message.tools && { tools: message.tools }),
188
+ ...(message.tool_calls && { tool_calls: message.tool_calls }),
189
+ ...(message.tool_call_id && { tool_call_id: message.tool_call_id }),
190
+ ...(message.name && { name: message.name }),
191
+ };
192
+ }
193
+
194
+ /**
195
+ * 处理助手消息内容
196
+ */
197
+ private async processAssistantMessage(message: any): Promise<any> {
198
+ // 检查是否有推理内容(thinking mode)
199
+ const shouldIncludeThinking = message.reasoning && !!message.reasoning?.signature;
200
+
201
+ if (shouldIncludeThinking) {
202
+ const contentParts: UserMessageContentPart[] = [
203
+ {
204
+ signature: message.reasoning!.signature,
205
+ thinking: message.reasoning!.content,
206
+ type: 'thinking',
207
+ },
208
+ {
209
+ text: message.content,
210
+ type: 'text',
211
+ },
212
+ ];
213
+
214
+ return {
215
+ ...message,
216
+ content: contentParts,
217
+ };
218
+ }
219
+
220
+ // 检查是否有图片(助手消息也可能包含图片)
221
+ const hasImages = message.imageList && message.imageList.length > 0;
222
+
223
+ if (hasImages && this.config.isCanUseVision?.(this.config.model, this.config.provider)) {
224
+ // 创建结构化内容
225
+ const contentParts: UserMessageContentPart[] = [];
226
+
227
+ if (message.content) {
228
+ contentParts.push({
229
+ text: message.content,
230
+ type: 'text',
231
+ });
232
+ }
233
+
234
+ // 处理图片内容
235
+ const imageContentParts = await this.processImageList(message.imageList || []);
236
+ contentParts.push(...imageContentParts);
237
+
238
+ return {
239
+ ...message,
240
+ content: contentParts,
241
+ };
242
+ }
243
+
244
+ // 普通助手消息,返回纯文本内容
245
+ return {
246
+ ...message,
247
+ content: message.content,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * 处理图片列表
253
+ */
254
+ private async processImageList(imageList: any[]): Promise<UserMessageContentPart[]> {
255
+ if (!imageList || imageList.length === 0) {
256
+ return [];
257
+ }
258
+
259
+ return Promise.all(
260
+ imageList.map(async (image) => {
261
+ const { type } = parseDataUri(image.url);
262
+
263
+ let processedUrl = image.url;
264
+ if (type === 'url' && isLocalUrl(image.url)) {
265
+ const { base64, mimeType } = await imageUrlToBase64(image.url);
266
+ processedUrl = `data:${mimeType};base64,${base64}`;
267
+ }
268
+
269
+ return {
270
+ image_url: { detail: 'auto', url: processedUrl },
271
+ type: 'image_url',
272
+ } as UserMessageContentPart;
273
+ }),
274
+ );
275
+ }
276
+
277
+ /**
278
+ * 验证内容部分格式
279
+ */
280
+ private validateContentPart(part: UserMessageContentPart): boolean {
281
+ if (!part || !part.type) return false;
282
+
283
+ switch (part.type) {
284
+ case 'text': {
285
+ return typeof part.text === 'string';
286
+ }
287
+ case 'image_url': {
288
+ return !!(part.image_url && part.image_url.url);
289
+ }
290
+ case 'thinking': {
291
+ return !!(part.thinking && part.signature);
292
+ }
293
+ default: {
294
+ return false;
295
+ }
296
+ }
297
+ }
298
+ }
@@ -0,0 +1,196 @@
1
+ import debug from 'debug';
2
+
3
+ import { BaseProcessor } from '../base/BaseProcessor';
4
+ import type { PipelineContext, ProcessorOptions } from '../types';
5
+
6
+ const log = debug('context-engine:processor:PlaceholderVariablesProcessor');
7
+
8
+ const placeholderVariablesRegex = /{{(.*?)}}/g;
9
+
10
+ export interface PlaceholderVariablesConfig {
11
+ /** Recursive parsing depth, default is 2 */
12
+ depth?: number;
13
+ /** Variable generators mapping, key is variable name, value is generator function */
14
+ variableGenerators: Record<string, () => string>;
15
+ }
16
+
17
+ /**
18
+ * Extract all {{variable}} placeholder variable names from text
19
+ * @param text String containing template variables
20
+ * @returns Array of variable names, e.g. ['date', 'nickname']
21
+ */
22
+ const extractPlaceholderVariables = (text: string): string[] => {
23
+ const matches = [...text.matchAll(placeholderVariablesRegex)];
24
+ return matches.map((m) => m[1].trim());
25
+ };
26
+
27
+ /**
28
+ * Replace template variables with actual values, supporting recursive parsing of nested variables
29
+ * @param text - Original text containing variables
30
+ * @param variableGenerators - Variable generators mapping
31
+ * @param depth - Recursive depth, default 2, set higher to support {{date}} within {{text}}
32
+ * @returns Text with variables replaced
33
+ */
34
+ export const parsePlaceholderVariables = (
35
+ text: string,
36
+ variableGenerators: Record<string, () => string>,
37
+ depth = 2,
38
+ ): string => {
39
+ let result = text;
40
+
41
+ // Recursive parsing to handle cases like {{text}} containing additional preset variables
42
+ for (let i = 0; i < depth; i++) {
43
+ try {
44
+ const extractedVariables = extractPlaceholderVariables(result);
45
+ const availableVariables = Object.fromEntries(
46
+ extractedVariables
47
+ .map((key) => [key, variableGenerators[key]?.()])
48
+ .filter(([, value]) => value !== undefined),
49
+ );
50
+
51
+ // Only perform replacement when there are available variables
52
+ if (Object.keys(availableVariables).length === 0) break;
53
+
54
+ // Replace variables one by one to avoid lodash template's error handling for undefined variables
55
+ let tempResult = result;
56
+ for (const [key, value] of Object.entries(availableVariables)) {
57
+ const regex = new RegExp(
58
+ `{{\\s*${key.replaceAll(/[$()*+.?[\\\]^{|}]/g, '\\$&')}\\s*}}`,
59
+ 'g',
60
+ );
61
+ // @ts-ignore
62
+ tempResult = tempResult.replace(regex, value);
63
+ }
64
+
65
+ if (tempResult === result) break;
66
+ result = tempResult;
67
+ } catch {
68
+ break;
69
+ }
70
+ }
71
+
72
+ return result;
73
+ };
74
+
75
+ /**
76
+ * Parse message content and replace placeholder variables
77
+ * @param messages Original messages array
78
+ * @param variableGenerators Variable generators mapping
79
+ * @param depth Recursive parsing depth, default is 2
80
+ * @returns Processed messages array
81
+ */
82
+ export const parsePlaceholderVariablesMessages = (
83
+ messages: any[],
84
+ variableGenerators: Record<string, () => string>,
85
+ depth = 2,
86
+ ): any[] =>
87
+ messages.map((message) => {
88
+ if (!message?.content) return message;
89
+
90
+ const { content } = message;
91
+
92
+ // Handle string type directly
93
+ if (typeof content === 'string') {
94
+ return { ...message, content: parsePlaceholderVariables(content, variableGenerators, depth) };
95
+ }
96
+
97
+ // Handle array type by processing text elements
98
+ if (Array.isArray(content)) {
99
+ return {
100
+ ...message,
101
+ content: content.map((item) =>
102
+ item?.type === 'text'
103
+ ? { ...item, text: parsePlaceholderVariables(item.text, variableGenerators, depth) }
104
+ : item,
105
+ ),
106
+ };
107
+ }
108
+
109
+ return message;
110
+ });
111
+
112
+ /**
113
+ * PlaceholderVariables Processor
114
+ * Responsible for handling placeholder variable replacement in messages
115
+ */
116
+ export class PlaceholderVariablesProcessor extends BaseProcessor {
117
+ readonly name = 'PlaceholderVariablesProcessor';
118
+
119
+ constructor(
120
+ private config: PlaceholderVariablesConfig,
121
+ options: ProcessorOptions = {},
122
+ ) {
123
+ super(options);
124
+ }
125
+
126
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
127
+ const clonedContext = this.cloneContext(context);
128
+
129
+ let processedCount = 0;
130
+ const depth = this.config.depth ?? 2;
131
+
132
+ log(
133
+ `Starting placeholder variables processing with ${Object.keys(this.config.variableGenerators).length} generators`,
134
+ );
135
+
136
+ // 处理每条消息的占位符变量
137
+ for (let i = 0; i < clonedContext.messages.length; i++) {
138
+ const message = clonedContext.messages[i];
139
+
140
+ try {
141
+ const originalMessage = JSON.stringify(message);
142
+ const processedMessage = this.processMessagePlaceholders(message, depth);
143
+
144
+ if (JSON.stringify(processedMessage) !== originalMessage) {
145
+ clonedContext.messages[i] = processedMessage;
146
+ processedCount++;
147
+ log(`Processed placeholders in message ${message.id}, role: ${message.role}`);
148
+ }
149
+ } catch (error) {
150
+ log.extend('error')(`Error processing placeholders in message ${message.id}: ${error}`);
151
+ // 继续处理其他消息
152
+ }
153
+ }
154
+
155
+ // 更新元数据
156
+ clonedContext.metadata.placeholderVariablesProcessed = processedCount;
157
+
158
+ log(`Placeholder variables processing completed, processed ${processedCount} messages`);
159
+
160
+ return this.markAsExecuted(clonedContext);
161
+ }
162
+
163
+ /**
164
+ * 处理单个消息的占位符变量
165
+ */
166
+ private processMessagePlaceholders(message: any, depth: number): any {
167
+ if (!message?.content) return message;
168
+
169
+ const { content } = message;
170
+
171
+ // Handle string type directly
172
+ if (typeof content === 'string') {
173
+ return {
174
+ ...message,
175
+ content: parsePlaceholderVariables(content, this.config.variableGenerators, depth),
176
+ };
177
+ }
178
+
179
+ // Handle array type by processing text elements
180
+ if (Array.isArray(content)) {
181
+ return {
182
+ ...message,
183
+ content: content.map((item) =>
184
+ item?.type === 'text'
185
+ ? {
186
+ ...item,
187
+ text: parsePlaceholderVariables(item.text, this.config.variableGenerators, depth),
188
+ }
189
+ : item,
190
+ ),
191
+ };
192
+ }
193
+
194
+ return message;
195
+ }
196
+ }
@@ -0,0 +1,186 @@
1
+ import debug from 'debug';
2
+
3
+ import { BaseProcessor } from '../base/BaseProcessor';
4
+ import type { MessageToolCall, PipelineContext, ProcessorOptions } from '../types';
5
+
6
+ const log = debug('context-engine:processor:ToolCallProcessor');
7
+
8
+ export interface ToolCallConfig {
9
+ /** Function to generate tool calling name */
10
+ genToolCallingName?: (identifier: string, apiName: string, type?: string) => string;
11
+ /** Function to check if function calling is supported */
12
+ isCanUseFC?: (model: string, provider: string) => boolean;
13
+ /** Model name */
14
+ model: string;
15
+ /** Provider name */
16
+ provider: string;
17
+ }
18
+
19
+ /**
20
+ * Tool Call Processor
21
+ * Responsible for converting ChatMessage format tool calls to OpenAI format
22
+ */
23
+ export class ToolCallProcessor extends BaseProcessor {
24
+ readonly name = 'ToolCallProcessor';
25
+
26
+ constructor(
27
+ private config: ToolCallConfig,
28
+ options: ProcessorOptions = {},
29
+ ) {
30
+ super(options);
31
+ }
32
+
33
+ protected async doProcess(context: PipelineContext): Promise<PipelineContext> {
34
+ const clonedContext = this.cloneContext(context);
35
+
36
+ const supportTools = this.config.isCanUseFC
37
+ ? this.config.isCanUseFC(this.config.model, this.config.provider)
38
+ : true;
39
+
40
+ let processedCount = 0;
41
+ let toolCallsConverted = 0;
42
+ let toolMessagesConverted = 0;
43
+
44
+ // 处理每条消息的工具调用
45
+ for (let i = 0; i < clonedContext.messages.length; i++) {
46
+ const message = clonedContext.messages[i];
47
+
48
+ try {
49
+ const updatedMessage = await this.processMessage(message, supportTools);
50
+
51
+ if (updatedMessage !== message) {
52
+ processedCount++;
53
+ clonedContext.messages[i] = updatedMessage;
54
+
55
+ // 统计转换的工具调用和工具消息数量
56
+ if (message.role === 'assistant' && message.tools) {
57
+ toolCallsConverted += message.tools.length;
58
+ }
59
+ if (message.role === 'tool') {
60
+ toolMessagesConverted++;
61
+ }
62
+
63
+ log(`处理消息 ${message.id},角色: ${message.role}`);
64
+ }
65
+ } catch (error) {
66
+ log.extend('error')(`处理消息 ${message.id} 工具调用时出错: ${error}`);
67
+ // 继续处理其他消息
68
+ }
69
+ }
70
+
71
+ // 更新元数据
72
+ clonedContext.metadata.toolCallProcessed = processedCount;
73
+ clonedContext.metadata.toolCallsConverted = toolCallsConverted;
74
+ clonedContext.metadata.toolMessagesConverted = toolMessagesConverted;
75
+ clonedContext.metadata.supportTools = supportTools;
76
+
77
+ log(
78
+ `Tool call processing completed, processed ${processedCount} messages, converted ${toolCallsConverted} tool calls, ${toolMessagesConverted} tool messages`,
79
+ );
80
+
81
+ return this.markAsExecuted(clonedContext);
82
+ }
83
+
84
+ /**
85
+ * 处理单条消息的工具调用
86
+ */
87
+ private async processMessage(message: any, supportTools: boolean): Promise<any> {
88
+ switch (message.role) {
89
+ case 'assistant': {
90
+ return this.processAssistantMessage(message, supportTools);
91
+ }
92
+
93
+ case 'tool': {
94
+ return this.processToolMessage(message, supportTools);
95
+ }
96
+
97
+ default: {
98
+ return message;
99
+ }
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 处理助手消息的工具调用
105
+ */
106
+ private processAssistantMessage(message: any, supportTools: boolean): any {
107
+ // 检查是否有工具调用
108
+ const hasTools = message.tools && message.tools.length > 0;
109
+ const hasEmptyToolCalls = message.tool_calls && message.tool_calls.length === 0;
110
+
111
+ if (!supportTools || (!hasTools && hasEmptyToolCalls)) {
112
+ // 如果不支持工具或只有空的工具调用,返回普通消息(移除工具相关属性)
113
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
114
+ const { tools, tool_calls, ...messageWithoutTools } = message;
115
+ return messageWithoutTools;
116
+ }
117
+
118
+ if (!hasTools) {
119
+ // 如果没有 tools 但有其他工具调用属性,只移除 tools
120
+ return message;
121
+ }
122
+
123
+ // 将 tools 转换为 tool_calls 格式
124
+ const tool_calls = message.tools.map(
125
+ (tool: any): MessageToolCall => ({
126
+ function: {
127
+ arguments: tool.arguments,
128
+ name: this.config.genToolCallingName
129
+ ? this.config.genToolCallingName(tool.identifier, tool.apiName, tool.type)
130
+ : `${tool.identifier}.${tool.apiName}`,
131
+ },
132
+ id: tool.id,
133
+ type: 'function',
134
+ }),
135
+ );
136
+
137
+ return { ...message, tool_calls };
138
+ }
139
+
140
+ /**
141
+ * 处理工具消息
142
+ */
143
+ private processToolMessage(message: any, supportTools: boolean): any {
144
+ if (!supportTools) {
145
+ // 如果不支持工具,将工具消息转换为用户消息
146
+ return {
147
+ ...message,
148
+ name: undefined,
149
+ plugin: undefined,
150
+ role: 'user',
151
+ tool_call_id: undefined,
152
+ };
153
+ }
154
+
155
+ // 生成工具名称
156
+ const toolName = message.plugin
157
+ ? this.config.genToolCallingName
158
+ ? this.config.genToolCallingName(
159
+ message.plugin.identifier,
160
+ message.plugin.apiName,
161
+ message.plugin.type,
162
+ )
163
+ : `${message.plugin.identifier}.${message.plugin.apiName}`
164
+ : undefined;
165
+
166
+ return {
167
+ ...message,
168
+ name: toolName,
169
+ // 保留 tool_call_id 用于关联
170
+ };
171
+ }
172
+
173
+ /**
174
+ * 验证工具调用格式
175
+ */
176
+ private validateToolCall(tool: any): boolean {
177
+ return !!(tool && tool.id && tool.identifier && tool.apiName && tool.arguments);
178
+ }
179
+
180
+ /**
181
+ * 验证工具消息格式
182
+ */
183
+ private validateToolMessage(message: any): boolean {
184
+ return !!(message && message.tool_call_id && message.content !== undefined);
185
+ }
186
+ }