byt-lingxiao-ai 0.3.24 → 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.
@@ -1,24 +1,60 @@
1
1
  <template>
2
2
  <div class="chat-window-message-ai">
3
3
  <div class="ai-render">
4
- <div class="ai-loading" v-if="isLoading">
4
+ <div v-if="message.loading" class="ai-loading">
5
5
  <div class="dot"></div>
6
6
  <div class="dot"></div>
7
7
  <div class="dot"></div>
8
8
  </div>
9
- <div class="ai-thinking" @click="$emit('thinking-toggle')" v-if="message.thinking">
10
- <div class="ai-thinking-time">{{ message.time ? `思考用时${message.time}秒` : '思考中...' }}</div>
11
- <div class="ai-thinking-content" v-if="thinkingExpanded">{{ message.thinking }}</div>
9
+ <div
10
+ v-for="item in message.timeline"
11
+ :key="item.id"
12
+ class="ai-message-item"
13
+ >
14
+ <div
15
+ v-if="item.type === 'thinking'"
16
+ class="ai-thinking"
17
+ >
18
+ <div class="ai-thinking-time" :class="{'thinking-expanded': !item.thinkingExpanded}" @click="handleThinkingToggle(item)">
19
+ {{
20
+ item.duration
21
+ ? `思考用时 ${item.duration} 秒`
22
+ : '思考中...'
23
+ }}
24
+ </div>
25
+ <div
26
+ class="ai-thinking-content"
27
+ v-if="item.thinkingExpanded"
28
+ >
29
+ {{ item.content }}
30
+ </div>
31
+ </div>
32
+ <div
33
+ v-else-if="item.type === 'tool_call'"
34
+ class="ai-tool-call"
35
+ >
36
+ <div class="tool-title">🔧 {{ item.name }}</div>
37
+ </div>
38
+ <div
39
+ v-else-if="item.type === 'tool_result'"
40
+ class="ai-tool-result"
41
+ >
42
+ <div class="tool-title">📦 {{ item.content }}</div>
43
+ </div>
44
+ <div
45
+ v-else-if="item.type === 'content'"
46
+ class="ai-content markdown-body"
47
+ v-html="renderMarkdown(item.content)"
48
+ ></div>
12
49
  </div>
13
- <div class="ai-content markdown-body" @click="handleLinkClick" v-html="renderedContent"></div>
14
50
  </div>
15
51
  </div>
16
52
  </template>
17
53
 
18
54
  <script>
19
- import { marked } from 'marked';
20
- import hljs from 'highlight.js';
21
- import 'highlight.js/styles/github.css';
55
+ import { marked } from 'marked'
56
+ import hljs from 'highlight.js'
57
+ import 'highlight.js/styles/github.css'
22
58
 
23
59
  // 创建自定义渲染器
24
60
  const renderer = new marked.Renderer()
