byt-lingxiao-ai 0.3.25 → 0.3.27

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.
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div class="chat-window-message-ai">
3
3
  <div class="ai-render">
4
- <div v-if="message.loading" class="ai-loading">
4
+ <div v-if="messageLoading" class="ai-loading">
5
5
  <div class="dot"></div>
6
6
  <div class="dot"></div>
7
7
  <div class="dot"></div>
@@ -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'"
@@ -45,6 +44,7 @@
45
44
  <div
46
45
  v-else-if="item.type === 'content'"
47
46
  class="ai-content markdown-body"
47
+ @click="handleLinkClick"
48
48
  v-html="renderMarkdown(item.content)"
49
49
  ></div>
50
50
  </div>
@@ -57,20 +57,40 @@ import { marked } from 'marked'
57
57
  import hljs from 'highlight.js'
58
58
  import 'highlight.js/styles/github.css'
59
59
 
60
+ // 创建自定义渲染器
61
+ const renderer = new marked.Renderer()
62
+
63
+ // 自定义链接渲染
64
+ renderer.link = function ({ href, text }) {
65
+ let dataPath = ''
66
+ const url = new URL(href, window.location.origin)
67
+ const pathname = url.pathname
68
+ const search = url.search
69
+
70
+ if (pathname.startsWith('/portal/')) {
71
+ dataPath = pathname.substring('/portal'.length) + search
72
+ return `<a href="javascript:void(0)" data-path="${dataPath}" data-text="${text}">${text}</a>`
73
+ } else {
74
+ dataPath = href
75
+ return `<a href="${dataPath}" target="_blank">${text}</a>`
76
+ }
77
+ }
60
78
  marked.setOptions({
61
- highlight(code, lang) {
79
+ renderer: renderer,
80
+ highlight: function(code, lang) {
62
81
  if (lang && hljs.getLanguage(lang)) {
63
- return hljs.highlight(code, { language: lang }).value
82
+ return hljs.highlight(code, { language: lang }).value;
64
83
  }
65
- return hljs.highlightAuto(code).value
84
+ return hljs.highlightAuto(code).value;
66
85
  },
67
86
  breaks: true,
68
87
  gfm: true
69
- })
88
+ });
70
89
 
