返回 AiToEarn
parseMessageContent.ts
根目录 / project / aitoearn-web / src / components / Chat / utils / parseMessageContent.ts
1 /**
2 * 消息内容解析工具
3 * 支持多种格式的用户消息内容解析:
4 * 1. Markdown 引用格式:[image]: url, [video]: url, [audio]: url, [document]: url
5 * 2. Claude Prompt 格式:{ type, source, text, cache_control }
6 */
7
8 import type { IParsedUserContent, IPromptContentItem, IUploadedMedia } from '@/store/agent'
9
10 /**
11 * Markdown 引用格式正则
12 * 匹配:[image]: url, [video]: url, [audio]: url, [document]: url
13 */
14 const MARKDOWN_REFERENCE_REGEX = /^\[(image|video|audio|document)\]: *(\S+)$/gim
15
16 /**
17 * 判断文本是否为 JSON 格式的 Claude Prompt
18 */
19 function isClaudePromptFormat(text: string): boolean {
20 const trimmed = text.trim()
21 return trimmed.startsWith('[') && trimmed.includes('"type"') && trimmed.includes('"source"')
22 }
23
24 /**
25 * 解析 Markdown 引用格式
26 * @example
27 * [image]: https://example.com/image.jpg
28 * [video]: https://example.com/video.mp4
29 * [audio]: https://example.com/audio.mp3
30 */
31 function parseMarkdownReferences(text: string): IParsedUserContent {
32 const medias: IUploadedMedia[] = []
33 let cleanedText = text
34
35 // 提取所有引用
36 const matches = text.matchAll(MARKDOWN_REFERENCE_REGEX)
37
38 for (const match of matches) {
39 const [fullMatch, type, url] = match
40 const trimmedUrl = url.trim()
41
42 if (trimmedUrl) {
43 medias.push({
44 url: trimmedUrl,
45 type: type as IUploadedMedia['type'],
46 })
47 // 从文本中移除这条引用
48 cleanedText = cleanedText.replace(fullMatch, '')
49 }
50 }
51
52 return {
53 text: cleanedText.trim(),
54 medias,
55 hasSpecialFormat: medias.length > 0,
56 }
57 }
58
59 /**
60 * 解析 Claude Prompt 格式
61 * @example
62 * [
63 * { "type": "image", "source": { "type": "url", "url": "..." } },
64 * { "type": "text", "text": "..." }
65 * ]
66 */
67 function parseClaudePrompt(text: string): IParsedUserContent {
68 try {
69 const items: IPromptContentItem[] = JSON.parse(text)
70
71 if (!Array.isArray(items)) {
72 throw new TypeError('Invalid format: not an array')
73 }
74
75 const medias: IUploadedMedia[] = []
76 const textParts: string[] = []
77
78 items.forEach((item) => {
79 if (item.type === 'text' && item.text) {
80 textParts.push(item.text)
81 }
82 else if (['image', 'video', 'audio', 'document'].includes(item.type)) {
83 if (item.source?.url) {
84 const media: IUploadedMedia = {
85 url: item.source.url,
86 type: item.type as IUploadedMedia['type'],
87 }
88
89 // 添加缓存控制(如果存在)
90 if (item.cache_control) {
91 media.cache_control = item.cache_control
92 }
93
94 medias.push(media)
95 }
96 }
97 })
98
99 return {
100 text: textParts.join('\n\n'),
101 medias,
102 hasSpecialFormat: true,
103 }
104 }
105 catch (error) {
106 console.error('Failed to parse Claude prompt format:', error)
107 // 解析失败,返回原始文本
108 return {
109 text,
110 medias: [],
111 hasSpecialFormat: false,
112 }
113 }
114 }
115
116 /**
117 * 解析用户消息内容
118 * 自动识别并解析不同格式
119 */
120 export function parseUserMessageContent(content: string | any[]): IParsedUserContent {
121 // 如果已经是数组格式(Claude format),直接解析
122 if (Array.isArray(content)) {
123 try {
124 const jsonStr = JSON.stringify(content)
125 return parseClaudePrompt(jsonStr)
126 }
127 catch {
128 return {
129 text: '',
130 medias: [],
131 hasSpecialFormat: false,
132 }
133 }
134 }
135
136 // 字符串格式
137 if (typeof content !== 'string') {
138 return {
139 text: '',
140 medias: [],
141 hasSpecialFormat: false,
142 }
143 }
144
145 const trimmedContent = content.trim()
146
147 // 判断是否为 JSON 格式的 Claude Prompt
148 if (isClaudePromptFormat(trimmedContent)) {
149 return parseClaudePrompt(trimmedContent)
150 }
151
152 // 判断是否包含 Markdown 引用
153 if (MARKDOWN_REFERENCE_REGEX.test(trimmedContent)) {
154 // 重置正则的 lastIndex
155 MARKDOWN_REFERENCE_REGEX.lastIndex = 0
156 return parseMarkdownReferences(trimmedContent)
157 }
158
159 // 普通文本
160 return {
161 text: trimmedContent,
162 medias: [],
163 hasSpecialFormat: false,
164 }
165 }
166
167 /**
168 * 格式化媒体类型显示名称
169 */
170 export function formatMediaTypeName(type: 'image' | 'video' | 'audio' | 'document'): string {
171 const names: Record<string, string> = {
172 image: 'Image',
173 video: 'Video',
174 audio: 'Audio',
175 document: 'Document',
176 }
177 return names[type] || type
178 }
179
179 lines TYPESCRIPT