































@@ -193,8 +193,8 @@ export interface FirstClosedMediaTag {
193193/**
194194 * 在文本中查找**第一个**完整闭合的媒体标签
195195 *
196- * 与 splitByMediaTags 不同,此函数只匹配一个标签就停止,
197- * 用于流式场景的"循环消费"模式:每次处理一个标签,更新偏移,再找下一个。
196+ * 只匹配一个标签就停止,用于流式场景的"循环消费"模式:
197+ * 每次处理一个标签,更新偏移,再找下一个。
198198 *
199199 * @param text 待检查的文本(应已 normalize 过)
200200 * @returns 第一个闭合标签的信息,没有则返回 null
@@ -250,119 +250,6 @@ export function findFirstClosedMediaTag(
250250return null;
251251}
252252253-/**
254- * 媒体标签拆分结果
255- */
256-export interface MediaSplitResult {
257-/** 是否包含媒体标签 */
258-hasMediaTags: boolean;
259-/** 媒体标签前的纯文本 */
260-textBeforeFirstTag: string;
261-/** 媒体标签后的剩余文本 */
262-textAfterLastTag: string;
263-/** 完整的发送队列(标签间的文本 + 媒体项) */
264-mediaQueue: SendQueueItem[];
265-}
266-267-/**
268- * 将文本按富媒体标签拆分为三部分
269- *
270- * 用于两个场景:
271- * 1. 流式模式:中断-恢复流程(标签前文本 → 结束流式 → 发送媒体 → 新流式 → 标签后文本)
272- * 2. 普通模式:构建按顺序发送的队列
273- */
274-export function splitByMediaTags(
275-text: string,
276-log?: {
277-info?: (msg: string) => void;
278-debug?: (msg: string) => void;
279-error?: (msg: string) => void;
280-},
281-): MediaSplitResult {
282-const normalized = normalizeMediaTags(text);
283-const regex = createMediaTagRegex();
284-// 过滤掉代码块内的匹配
285-const matches = [...normalized.matchAll(regex)].filter(
286-(m) => !isInsideCodeBlock(normalized, m.index),
287-);
288-289-if (matches.length === 0) {
290-return {
291-hasMediaTags: false,
292-textBeforeFirstTag: normalized,
293-textAfterLastTag: "",
294-mediaQueue: [],
295-};
296-}
297-298-// 第一个标签前的纯文本
299-const firstMatch = matches[0];
300-const textBeforeFirstTag = normalized
301-.slice(0, firstMatch.index)
302-.replace(/\n{3,}/g, "\n\n")
303-.trim();
304-305-// 最后一个标签后的纯文本
306-const lastMatch = matches[matches.length - 1];
307-const lastMatchEnd = lastMatch.index + lastMatch[0].length;
308-const textAfterLastTag = normalized
309-.slice(lastMatchEnd)
310-.replace(/\n{3,}/g, "\n\n")
311-.trim();
312-313-// 构建媒体发送队列
314-const mediaQueue: SendQueueItem[] = [];
315-let lastIndex = firstMatch.index;
316-317-for (const match of matches) {
318-// 标签前的文本(标签之间的间隔文本)
319-const textBetween = normalized
320-.slice(lastIndex, match.index)
321-.replace(/\n{3,}/g, "\n\n")
322-.trim();
323-if (textBetween && lastIndex !== firstMatch.index) {
324-// 只添加非首段的间隔文本(首段由 textBeforeFirstTag 覆盖)
325-mediaQueue.push({ type: "text", content: textBetween });
326-}
327-328-// 解析标签内容
329-const tagName = match[1].toLowerCase();
330-let mediaPath = match[2]?.trim() ?? "";
331-332-// 剥离 MEDIA: 前缀
333-if (mediaPath.startsWith("MEDIA:")) {
334-mediaPath = mediaPath.slice("MEDIA:".length);
335-}
336-mediaPath = normalizePath(mediaPath);
337-338-// 修复路径编码问题
339-mediaPath = fixPathEncoding(mediaPath, log);
340-341-// 根据标签类型加入队列
342-const typeMap: Record<string, SendQueueItem["type"]> = {
343-qqimg: "image",
344-qqvoice: "voice",
345-qqvideo: "video",
346-qqfile: "file",
347-qqmedia: "media",
348-};
349-const itemType = typeMap[tagName] ?? "image";
350-if (mediaPath) {
351-mediaQueue.push({ type: itemType, content: mediaPath });
352-log?.info?.(`Found ${itemType} in <${tagName}>: ${mediaPath.slice(0, 80)}`);
353-}
354-355-lastIndex = match.index + match[0].length;
356-}
357-358-return {
359-hasMediaTags: true,
360- textBeforeFirstTag,
361- textAfterLastTag,
362- mediaQueue,
363-};
364-}
365-366253// ============ 发送队列执行 ============
367254368255/**
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。