byt-lingxiao-ai 0.3.20 → 0.3.22

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,50 +1,102 @@
1
1
  <template>
2
2
  <div class="chat-window-message-ai">
3
3
  <div class="ai-render">
4
+
5
+ <!-- loading -->
4
6
  <div class="ai-loading" v-if="isLoading">
5
7
  <div class="dot"></div>
6
8
  <div class="dot"></div>
7
9
  <div class="dot"></div>
8
10
  </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>
11
+ <!-- 时间线渲染 -->
12
+ <div
13
+ v-for="item in mergedTimeline"
14
+ :key="item.id"
15
+ class="ai-timeline-item"
16
+ >
17
+ <!-- 思考 -->
18
+ <div
19
+ v-if="item.type === 'thinking'"
20
+ class="ai-thinking"
21
+ @click="$emit('thinking-toggle')"
22
+ >
23
+ <div class="ai-thinking-time">
24
+ {{ message.time ? `思考用时 ${message.time} 秒` : '思考中...' }}
25
+ </div>
26
+ <div
27
+ class="ai-thinking-content"
28
+ v-if="thinkingExpanded"
29
+ >
30
+ {{ item.text }}
31
+ </div>
32
+ </div>
33
+
34
+ <!-- 工具调用 -->
35
+ <div
36
+ v-else-if="item.type === 'tool'"
37
+ class="ai-tool"
38
+ >
39
+ <div class="ai-tool-header">
40
+ 调用工具:{{ item.tool.name }}
41
+ </div>
42
+ <details>
43
+ <summary>参数</summary>
44
+ <pre>{{ item.tool.args }}</pre>
45
+ </details>
46
+ <details v-if="item.tool.result">
47
+ <summary>结果</summary>
48
+ <pre>{{ item.tool.result }}</pre>
49
+ </details>
50
+ </div>
51
+ <div
52
+ v-else-if="item.type === 'content'"
53
+ class="ai-content markdown-body"
54
+ v-html="renderMarkdown(item.text)"
55
+ @click="handleLinkClick"
56
+ />
57
+ <div
58
+ v-if="item.type === 'protocol_error'"
59
+ class="ai-protocol-error"
60
+ >
61
+ {{ item.raw }}
62
+ </div>
12
63
  </div>
13
- <div class="ai-content markdown-body" v-html="renderedContent"></div>
14
64
  </div>
15
65
  </div>
16
66
  </template>
17
-
18
67
  <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
- // 自定义URL处理逻辑
27
- // renderer.link = function(content) {
28
- // console.log(content)
29
- // return `<router-link to="http://192.168.2.24:9529/portal/permission/user">点击跳转</router-link>`
30
- // };
31
- renderer.link = function({ href, title }) {
32
- console.log(href, title)
33
- // 需要截取portal后面的路径
34
- const url = `/permission/user`
35
- return `<a href="${url}" data-path="${url}">点击跳转</a>`;
36
- };
68
+ import { marked } from 'marked'
69
+ import hljs from 'highlight.js'
70
+ import 'highlight.js/styles/github.css'
71
+
72
+ const renderer = new marked.Renderer()
73
+
74
+ // 自定义链接渲染
75
+ renderer.link = function ({ href, text }) {
76
+ let dataPath = ''
77
+ const url = new URL(href, window.location.origin)
78
+ const pathname = url.pathname
79
+
80
+ if (pathname.startsWith('/portal/')) {
81
+ dataPath = pathname.substring('/portal'.length)
82
+ return `<a href="javascript:void(0)" data-path="${dataPath}" data-text="${text}">${text}</a>`
83
+ } else {
84
+ dataPath = href
85
+ return `<a href="${dataPath}" target="_blank">${text}</a>`
86
+ }
87
+ }
88
+
37
89
  marked.setOptions({
38
- renderer: renderer,
39
- highlight: function(code, lang) {
90
+ renderer,
91
+ highlight(code, lang) {
40
92
  if (lang && hljs.getLanguage(lang)) {
41
- return hljs.highlight(code, { language: lang }).value;
93
+ return hljs.highlight(code, { language: lang }).value
42
94
  }
43
- return hljs.highlightAuto(code).value;
95
+ return hljs.highlightAuto(code).value
44
96
  },
45
97
  breaks: true,
46
98
  gfm: true
47
- });
99
+ })
48
100
 