@@ -28,9 +64,10 @@ renderer.link = function ({ href, text }) {
28
64
  let dataPath = ''
29
65
  const url = new URL(href, window.location.origin)
30
66
  const pathname = url.pathname
67
+ const search = url.search
31
68
 
32
69
  if (pathname.startsWith('/portal/')) {
33
- dataPath = pathname.substring('/portal'.length)
70
+ dataPath = pathname.substring('/portal'.length) + search
34
71
  return `<a href="javascript:void(0)" data-path="${dataPath}" data-text="${text}">${text}</a>`
35
72
  } else {
36
73
  dataPath = href
@@ -50,58 +87,28 @@ marked.setOptions({
50
87
  });
51
88
 
52
89
  export default {
53
- name: 'AiMessage',
54
90
  props: {
55
- message: {
56
- type: Object,
57
- required: true
58
- }
91
+ message: Object,
92
+ thinkStatus: Boolean
59
93
  },
60
94
  computed: {
61
95
  thinkingExpanded() {
62
- return this.message.thinkingExpanded !== false;
63
- },
64
- renderedContent() {
65
- return marked.parse(this.message.content || '');
66
- },
67
- isLoading() {
68
- return this.message.loading === true;
69
- }
70
- },
71
- watch: {
72
- thinkStatus(newVal, oldVal) {
73
- console.log('thinkStatus 变化:', newVal, oldVal);
96
+ return this.message.thinkingExpanded !== false
74
97
  }
75
98
  },
76
99
  methods: {
77
- handleLinkClick(event) {
78
- const link = event.target.closest('a')
79
- if (!link) return
80
-
81
- const routePath = link.getAttribute('data-path')
82
- const linkText = link.getAttribute('data-text')
83
-
84
- if (routePath && linkText) {
85
- event.preventDefault()
86
-
87
- if (this.$appOptions?.store?.dispatch) {
88
- this.$appOptions.store.dispatch('tags/addTagview', {
89
- path: routePath,
90
- fullPath: routePath,
91
- label: linkText,
92
- name: linkText,
93
- meta: { title: linkText }
94
- })
95
- this.$appOptions.router.push({ path: routePath })
96
- } else {
97
- this.$router.push(routePath).catch(() => {})
98
- }
99
- }
100
+ renderMarkdown(text) {
101
+ return marked.parse(text || '')
102
+ },
103
+ handleThinkingToggle(item) {
104
+ this.$set(item, 'thinkingExpanded', !item.thinkingExpanded)
100
105
  }
101
106
  }
102
107
  }
103
108
  </script>
104
109
 
110
+
111
+
105
112
  <style scoped>
106
113
  /* Loading 容器 */
107
114
  .ai-loading {
@@ -201,6 +208,11 @@ export default {
201
208
  height: 16px;
202
209
  background: url('./assets/arrow.png') no-repeat;
203
210
  background-size: cover;
211
+ /* 添加过度动画 */
212
+ transition: all 0.2s ease-in-out;
213
+ }
214
+ .thinking-expanded::after {
215
+ transform: translateY(-50%) rotate(180deg);
204
216
  }
205
217
 
206
218
  .ai-thinking-content {
@@ -220,6 +232,7 @@ export default {
220
232
  font-style: normal;
221
233
  font-weight: 400;
222
234
  line-height: 24px;
235
+ margin: 10px 0;
223
236
  }
224
237
 
225
238
  /* Markdown 样式 */
@@ -6,15 +6,14 @@
6
6
  v-for="message in messages"
7
7
  :key="message.id"
8
8
  >
9
- <UserMessage
9
+ <UserMessage
10
10
  v-if="message.type === 'user'"
11
11
  :content="message.content"
12
12
  />
13
- <AiMessage
13
+ <AiMessage
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>
@@ -25,53 +24,40 @@ import UserMessage from './UserMessage.vue'
25
24
  import AiMessage from './AiMessage.vue'
26
25
 
27
26
  export default {
28
- name: 'ChatMessageList',
29
- components: {
30
- UserMessage,
31
- AiMessage
32
- },
27
+ components: { UserMessage, AiMessage },
33
28
  props: {
34
- messages: {
35
- type: Array,
36
- required: true
37
- },
38
- thinkStatus: {
39
- type: Boolean,
40
- default: true
41
- }
29
+ messages: Array,
30
+ thinkStatus: Boolean
42
31
  },
43
32
  computed: {
44
33
  lastMessageObject() {
45
- const len = this.messages.length;
46
- if (len === 0) {
47
- return null;
34
+ return this.messages[this.messages.length - 1] || null
35
+ },
36
+ timelineContents() {
37
+ if (!this.lastMessageObject || !this.lastMessageObject.timeline) {
38
+ return ''
48
39
  }
49
- return this.messages[len - 1];
40
+ // 将所有 content 拼接成一个字符串用于监听变化
41
+ return this.lastMessageObject.timeline
42
+ .map(item => item.content || '')
43
+ .join('')
50
44
  }
51
45
  },
52
46
  methods: {
53
- handleThinkingToggle(message) {
54
- console.log('handleThinkingToggle', message)
55
- this.$set(message, 'thinkingExpanded', !message.thinkingExpanded);
56
- },
57
47
  scrollToBottom() {
58
48
  this.$nextTick(() => {
59
- const chatArea = this.$refs.chatArea
60
- if (chatArea) {
61
- chatArea.scrollTop = chatArea.scrollHeight
62
- }
49
+ const el = this.$refs.chatArea
50
+ if (el) el.scrollTop = el.scrollHeight
63
51
  })
64
52
  }
65
53
  },
66
54
  watch: {
67
- lastMessageObject: {
68
- handler(newMsg) {
69
- if (newMsg) {
70
- this.scrollToBottom();
71
- }
55
+ timelineContents: {
56
+ handler() {
57
+ this.scrollToBottom()
72
58
  },
73
59
  deep: true,
74
- immediate: true
60
+ immediate: true
75
61
  }
76
62
  }
77
63
  }
@@ -1,13 +1,15 @@
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
 
7
8
  export default {
8
9
  data() {
9
10
  return {
10
- streamParser: null
11
+ streamParser: null,
12
+ currentMessage: null
11
13
  }
12
14
  },
13
15
 
@@ -21,25 +23,22 @@ export default {
21
23
 
22
24
  methods: {
23
25
  createAiMessage() {
24
- const message = {
25
- id: this.messages.length + 1,
26
+ const aiMessage = {
27
+ id: generateUuid(),
26
28
  type: 'ai',
27
29
  sender: 'AI',
28
- time: '',
29
- thinking: '',
30
- charts: [],
31
- content: '',
30
+ timeline: [],
32
31
  loading: true,
33
32
  thinkingExpanded: true
34
- };
35
- this.messages.push(message);
36
- this.currentMessage = message;
37
- return message;
33
+ }
34
+ this.messages.push(aiMessage);
35
+ this.currentMessage = aiMessage;
36
+ return aiMessage;
38
37
  },
39
38
 
40
39
  createUserMessage(content) {
41
40
  const message = {
42
- id: this.messages.length + 1,
41
+ id: generateUuid(),
43
42
  type: 'user',
44
43
  sender: '用户',
45
44
  time: '',
@@ -86,10 +85,7 @@ export default {
86
85
  await this.consumeStream(response.body);
87
86
 
88
87
  // 完成解析
89
- const self = this;
90
- this.streamParser.finish(function(result) {
91
- self.handleStreamUpdate(result);
92
- });
88
+ this.streamParser.finish(() => {});
93
89
 
94
90
  // 记录耗时
95
91
  const duration = Date.now() - startTime;
@@ -104,7 +100,6 @@ export default {
104
100
  console.error('发送消息失败:', error);
105
101
  if (this.currentMessage) {
106
102
  this.currentMessage.content = '抱歉,发生了错误,请重试。';
107
- this.$forceUpdate();
108
103
  }
109
104
  } finally {
110
105
  // 确保加载状态关闭
@@ -120,7 +115,6 @@ export default {
120
115
  async consumeStream(readableStream) {
121
116
  const reader = readableStream.getReader();
122
117
  const decoder = new TextDecoder('utf-8');
123
- const self = this;
124
118
 
125
119
  try {
126
120
  // eslint-disable-next-line no-constant-condition
@@ -131,9 +125,11 @@ export default {
131
125
  const chunk = decoder.decode(value, { stream: true });
132
126
  console.log('收到数据块:', chunk);
133
127
  // 使用解析器处理数据块,确保this指向正确
134
- this.streamParser.processChunk(chunk, function(result) {
135
- console.log('处理数据块:', result);
136
- self.handleStreamUpdate(result);
128
+ this.streamParser.processChunk(chunk, (e) => {
129
+ if (!this.currentMessage) return;
130
+ if (e.type === 'create') {
131
+ this.currentMessage.timeline.push(e.event)
132
+ }
137
133
  });
138
134
  }
139
135
  } finally {
@@ -174,11 +170,5 @@ export default {
174
170
  this.$forceUpdate();
175
171
  }
176
172
  },
177
-
178
- beforeDestroy() {
179
- // 清理解析器
180
- if (this.streamParser) {
181
- this.streamParser.destroy();
182
- }
183
- }
173
+ beforeDestroy() {}
184
174
  }
@@ -1,135 +1,210 @@
1
1
  export class StreamParser {
2
2
  constructor(options = {}) {
3
3
  this.options = {
4
- updateInterval: 16, // 每16ms刷新一次(约60fps)
5
- debug: false,
4
+ updateInterval: 16,
6
5
  ...options
7
- };
8
- this.reset();
6
+ }
7
+ this.reset()
9
8
  }
10
9
 
11
- // 重置解析器状态
12
10
  reset() {
13
- this.buffer = '';
14
- this.contentBuffer = [];
15
- this.thinkingBuffer = [];
16
- this.inTag = false;
17
- this.tagBuffer = '';
18
- this.updateTimer = null;
19
- this.status = 'output'; // thinking | output
11
+ this.inTag = false
12
+ this.tagBuffer = ''
13
+ this.currentType = 'content'
14
+ this.currentEvent = null
15
+ this.updateTimer = null
20
16
  }
21
17
 
22
- // 处理接收到的流数据块
23
- processChunk(chunk, callback) {
24
- if (!chunk) return;
25
- this.parseContent(chunk, callback);
18
+ processChunk(chunk, emit) {
19
+ if (!chunk) return
20
+ this.parse(chunk, emit)
26
21
  }
27
22
 
28
- // 核心内容解析,支持 <think> 标签
29
- parseContent(content, callback) {
30
- let i = 0;
23
+ parse(text, emit) {
24
+ let i = 0
31
25
 
32
- while (i < content.length) {
26
+ while (i < text.length) {
33
27
  if (this.inTag) {
34
- const endIndex = content.indexOf('>', i);
35
- if (endIndex !== -1) {
36
- this.tagBuffer += content.substring(i, endIndex + 1);
37
- this.handleTag(this.tagBuffer);
38
- this.tagBuffer = '';
39
- this.inTag = false;
40
- i = endIndex + 1;
28
+ const end = text.indexOf('>', i)
29
+ if (end !== -1) {
30
+ this.tagBuffer += text.slice(i, end + 1)
31
+ this.handleTag(this.tagBuffer, emit)
32
+ this.tagBuffer = ''
33
+ this.inTag = false
34
+ i = end + 1
41
35
  } else {
42
- // 标签未闭合,超过50字符强制输出,防止阻塞
43
- this.tagBuffer += content.substring(i);
44
- if (this.tagBuffer.length > 50) {
45
- this.appendText(this.tagBuffer);
46
- this.tagBuffer = '';
47
- this.inTag = false;
48
- }
49
- break;
36
+ this.tagBuffer += text.slice(i)
37
+ break
50
38
  }
51
39
  } else {
52
- const startIndex = content.indexOf('<', i);
53
- if (startIndex !== -1) {
54
- if (startIndex > i) this.appendText(content.substring(i, startIndex));
55
-
56
- const endIndex = content.indexOf('>', startIndex);
57
- if (endIndex !== -1) {
58
- const tag = content.substring(startIndex, endIndex + 1);
59
- this.handleTag(tag);
60
- i = endIndex + 1;
40
+ const lt = text.indexOf('<', i)
41
+ if (lt !== -1) {
42
+ if (lt > i) this.append(text.slice(i, lt), emit)
43
+
44
+ const gt = text.indexOf('>', lt)
45
+ if (gt !== -1) {
46
+ this.handleTag(text.slice(lt, gt + 1), emit)
47
+ i = gt + 1
61
48
  } else {
62
- const nextChar = content[startIndex + 1];
63
- if (!/[a-zA-Z/]/.test(nextChar)) {
64
- // 很可能不是标签,直接当文本输出
65
- this.appendText('<');
66
- i = startIndex + 1;
67
- } else {
68
- this.inTag = true;
69
- this.tagBuffer = content.substring(startIndex);
70
- break;
71
- }
49
+ this.inTag = true
50
+ this.tagBuffer = text.slice(lt)
51
+ break
72
52
  }
73
53
  } else {
74
- this.appendText(content.substring(i));
75
- break;
54
+ this.append(text.slice(i), emit)
55
+ break
76
56
  }
77
57
  }
78
58
  }
79
59
 
80
- this.scheduleUpdate(callback);
60
+ this.scheduleFlush(emit)
61
+ }
62
+
63
+ parseTag(tag) {
64
+ const isClosing = tag.startsWith('</')
65
+
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
85
+ }
81
86
  }
82
87
 
83
- // 处理标签
84
- handleTag(tag) {
85
- const t = tag.toLowerCase();
86
- if (t === '<think>') this.status = 'thinking';
87
- else if (t === '</think>') this.status = 'output';
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
+ }
100
+ }
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
+ }
108
+ }
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
+ }
116
+ }
117
+
118
+ else {
119
+ this.append(tag, emit)
120
+ }
88
121
  }
89
122
 
90
- // 添加文本到缓冲区
91
- appendText(text) {
92
- if (!text) return;
93
- if (this.status === 'thinking') this.thinkingBuffer.push(text);
94
- else this.contentBuffer.push(text);
123
+
124
+
125
+ startThinking(attrs={}, emit) {
126
+ // 如果上一个 thinking 还没结算,先结算
127
+ this.closeThinking()
128
+
129
+ this.currentType = 'thinking'
130
+ this.currentEvent = {
131
+ id: this.uuid(),
132
+ type: 'thinking',
133
+ name: attrs.name || null,
134
+ content: '',
135
+ startTime: Date.now(),
136
+ thinkingExpanded: true,
137
+ endTime: null,
138
+ duration: null
139
+ }
140
+
141
+ emit({ type: 'create', event: this.currentEvent })
95
142
  }
96
143
 
97
- // 防抖刷新
98
- scheduleUpdate(callback) {
99
- if (this.updateTimer) return;
100
- this.updateTimer = setTimeout(() => {
101
- this.flush(callback);
102
- this.updateTimer = null;
103
- }, this.options.updateInterval);
144
+ closeThinking() {
145
+ if (
146
+ this.currentEvent &&
147
+ this.currentEvent.type === 'thinking' &&
148
+ !this.currentEvent.endTime
149
+ ) {
150
+ this.currentEvent.endTime = Date.now()
151
+ this.currentEvent.duration = (
152
+ (this.currentEvent.endTime - this.currentEvent.startTime) / 1000
153
+ ).toFixed(2)
154
+ console.log(this.currentEvent.endTime - this.currentEvent.startTime)
155
+ }
104
156
  }
105
157
 
106
- // 刷新缓冲区
107
- flush(callback) {
108
- if (!callback) return;
109
- const result = {
110
- thinking: this.thinkingBuffer.length ? this.thinkingBuffer.join('') : null,
111
- content: this.contentBuffer.length ? this.contentBuffer.join('') : null,
112
- status: this.status
113
- };
114
- this.thinkingBuffer = [];
115
- this.contentBuffer = [];
116
- callback(result);
158
+ switchType(type, emit, attrs={}) {
159
+ // 如果是从 thinking 切走,结算时间
160
+ if (this.currentEvent?.type === 'thinking') {
161
+ this.closeThinking()
162
+ }
163
+
164
+ this.currentType = type
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 })
117
173
  }
118
174
 
119
- // 完成解析
120
- finish(callback) {
121
- if (this.inTag && this.tagBuffer) {
122
- this.appendText(this.tagBuffer);
123
- this.tagBuffer = '';
124
- this.inTag = false;
175
+ append(text, emit) {
176
+ if (!text) return
177
+
178
+ if (!this.currentEvent || this.currentEvent.type !== this.currentType) {
179
+ this.currentEvent = {
180
+ id: this.uuid(),
181
+ type: this.currentType,
182
+ content: ''
183
+ }
184
+ emit({ type: 'create', event: this.currentEvent })
125
185
  }
126
- this.flush(callback);
127
- if (this.updateTimer) clearTimeout(this.updateTimer);
186
+
187
+ this.currentEvent.content += text
188
+ emit({ type: 'append', event: this.currentEvent })
189
+ }
190
+
191
+ scheduleFlush(emit) {
192
+ if (this.updateTimer) return
193
+ this.updateTimer = setTimeout(() => {
194
+ emit({ type: 'flush' })
195
+ this.updateTimer = null
196
+ }, this.options.updateInterval)
197
+ }
198
+
199
+ finish(emit) {
200
+ // 结束时如果还在 thinking,也要结算
201
+ this.closeThinking()
202
+ emit({ type: 'flush' })
203
+ if (this.updateTimer) clearTimeout(this.updateTimer)
204
+ this.reset()
128
205
  }
129
206
 
130
- // 销毁解析器
131
- destroy() {
132
- if (this.updateTimer) clearTimeout(this.updateTimer);
133
- this.reset();
207
+ uuid() {
208
+ return Math.random().toString(36).slice(2)
134
209
  }
135
- }
210
+ }