byt-lingxiao-ai 0.3.24 → 0.3.25

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,107 +1,93 @@
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
+ @click="$emit('thinking-toggle')"
18
+ >
19
+ <div class="ai-thinking-time">
20
+ {{
21
+ item.duration
22
+ ? `思考用时 ${item.duration} 秒`
23
+ : '思考中...'
24
+ }}
25
+ </div>
26
+ <div
27
+ class="ai-thinking-content"
28
+ v-if="thinkingExpanded"
29
+ >
30
+ {{ item.content }}
31
+ </div>
32
+ </div>
33
+ <div
34
+ v-else-if="item.type === 'tool_call'"
35
+ class="ai-tool-call"
36
+ >
37
+ <div class="tool-title">🔧 {{ item.content }}{{ item.name }}</div>
38
+ </div>
39
+ <div
40
+ v-else-if="item.type === 'tool_result'"
41
+ class="ai-tool-result"
42
+ >
43
+ <div class="tool-title">📦 {{ item.content }}</div>
44
+ </div>
45
+ <div
46
+ v-else-if="item.type === 'content'"
47
+ class="ai-content markdown-body"
48
+ v-html="renderMarkdown(item.content)"
49
+ ></div>
12
50
  </div>
13
- <div class="ai-content markdown-body" @click="handleLinkClick" v-html="renderedContent"></div>
14
51
  </div>
15
52
  </div>
16
53
  </template>
17
54
 
18
55
  <script>
19
- import { marked } from 'marked';
20
- import hljs from 'highlight.js';
21
- import 'highlight.js/styles/github.css';
22
-
23
- // 创建自定义渲染器
24
- const renderer = new marked.Renderer()
25
-
26
- // 自定义链接渲染
27
- renderer.link = function ({ href, text }) {
28
- let dataPath = ''
29
- const url = new URL(href, window.location.origin)
30
- const pathname = url.pathname
31
-
32
- if (pathname.startsWith('/portal/')) {
33
- dataPath = pathname.substring('/portal'.length)
34
- return `<a href="javascript:void(0)" data-path="${dataPath}" data-text="${text}">${text}</a>`
35
- } else {
36
- dataPath = href
37
- return `<a href="${dataPath}" target="_blank">${text}</a>`
38
- }
39
- }
56
+ import { marked } from 'marked'
57
+ import hljs from 'highlight.js'
58
+ import 'highlight.js/styles/github.css'
59
+
40
60
  marked.setOptions({
41
- renderer: renderer,
42
- highlight: function(code, lang) {
61
+ highlight(code, lang) {
43
62
  if (lang && hljs.getLanguage(lang)) {
44
- return hljs.highlight(code, { language: lang }).value;
63
+ return hljs.highlight(code, { language: lang }).value
45
64
  }
46
- return hljs.highlightAuto(code).value;
65
+ return hljs.highlightAuto(code).value
47
66
  },
48
67
  breaks: true,
49
68
  gfm: true
50
- });
69
+ })
51
70
 
52
71
  export default {
53
- name: 'AiMessage',
54
72
  props: {
55
- message: {
56
- type: Object,
57
- required: true
58
- }
73
+ message: Object,
74
+ thinkStatus: Boolean
59
75
  },
60
76
  computed: {
61
77
  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);
78
+ return this.message.thinkingExpanded !== false
74
79
  }
75
80
  },
76
81
  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
- }
82
+ renderMarkdown(text) {
83
+ return marked.parse(text || '')
100
84
  }
101
85
  }
102
86
  }
103
87
  </script>
104
88
 
89
+
90
+
105
91
  <style scoped>
106
92
  /* Loading 容器 */