71
90
  export default {
72
91
  props: {
73
92
  message: Object,
93
+ messageLoading: Boolean,
74
94
  thinkStatus: Boolean
75
95
  },
76
96
  computed: {
@@ -81,6 +101,34 @@ export default {
81
101
  methods: {
82
102
  renderMarkdown(text) {
83
103
  return marked.parse(text || '')
104
+ },
105
+ handleThinkingToggle(item) {
106
+ this.$set(item, 'thinkingExpanded', !item.thinkingExpanded)
107
+ },
108
+ handleLinkClick(event) {
109
+ const link = event.target.closest('a')
110
+ if (!link) return
111
+
112
+ const routePath = link.getAttribute('data-path')
113
+ const linkText = link.getAttribute('data-text')
114
+
115
+ if (routePath && linkText) {
116
+ event.preventDefault()
117
+
118
+ if (this.$appOptions?.store?.dispatch) {
119
+ this.$appOptions.store.dispatch('tags/addTagview', {
120
+ path: routePath,
121
+ fullPath: routePath,
122
+ label: linkText,
123
+ name: linkText,
124
+ meta: { title: linkText }
125
+ })
126
+ this.$appOptions.router.push({ path: routePath })
127
+ } else {
128
+ console.log('路由跳转:', routePath)
129
+ this.$router.push(routePath).catch(() => {})
130
+ }
131
+ }
84
132
  }
85
133
  }
86
134
  }
@@ -187,6 +235,11 @@ export default {
187
235
  height: 16px;
188
236
  background: url('./assets/arrow.png') no-repeat;
189
237
  background-size: cover;
238
+ /* 添加过度动画 */
239
+ transition: all 0.2s ease-in-out;
240
+ }
241
+ .thinking-expanded::after {
242
+ transform: translateY(-50%) rotate(180deg);
190
243
  }
191
244
 
192
245
  .ai-thinking-content {
@@ -206,6 +259,7 @@ export default {
206
259
  font-style: normal;
207
260
  font-weight: 400;
208
261
  line-height: 24px;
262
+ margin-bottom: 10px;
209
263
  }
210
264
 
211
265
  /* Markdown 样式 */
@@ -13,8 +13,8 @@
13
13
  <AiMessage
14
14
  v-else
15
15
  :message="message"
16
+ :message-loading="messageLoading"
16
17
  :think-status="thinkStatus"
17
- @thinking-toggle="handleThinkingToggle(message)"
18
18
  />
19
19
  </div>
20
20
  </div>
@@ -28,17 +28,24 @@ export default {
28
28
  components: { UserMessage, AiMessage },
29
29
  props: {
30
30
  messages: Array,
31
+ messageLoading: Boolean,
31
32
  thinkStatus: Boolean
32
33
  },
33
34
  computed: {
34
35
  lastMessageObject() {
35
36
  return this.messages[this.messages.length - 1] || null
37
+ },
38
+ timelineContents() {
39
+ if (!this.lastMessageObject || !this.lastMessageObject.timeline) {
40
+ return ''
41
+ }
42
+ // 将所有 content 拼接成一个字符串用于监听变化
43
+ return this.lastMessageObject.timeline
44
+ .map(item => item.content || '')
45
+ .join('')
36
46
  }
37
47
  },
38
48
  methods: {
39
- handleThinkingToggle(message) {
40
- this.$set(message, 'thinkingExpanded', !message.thinkingExpanded)
41
- },
42
49
  scrollToBottom() {
43
50
  this.$nextTick(() => {
44
51
  const el = this.$refs.chatArea
@@ -47,7 +54,7 @@ export default {
47
54
  }
48
55
  },
49
56
  watch: {
50
- lastMessageObject: {
57
+ timelineContents: {
51
58
  handler() {
52
59
  this.scrollToBottom()
53
60
  },
@@ -31,6 +31,7 @@
31
31
  <ChatWindowDialog
32
32
  v-model="visible"
33
33
  :messages="messages"
34
+ :message-loading="messageLoading"
34
35
  :input-message="inputMessage"
35
36
  :think-status="thinkStatus"
36
37
  :chat-id="chatId"
@@ -77,6 +78,7 @@ export default {
77
78
  inputMessage: '',
78
79
  visible: false,
79
80
  messages: [],
81
+ messageLoading: true,
80
82
  robotStatus: 'leaving',
81
83
  avaterStatus: 'normal',
82
84
  currentMessage: null,
@@ -9,8 +9,8 @@
9
9
  <ChatMessageList
10
10
  ref="messageList"
11
11
  :messages="messages"
12
+ :message-loading="messageLoading"
12
13
  :think-status="thinkStatus"
13
- :loading="loading"
14
14
  @thinking-click="$emit('thinking-click')"
15
15
  />
16
16
 
@@ -45,6 +45,10 @@ export default {
45
45
  type: Array,
46
46
  required: true
47
47
  },
48
+ messageLoading: {
49
+ type: Boolean,
50
+ default: false
51
+ },
48
52
  inputMessage: {
49
53
  type: String,
50
54
  default: ''
@@ -53,10 +57,6 @@ export default {
53
57
  type: Boolean,
54
58
  default: true
55
59
  },
56
- loading: {
57
- type: Boolean,
58
- default: false
59
- },
60
60
  chatId: {
61
61
  type: String,
62
62
  default: ''
@@ -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: '',
@@ -61,6 +64,8 @@ export default {
61
64
  const controller = new AbortController();
62
65
 
63
66
  try {
67
+ this.messageLoading = true;
68
+
64
69
  const startTime = Date.now();
65
70
  const token = getCookie('bonyear-access_token') || `44e7f112-63f3-429d-908d-2c97ec380de2`;
66
71
 
@@ -83,7 +88,6 @@ export default {
83
88
 
84
89
  // 完成解析
85
90
  this.streamParser.finish(() => {});
86
- this.aiMessage.loading = false;
87
91
 
88
92
  // 记录耗时
89
93
  const duration = Date.now() - startTime;
@@ -98,13 +102,9 @@ export default {
98
102
  console.error('发送消息失败:', error);
99
103
  if (this.currentMessage) {
100
104
  this.currentMessage.content = '抱歉,发生了错误,请重试。';
101
- this.$forceUpdate();
102
105
  }
103
106
  } finally {
104
- // 确保加载状态关闭
105
- if (this.currentMessage) {
106
- this.currentMessage.loading = false;
107
- }
107
+ this.messageLoading = false;
108
108
  }
109
109
  },
110
110
 
@@ -125,9 +125,9 @@ export default {
125
125
  console.log('收到数据块:', chunk);
126
126
  // 使用解析器处理数据块,确保this指向正确
127
127
  this.streamParser.processChunk(chunk, (e) => {
128
- console.log('处理数据块:', e);
128
+ if (!this.currentMessage) return;
129
129
  if (e.type === 'create') {
130
- this.aiMessage.timeline.push(e.event)
130
+ this.currentMessage.timeline.push(e.event)
131
131
  }
132
132
  });
133
133
  }
@@ -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(() => {