@ray-js/t-agent-ui-ray 0.1.1 → 0.1.3-beta-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.
@@ -14,5 +14,12 @@ export declare class BlockParser {
14
14
  parse(input: string): MarkdownBlock[];
15
15
  private split;
16
16
  private parseBlock;
17
+ private processHtmlForStreaming;
18
+ private smartCompleteOrTruncateHtml;
19
+ private tryCompleteIncompleteHtml;
20
+ private canSafelyCompleteTag;
21
+ private completeOpenTag;
22
+ private removeIncompleteHtmlTags;
23
+ private findLastCompleteTagPosition;
17
24
  private lineToPosition;
18
25
  }
@@ -13,7 +13,9 @@ import MarkdownIt from 'markdown-it';
13
13
  import { full as emoji } from 'markdown-it-emoji';
14
14
  import footnote from 'markdown-it-footnote';
15
15
  import { generateId } from '@ray-js/t-agent';
16
- const md = new MarkdownIt();
16
+ const md = new MarkdownIt({
17
+ html: true // 允许 HTML 标签
18
+ });
17
19
  function addClassToTag(MD) {
18
20
  // 拦截所有的 token
19
21
  MD.core.ruler.push('_add_class_to_tags', state => {
@@ -125,27 +127,6 @@ export class BlockParser {
125
127
  }
126
128
  parse(input) {
127
129
  this.list = this.parseBlock(input);
128
- // if (!this.currentInput) {
129
- // // 第一次解析
130
- // this.list = this.parseBlock(input)
131
- // } else if (input.length < this.currentInput.length) {
132
- // // 长度有减少,重新开始
133
- // this.list = this.parseBlock(input)
134
- // } else if (input === this.currentInput) {
135
- // // 完全相等不需要处理
136
- // return this.list
137
- // } else if (input.startsWith(this.currentInput)) {
138
- // // 继续解析
139
- // const last = this.list.pop() // 丢弃掉最后一个 block
140
- // const position = this.lineToPosition(last.map[0], input)
141
- // const delta = input.slice(position)
142
- // const blocks = this.parseBlock(delta)
143
- // this.list = this.list.concat(blocks)
144
- // } else {
145
- // // 完全不匹配,重新开始
146
- // this.list = this.parseBlock(input)
147
- // }
148
-
149
130
  this.currentInput = input;
150
131
  return this.list;
151
132
  }
@@ -176,8 +157,11 @@ export class BlockParser {
176
157
  }
177
158
  parseBlock(input) {
178
159
  const _input = input.replace(/\\n/g, '<br>').replace(/\\<br>/g, '<br>').replace(/\\"/g, '"');
160
+
161
+ // 在markdown渲染之前先处理HTML完整性,避免不完整HTML被原样输出
162
+ const processedInput = this.processHtmlForStreaming(_input);
179
163
  this.pendingBlocks = [];
180
- const html = md.render(_input, {
164
+ const html = md.render(processedInput, {
181
165
  blockParser: this
182
166
  });
183
167
  if (!this.pendingBlocks.length) {
@@ -230,6 +214,189 @@ export class BlockParser {
230
214
  }
231
215
  return blocks;
232
216
  }
217
+ processHtmlForStreaming(html) {
218
+ if (!html || typeof html !== 'string') {
219
+ return html;
220
+ }
221
+
222
+ // 尝试智能补全不完整的HTML,如果无法补全则安全截断
223
+ return this.smartCompleteOrTruncateHtml(html);
224
+ }
225
+ smartCompleteOrTruncateHtml(html) {
226
+ // 首先尝试智能补全
227
+ const completed = this.tryCompleteIncompleteHtml(html);
228
+ if (completed !== html) {
229
+ return completed;
230
+ }
231
+
232
+ // 如果无法补全,则进行安全截断
233
+ return this.removeIncompleteHtmlTags(html);
234
+ }
235
+ tryCompleteIncompleteHtml(html) {
236
+ // 检测末尾的不完整标签
237
+ const incompleteTagMatch = html.match(/<([a-zA-Z][a-zA-Z0-9]*)[^>]*$/);
238
+ if (!incompleteTagMatch) {
239
+ return html; // 没有不完整标签
240
+ }
241
+ const [fullMatch, tagName] = incompleteTagMatch;
242
+ const beforeTag = html.slice(0, incompleteTagMatch.index);
243
+
244
+ // 检查这个标签是否是自闭合标签
245
+
246
+ if (['img', 'br', 'hr', 'input', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'].includes(tagName.toLowerCase())) {
247
+ // 自闭合标签,尝试补全为自闭合形式
248
+ return beforeTag + "<".concat(tagName, " />");
249
+ }
250
+
251
+ // 普通标签,检查是否可以安全补全
252
+ if (this.canSafelyCompleteTag(fullMatch, tagName)) {
253
+ // 尝试补全开始标签并立即闭合
254
+ const completedOpenTag = this.completeOpenTag(fullMatch);
255
+ return beforeTag + completedOpenTag + "</".concat(tagName, ">");
256
+ }
257
+
258
+ // 无法安全补全,返回原内容以便后续截断处理
259
+ return html;
260
+ }
261
+ canSafelyCompleteTag(incompleteTag, tagName) {
262
+ // 只对常见的、相对安全的标签进行补全
263
+
264
+ if (!['div', 'span', 'p', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'b', 'i', 'u'].includes(tagName.toLowerCase())) {
265
+ return false;
266
+ }
267
+
268
+ // 检查标签是否看起来是在正常的属性声明中
269
+ // 如果包含未闭合的引号,可能不安全
270
+ const quoteCount = (incompleteTag.match(/"/g) || []).length;
271
+ if (quoteCount % 2 !== 0) {
272
+ return false; // 有未闭合的引号
273
+ }
274
+ return true;
275
+ }
276
+ completeOpenTag(incompleteTag) {
277
+ // 简单的标签补全:如果没有以>结尾,就加上>
278
+ if (!incompleteTag.endsWith('>')) {
279
+ return incompleteTag + '>';
280
+ }
281
+ return incompleteTag;
282
+ }
283
+ removeIncompleteHtmlTags(html) {
284
+ // 找到最后一个不完整的开始标签
285
+ const lastOpenTagMatch = html.match(/<[^>]*$/);
286
+ if (lastOpenTagMatch) {
287
+ // 移除不完整的开始标签
288
+ return html.slice(0, lastOpenTagMatch.index);
289
+ }
290
+
291
+ // 检查是否有未闭合的标签
292
+ const openTags = [];
293
+ const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*>/g;
294
+ let match;
295
+ let processedHtml = html;
296
+
297
+ // 重新构建,确保标签配对
298
+ while ((match = tagRegex.exec(html)) !== null) {
299
+ const [fullMatch, tagName] = match;
300
+ const isClosingTag = fullMatch.startsWith('</');
301
+ const isSelfClosing = fullMatch.endsWith('/>') || ['img', 'br', 'hr', 'input'].includes(tagName.toLowerCase());
302
+ if (isSelfClosing) {
303
+ continue;
304
+ }
305
+ if (isClosingTag) {
306
+ // 移除对应的开始标签
307
+ const lastOpenIndex = openTags.lastIndexOf(tagName.toLowerCase());
308
+ if (lastOpenIndex !== -1) {
309
+ openTags.splice(lastOpenIndex, 1);
310
+ }
311
+ } else {
312
+ // 添加开始标签
313
+ openTags.push(tagName.toLowerCase());
314
+ }
315
+ }
316
+
317
+ // 如果有未闭合的标签,检查是否是在流式渲染过程中的正常情况
318
+ if (openTags.length > 0) {
319
+ // 检查未闭合的标签是否都是常见的容器标签,可能是流式渲染中的正常状态
320
+ const commonContainerTags = ['div', 'span', 'p', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'b', 'i', 'u'];
321
+ const hasOnlyCommonTags = openTags.every(tag => commonContainerTags.includes(tag));
322
+ if (hasOnlyCommonTags && openTags.length <= 3) {
323
+ // 如果只有少量常见的容器标签未闭合,可能是流式渲染中的正常状态
324
+ // 尝试保留更多内容,只在必要时才截断
325
+ const lastTagMatch = html.match(/<[^>]*>(?:[^<]*)$/);
326
+ if (lastTagMatch && lastTagMatch.index !== undefined) {
327
+ // 检查最后的内容是否看起来完整
328
+ const afterLastTag = html.slice(lastTagMatch.index + lastTagMatch[0].length);
329
+ if (afterLastTag.trim().length > 0 && !afterLastTag.includes('<')) {
330
+ // 最后有完整的文本内容,保留整个HTML
331
+ return html;
332
+ }
333
+ }
334
+ }
335
+
336
+ // 找到最后一个完整标签对的位置
337
+ const safeEndIndex = this.findLastCompleteTagPosition(html);
338
+ if (safeEndIndex > 0 && safeEndIndex < html.length) {
339
+ // 只有在能找到安全截断点时才进行截断
340
+ processedHtml = html.slice(0, safeEndIndex);
341
+ } else {
342
+ // 如果找不到安全截断点,保留原始HTML
343
+ // 在流式渲染中,不完整的标签可能会在后续的渲染中被补全
344
+ return html;
345
+ }
346
+ }
347
+ return processedHtml;
348
+ }
349
+ findLastCompleteTagPosition(html) {
350
+ const stack = [];
351
+ const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9]*)[^>]*>/g;
352
+ let match;
353
+ let lastCompletePos = 0;
354
+ while ((match = tagRegex.exec(html)) !== null) {
355
+ const [fullMatch, tagName] = match;
356
+ const isClosingTag = fullMatch.startsWith('</');
357
+ const isSelfClosing = fullMatch.endsWith('/>') || ['img', 'br', 'hr', 'input'].includes(tagName.toLowerCase());
358
+ if (isSelfClosing) {
359
+ lastCompletePos = match.index + fullMatch.length;
360
+ continue;
361
+ }
362
+ if (isClosingTag) {
363
+ // 查找对应的开始标签
364
+ let found = false;
365
+ for (let i = stack.length - 1; i >= 0; i--) {
366
+ if (stack[i].tag === tagName.toLowerCase()) {
367
+ stack.splice(i, 1);
368
+ found = true;
369
+ // 更新最后完整位置到这个闭合标签的结束位置
370
+ lastCompletePos = match.index + fullMatch.length;
371
+ break;
372
+ }
373
+ }
374
+ // 如果没有找到匹配的开始标签,可能是流式渲染中的部分内容,保持原有位置
375
+ if (!found) {
376
+ // 不更新lastCompletePos,保持之前的完整位置
377
+ }
378
+ } else {
379
+ stack.push({
380
+ tag: tagName.toLowerCase(),
381
+ pos: match.index
382
+ });
383
+ // 对于开始标签,如果当前没有未闭合的标签,更新完整位置
384
+ if (stack.length === 1) {
385
+ // 这是一个新的顶级标签,暂时认为到开始标签结束是安全的
386
+ // 但不更新lastCompletePos,等待对应的闭合标签
387
+ }
388
+ }
389
+ }
390
+
391
+ // 如果栈为空,说明所有标签都已闭合,整个HTML都是完整的
392
+ if (stack.length === 0) {
393
+ return html.length;
394
+ }
395
+
396
+ // 如果还有未闭合的标签,但我们有一些完整的内容,返回最后的完整位置
397
+ // 如果lastCompletePos为0,说明没有找到任何完整的标签对
398
+ return lastCompletePos;
399
+ }
233
400
  lineToPosition(lineNumber, input) {
234
401
  let position = 0;
235
402
  if (lineNumber === 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ray-js/t-agent-ui-ray",
3
- "version": "0.1.1",
3
+ "version": "0.1.3-beta-1",
4
4
  "author": "Tuya.inc",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -41,5 +41,5 @@
41
41
  "@types/echarts": "^4.9.22",
42
42
  "@types/markdown-it": "^14.1.1"
43
43
  },
44
- "gitHead": "e97f7ef72e4d5d366b5bf923525ca2e064248ab5"
44
+ "gitHead": "faf2ae5ea59b9dec9752b1e532acd4898964ea7e"
45
45
  }