107
93
  .ai-loading {
@@ -6,11 +6,11 @@
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"
@@ -25,53 +25,34 @@ import UserMessage from './UserMessage.vue'
25
25
  import AiMessage from './AiMessage.vue'
26
26
 
27
27
  export default {
28
- name: 'ChatMessageList',
29
- components: {
30
- UserMessage,
31
- AiMessage
32
- },
28
+ components: { UserMessage, AiMessage },
33
29
  props: {
34
- messages: {
35
- type: Array,
36
- required: true
37
- },
38
- thinkStatus: {
39
- type: Boolean,
40
- default: true
41
- }
30
+ messages: Array,
31
+ thinkStatus: Boolean
42
32
  },
43
33
  computed: {
44
34
  lastMessageObject() {
45
- const len = this.messages.length;
46
- if (len === 0) {
47
- return null;
48
- }
49
- return this.messages[len - 1];
35
+ return this.messages[this.messages.length - 1] || null
50
36
  }
51
37
  },
52
38
  methods: {
53
39
  handleThinkingToggle(message) {
54
- console.log('handleThinkingToggle', message)
55
- this.$set(message, 'thinkingExpanded', !message.thinkingExpanded);
40
+ this.$set(message, 'thinkingExpanded', !message.thinkingExpanded)
56
41
  },
57
42
  scrollToBottom() {
58
43
  this.$nextTick(() => {
59
- const chatArea = this.$refs.chatArea
60
- if (chatArea) {
61
- chatArea.scrollTop = chatArea.scrollHeight
62
- }
44
+ const el = this.$refs.chatArea
45
+ if (el) el.scrollTop = el.scrollHeight
63
46
  })
64
47
  }
65
48
  },
66
49
  watch: {
67
50
  lastMessageObject: {
68
- handler(newMsg) {
69
- if (newMsg) {
70
- this.scrollToBottom();
71
- }
51
+ handler() {
52
+ this.scrollToBottom()
72
53
  },
73
54
  deep: true,
74
- immediate: true
55
+ immediate: true
75
56
  }
76
57
  }
77
58
  }
@@ -7,7 +7,14 @@ import { getCookie } from '../utils/Cookie.js'
7
7
  export default {
8
8
  data() {
9
9
  return {
10
- streamParser: null
10
+ streamParser: null,
11
+ aiMessage: {
12
+ id: Date.now(),
13
+ type: 'ai',
14
+ timeline: [],
15
+ loading: true,
16
+ thinkingExpanded: true
17
+ }
11
18
  }
12
19
  },
13
20
 
@@ -21,20 +28,9 @@ export default {
21
28
 
22
29
  methods: {
23
30
  createAiMessage() {
24
- const message = {
25
- id: this.messages.length + 1,
26
- type: 'ai',
27
- sender: 'AI',
28
- time: '',
29
- thinking: '',
30
- charts: [],
31
- content: '',
32
- loading: true,
33
- thinkingExpanded: true
34
- };
35
- this.messages.push(message);
36
- this.currentMessage = message;
37
- return message;
31
+ this.messages.push(this.aiMessage);
32
+ this.currentMessage = this.aiMessage;
33
+ return this.aiMessage;
38
34
  },
39
35
 
40
36
  createUserMessage(content) {
@@ -86,10 +82,8 @@ export default {
86
82
  await this.consumeStream(response.body);
87
83
 
88
84
  // 完成解析
89
- const self = this;
90
- this.streamParser.finish(function(result) {
91
- self.handleStreamUpdate(result);
92
- });
85
+ this.streamParser.finish(() => {});
86
+ this.aiMessage.loading = false;
93
87
 
94
88
  // 记录耗时
95
89
  const duration = Date.now() - startTime;
@@ -120,7 +114,6 @@ export default {
120
114
  async consumeStream(readableStream) {
121
115
  const reader = readableStream.getReader();
122
116
  const decoder = new TextDecoder('utf-8');
123
- const self = this;
124
117
 
125
118
  try {
126
119
  // eslint-disable-next-line no-constant-condition
@@ -131,9 +124,11 @@ export default {
131
124
  const chunk = decoder.decode(value, { stream: true });
132
125
  console.log('收到数据块:', chunk);
133
126
  // 使用解析器处理数据块,确保this指向正确
134
- this.streamParser.processChunk(chunk, function(result) {
135
- console.log('处理数据块:', result);
136
- self.handleStreamUpdate(result);
127
+ this.streamParser.processChunk(chunk, (e) => {
128
+ console.log('处理数据块:', e);
129
+ if (e.type === 'create') {
130
+ this.aiMessage.timeline.push(e.event)
131
+ }
137
132
  });
138
133
  }
139
134
  } finally {
@@ -174,11 +169,5 @@ export default {
174
169
  this.$forceUpdate();
175
170
  }
176
171
  },
177
-
178
- beforeDestroy() {
179
- // 清理解析器
180
- if (this.streamParser) {
181
- this.streamParser.destroy();
182
- }
183
- }
172
+ beforeDestroy() {}
184
173
  }
@@ -1,135 +1,181 @@
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)
81
61
  }
82
62
 
83
- // 处理标签
84
- handleTag(tag) {
85
- const t = tag.toLowerCase();
86
- if (t === '<think>') this.status = 'thinking';
87
- else if (t === '</think>') this.status = 'output';
63
+ handleTag(tag, emit) {
64
+ const t = tag.toLowerCase().trim()
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)
77
+ }
78
+ else if (t.startsWith('</tool_call')) {
79
+ this.switchType('content', emit)
80
+ }
81
+ else if (t.startsWith('<tool_result')) {
82
+ this.switchType('tool_result', emit)
83
+ }
84
+ else if (t.startsWith('</tool_result')) {
85
+ this.switchType('content', emit)
86
+ }
87
+ else {
88
+ this.append(tag, emit)
89
+ }
90
+ }
91
+
92
+
93
+ startThinking(emit) {
94
+ // 如果上一个 thinking 还没结算,先结算
95
+ this.closeThinking()
96
+
97
+ this.currentType = 'thinking'
98
+ this.currentEvent = {
99
+ id: this.uuid(),
100
+ type: 'thinking',
101
+ content: '',
102
+ startTime: Date.now(),
103
+ endTime: null,
104
+ duration: null
105
+ }
106
+
107
+ emit({ type: 'create', event: this.currentEvent })
88
108
  }
89
109
 
90
- // 添加文本到缓冲区
91
- appendText(text) {
92
- if (!text) return;
93
- if (this.status === 'thinking') this.thinkingBuffer.push(text);
94
- else this.contentBuffer.push(text);
110
+ closeThinking() {
111
+ if (
112
+ this.currentEvent &&
113
+ this.currentEvent.type === 'thinking' &&
114
+ !this.currentEvent.endTime
115
+ ) {
116
+ this.currentEvent.endTime = Date.now()
117
+ this.currentEvent.duration = (
118
+ (this.currentEvent.endTime - this.currentEvent.startTime) / 1000
119
+ ).toFixed(2)
120
+ console.log(this.currentEvent.endTime - this.currentEvent.startTime)
121
+ }
95
122
  }
96
123
 
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);
124
+ switchType(type) {
125
+ // 如果是从 thinking 切走,结算时间
126
+ if (this.currentEvent?.type === 'thinking') {
127
+ this.closeThinking()
128
+ }
129
+
130
+ this.currentType = type
131
+ this.currentEvent = null
104
132
  }
105
133
 
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);
134
+ append(text, emit) {
135
+ if (!text) return
136
+
137
+ if (!this.currentEvent || this.currentEvent.type !== this.currentType) {
138
+ this.currentEvent = {
139
+ id: this.uuid(),
140
+ type: this.currentType,
141
+ content: ''
142
+ }
143
+ emit({ type: 'create', event: this.currentEvent })
144
+ }
145
+
146
+ this.currentEvent.content += text
147
+ emit({ type: 'append', event: this.currentEvent })
117
148
  }
118
149
 
119
- // 完成解析
120
- finish(callback) {
121
- if (this.inTag && this.tagBuffer) {
122
- this.appendText(this.tagBuffer);
123
- this.tagBuffer = '';
124
- this.inTag = false;
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]
125
157
  }
126
- this.flush(callback);
127
- if (this.updateTimer) clearTimeout(this.updateTimer);
158
+
159
+ return attrs
160
+ }
161
+
162
+ scheduleFlush(emit) {
163
+ if (this.updateTimer) return
164
+ this.updateTimer = setTimeout(() => {
165
+ emit({ type: 'flush' })
166
+ this.updateTimer = null
167
+ }, this.options.updateInterval)
168
+ }
169
+
170
+ finish(emit) {
171
+ // 结束时如果还在 thinking,也要结算
172
+ this.closeThinking()
173
+ emit({ type: 'flush' })
174
+ if (this.updateTimer) clearTimeout(this.updateTimer)
175
+ this.reset()
128
176
  }
129
177
 
130
- // 销毁解析器
131
- destroy() {
132
- if (this.updateTimer) clearTimeout(this.updateTimer);
133
- this.reset();
178
+ uuid() {
179
+ return Math.random().toString(36).slice(2)
134
180
  }
135
- }
181
+ }