49
101
  export default {
50
102
  name: 'AiMessage',
@@ -56,23 +108,63 @@ export default {
56
108
  },
57
109
  computed: {
58
110
  thinkingExpanded() {
59
- return this.message.thinkingExpanded !== false;
60
- },
61
- renderedContent() {
62
- return marked.parse(this.message.content || '');
111
+ return this.message.thinkingExpanded !== false
63
112
  },
64
113
  isLoading() {
65
- return this.message.loading === true;
114
+ return this.message.loading === true
115
+ },
116
+ mergedTimeline() {
117
+ const result = []
118
+ let last = null
119
+
120
+ for (const item of this.message.timeline || []) {
121
+ if (item.type === 'content') {
122
+ if (last && last.type === 'content') {
123
+ last.text += item.text
124
+ } else {
125
+ last = { ...item }
126
+ result.push(last)
127
+ }
128
+ } else {
129
+ last = null
130
+ result.push(item)
131
+ }
132
+ }
133
+ return result
66
134
  }
67
135
  },
68
- watch: {
69
- thinkStatus(newVal, oldVal) {
70
- console.log('thinkStatus 变化:', newVal, oldVal);
136
+
137
+ methods: {
138
+ renderMarkdown(text) {
139
+ return marked.parse(text || '')
140
+ },
141
+ handleLinkClick(event) {
142
+ const link = event.target.closest('a')
143
+ if (!link) return
144
+
145
+ const routePath = link.getAttribute('data-path')
146
+ const linkText = link.getAttribute('data-text')
147
+
148
+ if (routePath && linkText) {
149
+ event.preventDefault()
150
+
151
+ if (this.$appOptions?.store?.dispatch) {
152
+ this.$appOptions.store.dispatch('tags/addTagview', {
153
+ path: routePath,
154
+ fullPath: routePath,
155
+ label: linkText,
156
+ name: linkText,
157
+ meta: { title: linkText }
158
+ })
159
+ this.$appOptions.router.push({ path: routePath })
160
+ } else {
161
+ this.$router.push(routePath).catch(() => {})
162
+ }
163
+ }
71
164
  }
72
165
  }
73
166
  }
74
167
  </script>
75
-
76
168
  <style scoped>
77
169
  /* Loading 容器 */
78
170
  .ai-loading {
@@ -26,9 +26,7 @@ export default {
26
26
  type: 'ai',
27
27
  sender: 'AI',
28
28
  time: '',
29
- thinking: '',
30
- charts: [],
31
- content: '',
29
+ timeline: [],
32
30
  loading: true,
33
31
  thinkingExpanded: true
34
32
  };
@@ -66,7 +64,7 @@ export default {
66
64
 
67
65
  try {
68
66
  const startTime = Date.now();
69
- const token = getCookie('bonyear-access_token') || `44e7f112-63f3-429d-908d-2c97ec380de2`;
67
+ const token = getCookie('bonyear-access_token') || `b6dd936e-4b8b-4e10-ba70-573eb35673f1`;
70
68
 
71
69
  const response = await fetch(API_URL, {
72
70
  method: 'POST',
@@ -147,28 +145,39 @@ export default {
147
145
  handleStreamUpdate(result) {
148
146
  if (!this.currentMessage) return;
149
147
 
150
- console.log('收到更新:', result);
148
+ this.currentMessage.loading = false
151
149
 
152
- if (this.currentMessage.loading) {
153
- this.currentMessage.loading = false;
150
+ if (result.protocolError) {
151
+ this.currentMessage.timeline.push({
152
+ type: 'protocol_error',
153
+ id: Date.now() + Math.random(),
154
+ raw: result.protocolError
155
+ })
154
156
  }
155
- // 更新思考内容
156
- if (result.thinking) {
157
- this.currentMessage.thinking += result.thinking;
157
+ if (result.thinkingSegment) {
158
+ this.currentMessage.timeline.push({
159
+ type: 'thinking',
160
+ id: Date.now() + Math.random(),
161
+ text: result.thinkingSegment
162
+ })
163
+ }
164
+ if (result.toolCall) {
165
+ this.currentMessage.timeline.push({
166
+ type: 'tool',
167
+ id: result.toolCall.id,
168
+ tool: result.toolCall
169
+ })
158
170
  }
159
-
160
- // 更新回复内容
161
- console.log('更新回复内容:', result.content);
162
171
  if (result.content) {
163
- this.currentMessage.content += result.content;
172
+ this.currentMessage.timeline.push({
173
+ type: 'content',
174
+ id: Date.now() + Math.random(),
175
+ text: result.content
176
+ })
164
177
  }
165
178
 
166
179
  // 更新状态
167
- if (result.status === 'thinking') {
168
- this.avaterStatus = 'thinking';
169
- } else {
170
- this.avaterStatus = 'output';
171
- }
180
+ this.avaterStatus = result.status === 'thinking' ? 'thinking' : 'output'
172
181
 
173
182
  // 触发视图更新
174
183
  this.$forceUpdate();
@@ -1,135 +1,213 @@
1
+ // utils/StreamParser.js
1
2
  export class StreamParser {
2
3
  constructor(options = {}) {
3
4
  this.options = {
4
- updateInterval: 16, // 每16ms刷新一次(约60fps)
5
+ updateInterval: 16,
5
6
  debug: false,
6
7
  ...options
7
- };
8
- this.reset();
8
+ }
9
+ this.reset()
9
10
  }
10
11
 
11
- // 重置解析器状态
12
12
  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
13
+ this.contentBuffer = []
14
+ this.thinkingBuffer = []
15
+
16
+ this.completedThinkingSegments = []
17
+ this.completedToolCalls = []
18
+
19
+ this.currentToolCall = null
20
+
21
+ this.protocolErrorBuffer = [] // ✅ 新增
22
+
23
+ this.inTag = false
24
+ this.tagBuffer = ''
25
+ this.updateTimer = null
26
+
27
+ this.status = 'output' // thinking | output | tool_call | tool_result
20
28
  }
21
29
 
22
- // 处理接收到的流数据块
23
30
  processChunk(chunk, callback) {
24
- if (!chunk) return;
25
- this.parseContent(chunk, callback);
31
+ if (!chunk) return
32
+ this.parseContent(chunk, callback)
26
33
  }
27
34
 
28
- // 核心内容解析,支持 <think> 标签
29
35
  parseContent(content, callback) {
30
- let i = 0;
36
+ let i = 0
31
37
 
32
38
  while (i < content.length) {
33
39
  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;
40
+ const end = content.indexOf('>', i)
41
+ if (end !== -1) {
42
+ this.tagBuffer += content.slice(i, end + 1)
43
+ this.handleTag(this.tagBuffer)
44
+ this.tagBuffer = ''
45
+ this.inTag = false
46
+ i = end + 1
41
47
  } 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;
48
+ this.tagBuffer += content.slice(i)
49
+ break
50
50
  }
51
51
  } 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;
61
- } 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
- }
72
- }
73
- } else {
74
- this.appendText(content.substring(i));
75
- break;
52
+ const start = content.indexOf('<', i)
53
+ if (start === -1) {
54
+ this.appendText(content.slice(i))
55
+ break
56
+ }
57
+
58
+ if (start > i) {
59
+ this.appendText(content.slice(i, start))
76
60
  }
61
+
62
+ const end = content.indexOf('>', start)
63
+ if (end === -1) {
64
+ this.inTag = true
65
+ this.tagBuffer = content.slice(start)
66
+ break
67
+ }
68
+
69
+ const tag = content.slice(start, end + 1)
70
+ this.handleTag(tag)
71
+ i = end + 1
77
72
  }
78
73
  }
79
74
 
80
- this.scheduleUpdate(callback);
75
+ this.scheduleUpdate(callback)
81
76
  }
82
77
 
83
- // 处理标签
84
78
  handleTag(tag) {
85
- const t = tag.toLowerCase();
86
- if (t === '<think>') this.status = 'thinking';
87
- else if (t === '</think>') this.status = 'output';
79
+ const t = tag.toLowerCase()
80
+
81
+ // ---------- think ----------
82
+ if (t === '<think>') {
83
+ this.status = 'thinking'
84
+ return
85
+ }
86
+
87
+ if (t === '</think>') {
88
+ if (this.thinkingBuffer.length) {
89
+ this.completedThinkingSegments.push(
90
+ this.thinkingBuffer.join('')
91
+ )
92
+ this.thinkingBuffer = []
93
+ }
94
+ this.status = 'output'
95
+ return
96
+ }
97
+
98
+ // ---------- tool_call ----------
99
+ if (t.startsWith('<tool_call')) {
100
+ const nameMatch = tag.match(/name\s*=\s*["'](.+?)["']/)
101
+ this.currentToolCall = {
102
+ id: `${Date.now()}-${Math.random()}`,
103
+ name: nameMatch ? nameMatch[1] : 'unknown',
104
+ args: '',
105
+ result: '',
106
+ status: 'calling'
107
+ }
108
+ this.status = 'tool_call'
109
+ return
110
+ }
111
+
112
+ if (t === '</tool_call>') {
113
+ this.status = 'output'
114
+ return
115
+ }
116
+
117
+ // ---------- tool_result ----------
118
+ if (t === '<tool_result>') {
119
+ this.status = 'tool_result'
120
+ return
121
+ }
122
+
123
+ if (t === '</tool_result>') {
124
+ if (this.currentToolCall) {
125
+ this.currentToolCall.status = 'done'
126
+ this.completedToolCalls.push(this.currentToolCall)
127
+ this.currentToolCall = null
128
+ }
129
+ this.status = 'output'
130
+ return
131
+ }
132
+
133
+ // ---------- ❌ 协议错误 ----------
134
+ if (t.startsWith('<') && t.endsWith('>')) {
135
+ this.protocolErrorBuffer.push(tag)
136
+ this.status = 'output'
137
+ }
88
138
  }
89
139
 
90
- // 添加文本到缓冲区
91
140
  appendText(text) {
92
- if (!text) return;
93
- if (this.status === 'thinking') this.thinkingBuffer.push(text);
94
- else this.contentBuffer.push(text);
141
+ if (!text) return
142
+
143
+ if (this.status === 'thinking') {
144
+ this.thinkingBuffer.push(text)
145
+ }
146
+ else if (this.status === 'tool_call') {
147
+ this.currentToolCall && (this.currentToolCall.args += text)
148
+ }
149
+ else if (this.status === 'tool_result') {
150
+ this.currentToolCall && (this.currentToolCall.result += text)
151
+ }
152
+ else {
153
+ this.contentBuffer.push(text)
154
+ }
95
155
  }
96
156
 
97
- // 防抖刷新
98
157
  scheduleUpdate(callback) {
99
- if (this.updateTimer) return;
158
+ if (this.updateTimer) return
100
159
  this.updateTimer = setTimeout(() => {
101
- this.flush(callback);
102
- this.updateTimer = null;
103
- }, this.options.updateInterval);
160
+ this.flush(callback)
161
+ this.updateTimer = null
162
+ }, this.options.updateInterval)
104
163
  }
105
164
 
106
- // 刷新缓冲区
107
165
  flush(callback) {
108
- if (!callback) return;
166
+ if (!callback) return
167
+
109
168
  const result = {
110
- thinking: this.thinkingBuffer.length ? this.thinkingBuffer.join('') : null,
111
- content: this.contentBuffer.length ? this.contentBuffer.join('') : null,
169
+ thinkingSegment: null,
170
+ toolCall: null,
171
+ content: null,
172
+ protocolError: null,
112
173
  status: this.status
113
- };
114
- this.thinkingBuffer = [];
115
- this.contentBuffer = [];
116
- callback(result);
174
+ }
175
+
176
+ if (this.protocolErrorBuffer.length) {
177
+ result.protocolError = this.protocolErrorBuffer.join('\n')
178
+ this.protocolErrorBuffer = []
179
+ }
180
+
181
+ if (this.completedThinkingSegments.length) {
182
+ result.thinkingSegment =
183
+ this.completedThinkingSegments.shift()
184
+ }
185
+
186
+ if (this.completedToolCalls.length) {
187
+ result.toolCall = this.completedToolCalls.shift()
188
+ }
189
+
190
+ if (this.contentBuffer.length) {
191
+ result.content = this.contentBuffer.join('')
192
+ this.contentBuffer = []
193
+ }
194
+
195
+ callback(result)
117
196
  }
118
197
 
119
- // 完成解析
120
198
  finish(callback) {
121
- if (this.inTag && this.tagBuffer) {
122
- this.appendText(this.tagBuffer);
123
- this.tagBuffer = '';
124
- this.inTag = false;
199
+ if (this.thinkingBuffer.length) {
200
+ this.completedThinkingSegments.push(
201
+ this.thinkingBuffer.join('')
202
+ )
203
+ this.thinkingBuffer = []
125
204
  }
126
- this.flush(callback);
127
- if (this.updateTimer) clearTimeout(this.updateTimer);
205
+ this.flush(callback)
206
+ if (this.updateTimer) clearTimeout(this.updateTimer)
128
207
  }
129
208
 
130
- // 销毁解析器
131
209
  destroy() {
132
- if (this.updateTimer) clearTimeout(this.updateTimer);
133
- this.reset();
210
+ if (this.updateTimer) clearTimeout(this.updateTimer)
211
+ this.reset()
134
212
  }
135
213
  }