byt-lingxiao-ai 0.3.25 → 0.3.26

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,9 +14,8 @@
14
14
  <div
15
15
  v-if="item.type === 'thinking'"
16
16
  class="ai-thinking"
17
- @click="$emit('thinking-toggle')"
18
17
  >
19
- <div class="ai-thinking-time">
18
+ <div class="ai-thinking-time" :class="{'thinking-expanded': !item.thinkingExpanded}" @click="handleThinkingToggle(item)">
20
19
  {{
21
20
  item.duration
22
21
  ? `思考用时 ${item.duration} 秒`
@@ -25,7 +24,7 @@
25
24
  </div>
26
25
  <div
27
26
  class="ai-thinking-content"
28
- v-if="thinkingExpanded"
27
+ v-if="item.thinkingExpanded"
29
28
  >
30
29
  {{ item.content }}
31
30
  </div>
@@ -34,7 +33,7 @@
34
33
  v-else-if="item.type === 'tool_call'"
35
34
  class="ai-tool-call"
36
35
  >
37
- <div class="tool-title">🔧 {{ item.content }}{{ item.name }}</div>
36
+ <div class="tool-title">🔧 {{ item.name }}</div>
38
37
  </div>
39
38
  <div
40
39
  v-else-if="item.type === 'tool_result'"
@@ -57,16 +56,35 @@ import { marked } from 'marked'
57
56
  import hljs from 'highlight.js'
58
57
  import 'highlight.js/styles/github.css'
59
58
 
59
+ // 创建自定义渲染器
60
+ const renderer = new marked.Renderer()
61
+
62
+ // 自定义链接渲染
63
+ renderer.link = function ({ href, text }) {
64
+ let dataPath = ''
65
+ const url = new URL(href, window.location.origin)
66
+ const pathname = url.pathname
67
+ const search = url.search
68
+
69
+ if (pathname.startsWith('/portal/')) {
70
+ dataPath = pathname.substring('/portal'.length) + search
71
+ return `<a href="javascript:void(0)" data-path="${dataPath}" data-text="${text}">${text}</a>`
72
+ } else {
73
+ dataPath = href
74
+ return `<a href="${dataPath}" target="_blank">${text}</a>`
75
+ }
76
+ }
60
77
  marked.setOptions({
61
- highlight(code, lang) {
78
+ renderer: renderer,
79
+ highlight: function(code, lang) {
62
80
  if (lang && hljs.getLanguage(lang)) {
63
- return hljs.highlight(code, { language: lang }).value
81
+ return hljs.highlight(code, { language: lang }).value;
64
82
  }
65
- return hljs.highlightAuto(code).value
83
+ return hljs.highlightAuto(code).value;
66
84
  },
67
85
  breaks: true,
68
86
  gfm: true
69
- })
87
+ });
70
88
 
71
89
  export default {
72
90
  props: {
@@ -81,6 +99,9 @@ export default {
81
99
  methods: {
82
100
  renderMarkdown(text) {
83
101
  return marked.parse(text || '')
102
+ },
103
+ handleThinkingToggle(item) {
104
+ this.$set(item, 'thinkingExpanded', !item.thinkingExpanded)
84
105
  }
85
106
  }
86
107
  }
@@ -187,6 +208,11 @@ export default {
187
208
  height: 16px;
188
209
  background: url('./assets/arrow.png') no-repeat;
189
210
  background-size: cover;
211
+ /* 添加过度动画 */
212
+ transition: all 0.2s ease-in-out;
213
+ }
214
+ .thinking-expanded::after {
215
+ transform: translateY(-50%) rotate(180deg);
190
216
  }
191
217
 
192
218
  .ai-thinking-content {
@@ -206,6 +232,7 @@ export default {
206
232
  font-style: normal;
207
233
  font-weight: 400;
208
234
  line-height: 24px;
235
+ margin: 10px 0;
209
236
  }
210
237
 
211
238
  /* Markdown 样式 */
@@ -14,7 +14,6 @@
14
14
  v-else
15
15
  :message="message"
16
16
  :think-status="thinkStatus"
17
- @thinking-toggle="handleThinkingToggle(message)"
18
17
  />
19
18
  </div>
20
19
  </div>
@@ -33,12 +32,18 @@ export default {
33
32
  computed: {
34
33
  lastMessageObject() {
35
34
  return this.messages[this.messages.length - 1] || null
35
+ },
36
+ timelineContents() {
37
+ if (!this.lastMessageObject || !this.lastMessageObject.timeline) {
38
+ return ''
39
+ }
40
+ // 将所有 content 拼接成一个字符串用于监听变化
41
+ return this.lastMessageObject.timeline
42
+ .map(item => item.content || '')
43
+ .join('')
36
44
  }
37
45
  },
38
46
  methods: {
39
- handleThinkingToggle(message) {
40
- this.$set(message, 'thinkingExpanded', !message.thinkingExpanded)
41
- },
42
47
  scrollToBottom() {
43
48
  this.$nextTick(() => {
44
49
  const el = this.$refs.chatArea
@@ -47,7 +52,7 @@ export default {
47
52
  }
48
53
  },
49
54
  watch: {
50
- lastMessageObject: {
55
+ timelineContents: {
51
56
  handler() {
52
57
  this.scrollToBottom()
53
58
  },
@@ -1,6 +1,7 @@
1
1
  import { StreamParser } from '../utils/StreamParser'
2
2
  import { API_URL } from '../config/index.js'
3
3
  import { getCookie } from '../utils/Cookie.js'
4
+ import generateUuid from 'components/utils/Uuid'
4
5
 
5
6
 
6
7
 
@@ -8,13 +9,7 @@ export default {
8
9
  data() {
9
10
  return {
10
11
  streamParser: null,
11
- aiMessage: {
12
- id: Date.now(),
13
- type: 'ai',
14
- timeline: [],
15
- loading: true,
16
- thinkingExpanded: true
17
- }
12
+ currentMessage: null
18
13
  }
19
14
  },
20
15
 
@@ -28,14 +23,22 @@ export default {
28
23
 
29
24
  methods: {
30
25
  createAiMessage() {
31
- this.messages.push(this.aiMessage);
32
- this.currentMessage = this.aiMessage;
33
- return this.aiMessage;
26
+ const aiMessage = {
27
+ id: generateUuid(),
28
+ type: 'ai',
29
+ sender: 'AI',
30
+ timeline: [],
31
+ loading: true,
32
+ thinkingExpanded: true
33
+ }
34
+ this.messages.push(aiMessage);
35
+ this.currentMessage = aiMessage;
36
+ return aiMessage;
34
37
  },
35
38
 
36
39
  createUserMessage(content) {
37
40
  const message = {
38
- id: this.messages.length + 1,
41
+ id: generateUuid(),
39
42
  type: 'user',
40
43
  sender: '用户',
41
44
  time: '',
@@ -83,7 +86,6 @@ export default {
83
86
 
84
87
  // 完成解析
85
88
  this.streamParser.finish(() => {});
86
- this.aiMessage.loading = false;
87
89
 
88
90
  // 记录耗时
89
91
  const duration = Date.now() - startTime;
@@ -98,7 +100,6 @@ export default {
98
100
  console.error('发送消息失败:', error);
99
101
  if (this.currentMessage) {
100
102
  this.currentMessage.content = '抱歉,发生了错误,请重试。';
101
- this.$forceUpdate();
102
103
  }
103
104
  } finally {
104
105
  // 确保加载状态关闭
@@ -125,9 +126,9 @@ export default {
125
126
  console.log('收到数据块:', chunk);
126
127
  // 使用解析器处理数据块,确保this指向正确
127
128
  this.streamParser.processChunk(chunk, (e) => {
128
- console.log('处理数据块:', e);
129
+ if (!this.currentMessage) return;
129
130
  if (e.type === 'create') {
130
- this.aiMessage.timeline.push(e.event)
131
+ this.currentMessage.timeline.push(e.event)
131
132
  }
132
133
  });
133
134
  }
@@ -60,37 +60,69 @@ export class StreamParser {
60
60
  this.scheduleFlush(emit)
61
61
  }
62
62
 
63
- handleTag(tag, emit) {
64
- const t = tag.toLowerCase().trim()
63
+ parseTag(tag) {
64
+ const isClosing = tag.startsWith('</')
65
65
 
66
- if (t.startsWith('<think')) {
67
- this.startThinking(emit)
68
- }
69
- else if (t.startsWith('</think')) {
70
- this.closeThinking()
71
- this.currentType = 'content'
72
- this.currentEvent = null
73
- }
74
- else if (t.startsWith('<tool_call')) {
75
- const attrs = this.parseTagAttributes(tag)
76
- this.switchType('tool_call', emit, attrs.name)
66
+ const content = tag
67
+ .replace(/^<\/?|>$/g, '')
68
+ .trim()
69
+
70
+ const [tagName, ...attrParts] = content.split(/\s+/)
71
+
72
+ const attrs = {}
73
+
74
+ attrParts.forEach(part => {
75
+ const [key, rawValue] = part.split('=')
76
+ if (!key || !rawValue) return
77
+
78
+ attrs[key] = rawValue.replace(/^['"]|['"]$/g, '')
79
+ })
80
+
81
+ return {
82
+ tag: tagName.replace('/', ''),
83
+ attrs,
84
+ isClosing
77
85
  }
78
- else if (t.startsWith('</tool_call')) {
79
- this.switchType('content', emit)
86
+ }
87
+
88
+
89
+ handleTag(tag, emit) {
90
+ const { tag: tagName, attrs, isClosing } = this.parseTag(tag.toLowerCase())
91
+
92
+ if (tagName === 'think') {
93
+ if (!isClosing) {
94
+ this.startThinking(attrs, emit)
95
+ } else {
96
+ this.closeThinking()
97
+ this.currentType = 'content'
98
+ this.currentEvent = null
99
+ }
80
100
  }
81
- else if (t.startsWith('<tool_result')) {
82
- this.switchType('tool_result', emit)
101
+
102
+ else if (tagName === 'tool_call') {
103
+ if (!isClosing) {
104
+ this.switchType('tool_call', emit, attrs)
105
+ } else {
106
+ this.switchType('content', emit)
107
+ }
83
108
  }
84
- else if (t.startsWith('</tool_result')) {
85
- this.switchType('content', emit)
109
+
110
+ else if (tagName === 'tool_result') {
111
+ if (!isClosing) {
112
+ this.switchType('tool_result', emit, attrs)
113
+ } else {
114
+ this.switchType('content', emit)
115
+ }
86
116
  }
117
+
87
118
  else {
88
119
  this.append(tag, emit)
89
120
  }
90
121
  }
91
122
 
92
123
 
93
- startThinking(emit) {
124
+
125
+ startThinking(attrs={}, emit) {
94
126
  // 如果上一个 thinking 还没结算,先结算
95
127
  this.closeThinking()
96
128
 
@@ -98,8 +130,10 @@ export class StreamParser {
98
130
  this.currentEvent = {
99
131
  id: this.uuid(),
100
132
  type: 'thinking',
133
+ name: attrs.name || null,
101
134
  content: '',
102
135
  startTime: Date.now(),
136
+ thinkingExpanded: true,
103
137
  endTime: null,
104
138
  duration: null
105
139
  }
@@ -121,14 +155,21 @@ export class StreamParser {
121
155
  }
122
156
  }
123
157
 
124
- switchType(type) {
158
+ switchType(type, emit, attrs={}) {
125
159
  // 如果是从 thinking 切走,结算时间
126
160
  if (this.currentEvent?.type === 'thinking') {
127
161
  this.closeThinking()
128
162
  }
129
163
 
130
164
  this.currentType = type
131
- this.currentEvent = null
165
+ this.currentEvent = {
166
+ id: this.uuid(),
167
+ type,
168
+ name: attrs.name || null,
169
+ content: ''
170
+ }
171
+
172
+ emit({ type: 'create', event: this.currentEvent })
132
173
  }
133
174
 
134
175
  append(text, emit) {
@@ -147,18 +188,6 @@ export class StreamParser {
147
188
  emit({ type: 'append', event: this.currentEvent })
148
189
  }
149
190
 
150
- parseTagAttributes(tag) {
151
- const attrRegex = /(\w+)=['"]([^'"]+)['"]/g
152
- const attrs = {}
153
- let match
154
-
155
- while ((match = attrRegex.exec(tag)) !== null) {
156
- attrs[match[1]] = match[2]
157
- }
158
-
159
- return attrs
160
- }
161
-
162
191
  scheduleFlush(emit) {
163
192
  if (this.updateTimer) return
164
193
  this.updateTimer = setTimeout(() => {