返回 AiToEarn
agent.utils.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / agent / agent.utils.ts
1 import type { ContentBlockParam } from '@anthropic-ai/sdk/resources'
2 import type { ContentBlock } from './agent.dto'
3 import { SDKMessage } from '@anthropic-ai/claude-agent-sdk'
4
5 /**
6 * 标准化 prompt 为 content 数组
7 * 将字符串转换为文本内容块数组,或直接返回已有的内容块数组
8 */
9 export function normalizePrompt(prompt: string | ContentBlock[]): ContentBlock[] {
10 if (typeof prompt === 'string') {
11 return [{ type: 'text', text: prompt }]
12 }
13 return prompt
14 }
15
16 /**
17 * 增强 prompt,在文本中添加图片和视频URL的结构化说明
18 * 用于发送给模型,不应保存到数据库
19 * 注意:video 块会被过滤掉(转换为文本说明),因为 Anthropic SDK 原生不支持 video 类型
20 */
21 export function enhancePrompt(blocks: ContentBlock[]): ContentBlockParam[] {
22 const imageUrls: string[] = []
23 const videoUrls: string[] = []
24
25 for (const block of blocks) {
26 if (block.type === 'image' && 'source' in block && block.source && typeof block.source === 'object' && 'url' in block.source) {
27 imageUrls.push(block.source.url as string)
28 }
29 if (block.type === 'video' && 'source' in block && block.source && typeof block.source === 'object' && 'url' in block.source) {
30 videoUrls.push(block.source.url as string)
31 }
32 }
33
34 // 过滤掉 video 块(原生不支持)
35 const filteredBlocks = blocks.filter(block => block.type !== 'video')
36
37 if (imageUrls.length === 0 && videoUrls.length === 0) {
38 return filteredBlocks
39 }
40
41 let mediaListText = ''
42
43 if (imageUrls.length > 0) {
44 mediaListText += `\n\nReference Images:\n${imageUrls.map((url, index) => `- Image ${index + 1}: ${url}`).join('\n')}`
45 }
46
47 if (videoUrls.length > 0) {
48 mediaListText += `\n\nReference Videos:\n${videoUrls.map((url, index) => `- Video ${index + 1}: ${url}`).join('\n')}`
49 }
50
51 let firstTextBlockIndex = -1
52 for (let i = 0; i < filteredBlocks.length; i++) {
53 if (filteredBlocks[i].type === 'text') {
54 firstTextBlockIndex = i
55 break
56 }
57 }
58
59 const result = [...filteredBlocks]
60
61 if (firstTextBlockIndex !== -1) {
62 const textBlock = result[firstTextBlockIndex] as { type: 'text', text: string }
63 result[firstTextBlockIndex] = {
64 type: 'text',
65 text: `${textBlock.text}${mediaListText}`,
66 }
67 }
68 else {
69 result.unshift({
70 type: 'text',
71 text: `Please use the following media:${mediaListText}`,
72 })
73 }
74
75 return result
76 }
77
78 /**
79 * 过滤请求 headers,移除基础 HTTP headers,保留业务相关的 headers
80 * @param headers 原始请求 headers
81 * @returns 过滤后的 headers
82 */
83 export function filterHeaders(headers: Record<string, unknown>): Record<string, string> {
84 const basicHeaders = new Set([
85 'host',
86 'connection',
87 'content-length',
88 'content-type',
89 'accept',
90 'accept-encoding',
91 'user-agent',
92 'cache-control',
93 'pragma',
94 'upgrade-insecure-requests',
95 'if-modified-since',
96 'if-none-match',
97 ])
98
99 const filtered: Record<string, string> = {}
100
101 for (const [key, value] of Object.entries(headers)) {
102 const lowerKey = key.toLowerCase()
103 if (!basicHeaders.has(lowerKey)) {
104 if (typeof value === 'string') {
105 filtered[key] = value
106 }
107 else if (Array.isArray(value)) {
108 filtered[key] = value.join(', ')
109 }
110 }
111 }
112
113 return filtered
114 }
115
116 /**
117 * 判断消息是否应该被过滤
118 * 过滤掉 type 为 user 且 isSynthetic 为 true 的消息
119 * 这些消息不应该保存到数据库,也不应该返回到前端
120 */
121 export function shouldFilterSyntheticMessage(message: SDKMessage): boolean {
122 return (
123 message.type === 'user'
124 && 'isSynthetic' in message
125 && message.isSynthetic === true
126 ) || message.type === 'system'
127 }
128
129 export function sanitizeMessage(msg: SDKMessage): Omit<SDKMessage, 'session_id'> {
130 const { session_id, ...rest } = msg
131 if (rest.type === 'assistant') {
132 rest.message.content.forEach((block) => {
133 if (block.type === 'text' && block.text === '(no content)') {
134 block.text = ''
135 }
136 })
137 }
138 if (rest.type === 'user' && typeof rest.message.content !== 'string') {
139 rest.message.content.forEach((block) => {
140 if (block.type === 'tool_result' && block.content && typeof block.content !== 'string') {
141 block.content.forEach((subBlock) => {
142 if (subBlock.type === 'image') {
143 if (subBlock.source.type === 'base64') {
144 subBlock.source.data = ''
145 }
146 }
147 })
148 }
149 })
150 if (rest.tool_use_result && typeof rest.tool_use_result !== 'string' && Array.isArray(rest.tool_use_result)) {
151 rest.tool_use_result.forEach((block) => {
152 if (block.type === 'image') {
153 if (block.source.type === 'base64') {
154 block.source.data = ''
155 }
156 }
157 })
158 }
159 }
160 return rest
161 }
162
162 lines TYPESCRIPT