@veltra/ai 1.4.0 → 2.1.0
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.
- package/dist/chat/index.d.ts +3 -3
- package/dist/chat/transports/index.d.ts +2 -2
- package/dist/chat/transports/openai.d.ts +16 -12
- package/dist/chat/transports/openai.js +56 -15
- package/dist/chat/transports/openai.js.map +1 -1
- package/dist/chat/types.d.ts +41 -2
- package/dist/chat/use-chat.d.ts +26 -1
- package/dist/chat/use-chat.js +132 -13
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +222 -41
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/ask-question.js +2 -2
- package/dist/components/ai-chat/ask-question.js.map +1 -1
- package/dist/components/ai-chat/chat-input.js +81 -44
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/di.js.map +1 -1
- package/dist/components/ai-chat/message-item.js +101 -23
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +217 -32
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/model-picker.js +140 -0
- package/dist/components/ai-chat/model-picker.js.map +1 -0
- package/dist/components/ai-chat/queue-list.js +70 -0
- package/dist/components/ai-chat/queue-list.js.map +1 -0
- package/dist/components/ai-chat/side-panel.js +96 -0
- package/dist/components/ai-chat/side-panel.js.map +1 -0
- package/dist/components/ai-chat/style.d.ts +6 -0
- package/dist/components/ai-chat/style.js +6 -0
- package/dist/components/ai-chat/style2.css +552 -41
- package/dist/components/ai-chat/tool-call.js +61 -27
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-orb/ai-orb.js +94 -0
- package/dist/components/ai-orb/ai-orb.js.map +1 -0
- package/dist/components/ai-orb/orb-renderer.js +411 -0
- package/dist/components/ai-orb/orb-renderer.js.map +1 -0
- package/dist/components/ai-orb/style.d.ts +1 -0
- package/dist/components/ai-orb/style.js +1 -0
- package/dist/components/ai-orb/style2.css +23 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +2 -2
- package/dist/providers/types.d.ts +47 -0
- package/dist/style.js +1 -0
- package/dist/tools/ask-question/ask-question.d.ts +2 -17
- package/dist/tools/ask-question/index.d.ts +2 -2
- package/dist/tools/index.d.ts +3 -2
- package/dist/tools/index.js +10 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/ai-chat.d.ts +35 -7
- package/dist/types/ai-orb.d.ts +27 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +8 -8
- package/src/chat/__test__/use-chat.test.ts +295 -9
- package/src/chat/transports/__test__/openai.test.ts +317 -0
- package/src/chat/transports/openai.ts +88 -26
- package/src/chat/types.ts +41 -1
- package/src/chat/use-chat.ts +177 -14
- package/src/components/ai-chat/__test__/ai-chat.test.ts +690 -12
- package/src/components/ai-chat/ai-chat.vue +241 -31
- package/src/components/ai-chat/chat-input.vue +78 -31
- package/src/components/ai-chat/di.ts +2 -0
- package/src/components/ai-chat/message-item.vue +98 -11
- package/src/components/ai-chat/message-list.vue +261 -21
- package/src/components/ai-chat/model-picker.vue +177 -0
- package/src/components/ai-chat/queue-list.vue +63 -0
- package/src/components/ai-chat/side-panel.vue +76 -0
- package/src/components/ai-chat/style.scss +621 -43
- package/src/components/ai-chat/style.ts +6 -0
- package/src/components/ai-chat/tool-call.vue +86 -42
- package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
- package/src/components/ai-orb/ai-orb.vue +107 -0
- package/src/components/ai-orb/index.ts +1 -0
- package/src/components/ai-orb/orb-renderer.ts +505 -0
- package/src/components/ai-orb/style.scss +10 -0
- package/src/components/ai-orb/style.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +8 -1
- package/src/providers/index.ts +1 -0
- package/src/providers/types.ts +46 -0
- package/src/style.ts +1 -0
- package/src/tools/ask-question/__test__/ask-question.test.ts +4 -4
- package/src/tools/index.ts +14 -1
- package/src/types/ai-chat.ts +35 -6
- package/src/types/ai-orb.ts +27 -0
- package/src/types/index.ts +1 -0
- package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
- package/dist/components/ai-chat/index.d.ts +0 -2
- package/dist/components/index.d.ts +0 -3
package/src/chat/use-chat.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { useModel } from '@veltra/compositions'
|
|
2
|
-
import { reactive, ref, type Ref } from 'vue'
|
|
2
|
+
import { reactive, ref, watch, type Ref } from 'vue'
|
|
3
3
|
|
|
4
|
+
import { createBuiltinTools } from '../tools'
|
|
4
5
|
import type { AiChatEmits, AiChatProps } from '../types'
|
|
5
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
ChatAttachment,
|
|
8
|
+
ChatMessage,
|
|
9
|
+
ChatQueuedMessage,
|
|
10
|
+
ChatTool,
|
|
11
|
+
ChatToolCall
|
|
12
|
+
} from './types'
|
|
6
13
|
|
|
7
14
|
export interface UseChatOptions {
|
|
8
15
|
props: AiChatProps
|
|
@@ -24,9 +31,17 @@ function serializeResult(result: unknown): string {
|
|
|
24
31
|
}
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
/** 内置工具 + 用户工具,同名时内置优先 */
|
|
35
|
+
function resolveTools(userTools?: ChatTool[]): ChatTool[] {
|
|
36
|
+
const builtins = createBuiltinTools()
|
|
37
|
+
const names = new Set(builtins.map((t) => t.name))
|
|
38
|
+
return [...builtins, ...(userTools ?? []).filter((t) => !names.has(t.name))]
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
/**
|
|
28
42
|
* AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。
|
|
29
43
|
* 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。
|
|
44
|
+
* 始终注入内置工具(如 askQuestion),无需经 props 传入。
|
|
30
45
|
*/
|
|
31
46
|
export function useChat(options: UseChatOptions) {
|
|
32
47
|
const { props, emit } = options
|
|
@@ -39,23 +54,120 @@ export function useChat(options: UseChatOptions) {
|
|
|
39
54
|
defaultValue: []
|
|
40
55
|
}) as Ref<ChatMessage[]>
|
|
41
56
|
|
|
57
|
+
const model = useModel({ props, propName: 'model', emit, local: true }) as Ref<string | undefined>
|
|
58
|
+
|
|
59
|
+
const reasoningLevel = useModel({ props, propName: 'reasoningLevel', emit, local: true }) as Ref<
|
|
60
|
+
string | undefined
|
|
61
|
+
>
|
|
62
|
+
|
|
42
63
|
/** 是否正在生成中(含工具执行与多轮循环) */
|
|
43
64
|
const running = ref(false)
|
|
44
65
|
|
|
66
|
+
/** 待发送队列:会话进行中提交的新消息按序排队,收尾后先进先出自动接续 */
|
|
67
|
+
const queue = ref<ChatQueuedMessage[]>([])
|
|
68
|
+
|
|
45
69
|
let abortController: AbortController | null = null
|
|
46
70
|
/** needsConfirm 工具的挂起确认器,key 为 toolCallId */
|
|
47
71
|
const confirmResolvers = new Map<string, (approved: boolean) => void>()
|
|
72
|
+
/** 当前会话是否自然完成(finish);决定收尾后是否自动消耗队列 */
|
|
73
|
+
let finishedNaturally = false
|
|
74
|
+
/** startQueued 触发的中断需要在收尾后立即接续队首 */
|
|
75
|
+
let resumeAfterAbort = false
|
|
76
|
+
|
|
77
|
+
/** 按当前模型校正推理等级(无 levels 则清空;值不合法则落到默认/首项) */
|
|
78
|
+
const syncReasoningForModel = (modelId: string | undefined) => {
|
|
79
|
+
const option = props.models?.find((m) => m.id === modelId)
|
|
80
|
+
if (!option?.reasoningLevels?.length) {
|
|
81
|
+
if (reasoningLevel.value !== undefined) {
|
|
82
|
+
reasoningLevel.value = undefined
|
|
83
|
+
}
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const levels = option.reasoningLevels
|
|
87
|
+
if (reasoningLevel.value && levels.some((l) => l.value === reasoningLevel.value)) return
|
|
88
|
+
reasoningLevel.value = option.defaultReasoningLevel ?? levels[0]?.value
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// models / model 变化时:确保有合法选中模型,并校正推理等级
|
|
92
|
+
watch(
|
|
93
|
+
() => [props.models, model.value] as const,
|
|
94
|
+
([models, current]) => {
|
|
95
|
+
if (!models?.length) return
|
|
96
|
+
if (!current || !models.some((m) => m.id === current)) {
|
|
97
|
+
model.value = models[0]!.id
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
syncReasoningForModel(current)
|
|
101
|
+
},
|
|
102
|
+
{ immediate: true }
|
|
103
|
+
)
|
|
48
104
|
|
|
49
105
|
/** 向父组件同步消息快照(流式 delta 过于频繁,不同步,只在关键节点同步) */
|
|
50
106
|
const snapshot = () => {
|
|
51
107
|
emit('update:messages', messages.value.slice())
|
|
52
108
|
}
|
|
53
109
|
|
|
54
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* 发送一条用户消息。会话进行中时进入待发送队列(不丢失),
|
|
112
|
+
* 会话自然结束后按 FIFO 自动接续;空闲时立即开始新会话。
|
|
113
|
+
*/
|
|
55
114
|
const send = (content: string, attachments?: ChatAttachment[]) => {
|
|
56
|
-
if (running.value) return
|
|
57
115
|
if (!content.trim() && !attachments?.length) return
|
|
116
|
+
enqueue(content, attachments)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 向队列插入一条消息;beforeId 指定插到某条之前(缺省追加到尾部)。
|
|
121
|
+
* 空闲时立即消耗队首(保持先来先发的合理顺序)。
|
|
122
|
+
*/
|
|
123
|
+
const enqueue = (
|
|
124
|
+
content: string,
|
|
125
|
+
attachments?: ChatAttachment[],
|
|
126
|
+
beforeId?: string
|
|
127
|
+
): ChatQueuedMessage => {
|
|
128
|
+
const item: ChatQueuedMessage = { id: uid(), content, attachments }
|
|
129
|
+
const anchorIndex = beforeId ? queue.value.findIndex((q) => q.id === beforeId) : -1
|
|
130
|
+
if (anchorIndex === -1) {
|
|
131
|
+
queue.value.push(item)
|
|
132
|
+
} else {
|
|
133
|
+
queue.value.splice(anchorIndex, 0, item)
|
|
134
|
+
}
|
|
135
|
+
if (!running.value) drainQueue()
|
|
136
|
+
return item
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 消耗队首并开始新一轮会话 */
|
|
140
|
+
const drainQueue = () => {
|
|
141
|
+
const next = queue.value.shift()
|
|
142
|
+
if (next) startUserTurn(next.content, next.attachments)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 立即执行队列中的某条:中断当前会话,该条插队为下一条(其余保持原顺序) */
|
|
146
|
+
const startQueued = (id: string) => {
|
|
147
|
+
const item = removeQueued(id)
|
|
148
|
+
if (!item) return
|
|
58
149
|
|
|
150
|
+
if (!running.value) {
|
|
151
|
+
startUserTurn(item.content, item.attachments)
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 插回队首并中断当前会话,收尾逻辑会自动接上它
|
|
156
|
+
queue.value.unshift(item)
|
|
157
|
+
resumeAfterAbort = true
|
|
158
|
+
abort()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 从队列移除某条(返回被移除项;编辑场景由 UI 取回内容) */
|
|
162
|
+
const removeQueued = (id: string): ChatQueuedMessage | undefined => {
|
|
163
|
+
const index = queue.value.findIndex((item) => item.id === id)
|
|
164
|
+
if (index === -1) return undefined
|
|
165
|
+
const [item] = queue.value.splice(index, 1)
|
|
166
|
+
return item
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 推送用户消息并启动对话循环 */
|
|
170
|
+
const startUserTurn = (content: string, attachments?: ChatAttachment[]) => {
|
|
59
171
|
const message = reactive<ChatMessage>({ id: uid(), role: 'user', content, attachments })
|
|
60
172
|
messages.value.push(message)
|
|
61
173
|
snapshot()
|
|
@@ -65,14 +177,18 @@ export function useChat(options: UseChatOptions) {
|
|
|
65
177
|
}
|
|
66
178
|
|
|
67
179
|
/** 执行单个工具调用(含确认门控),结果以 tool 消息追加 */
|
|
68
|
-
const executeToolCall = async (
|
|
180
|
+
const executeToolCall = async (
|
|
181
|
+
toolCall: ChatToolCall,
|
|
182
|
+
signal: AbortSignal,
|
|
183
|
+
tools: ChatTool[]
|
|
184
|
+
) => {
|
|
69
185
|
const appendToolMessage = (content: string) => {
|
|
70
186
|
messages.value.push(
|
|
71
187
|
reactive<ChatMessage>({ id: uid(), role: 'tool', toolCallId: toolCall.id, content })
|
|
72
188
|
)
|
|
73
189
|
}
|
|
74
190
|
|
|
75
|
-
const tool =
|
|
191
|
+
const tool = tools.find((t) => t.name === toolCall.name)
|
|
76
192
|
|
|
77
193
|
if (!tool) {
|
|
78
194
|
toolCall.status = 'error'
|
|
@@ -115,22 +231,31 @@ export function useChat(options: UseChatOptions) {
|
|
|
115
231
|
if (!props.transport) return
|
|
116
232
|
|
|
117
233
|
running.value = true
|
|
234
|
+
finishedNaturally = false
|
|
118
235
|
const controller = new AbortController()
|
|
119
236
|
abortController = controller
|
|
120
237
|
|
|
121
238
|
try {
|
|
122
|
-
await runRound(controller)
|
|
239
|
+
await runRound(controller, 0)
|
|
123
240
|
} finally {
|
|
124
241
|
running.value = false
|
|
125
242
|
abortController = null
|
|
126
243
|
snapshot()
|
|
244
|
+
|
|
245
|
+
// 队列接续:自然完成或 startQueued 中断插队时自动发下一条;
|
|
246
|
+
// 手动停止 / 出错时保留队列,交由用户处置
|
|
247
|
+
const shouldResume = finishedNaturally || resumeAfterAbort
|
|
248
|
+
finishedNaturally = false
|
|
249
|
+
resumeAfterAbort = false
|
|
250
|
+
if (shouldResume) drainQueue()
|
|
127
251
|
}
|
|
128
252
|
}
|
|
129
253
|
|
|
130
254
|
/** 单轮生成;存在工具调用时执行后递归进入下一轮(递归避免 await-in-loop) */
|
|
131
|
-
const runRound = async (controller: AbortController): Promise<void> => {
|
|
255
|
+
const runRound = async (controller: AbortController, depth: number): Promise<void> => {
|
|
132
256
|
if (!props.transport) return
|
|
133
257
|
const { signal } = controller
|
|
258
|
+
const tools = resolveTools(props.tools)
|
|
134
259
|
|
|
135
260
|
const assistant = reactive<ChatMessage>({
|
|
136
261
|
id: uid(),
|
|
@@ -149,7 +274,9 @@ export function useChat(options: UseChatOptions) {
|
|
|
149
274
|
// 排除刚追加的 assistant 占位消息
|
|
150
275
|
messages: messages.value.slice(0, -1),
|
|
151
276
|
systemPrompt: props.systemPrompt,
|
|
152
|
-
tools
|
|
277
|
+
tools,
|
|
278
|
+
model: model.value,
|
|
279
|
+
reasoningLevel: reasoningLevel.value,
|
|
153
280
|
signal
|
|
154
281
|
},
|
|
155
282
|
{
|
|
@@ -188,21 +315,42 @@ export function useChat(options: UseChatOptions) {
|
|
|
188
315
|
|
|
189
316
|
const toolCalls = assistant.toolCalls ?? []
|
|
190
317
|
if (!toolCalls.length) {
|
|
191
|
-
|
|
318
|
+
completeAsFinish(assistant)
|
|
192
319
|
return
|
|
193
320
|
}
|
|
194
321
|
|
|
195
322
|
// 工具串行执行(保持结果消息与调用顺序一致),用 Promise 链避免 await-in-loop
|
|
196
323
|
await toolCalls.reduce<Promise<void>>(
|
|
197
324
|
(prev, toolCall) =>
|
|
198
|
-
prev.then(() => (signal.aborted ? undefined : executeToolCall(toolCall, signal))),
|
|
325
|
+
prev.then(() => (signal.aborted ? undefined : executeToolCall(toolCall, signal, tools))),
|
|
199
326
|
Promise.resolve()
|
|
200
327
|
)
|
|
201
328
|
snapshot()
|
|
202
329
|
|
|
203
330
|
if (signal.aborted) return
|
|
204
331
|
|
|
205
|
-
|
|
332
|
+
// 终结工具执行成功:工具 UI 即最终答复,不再回灌模型生成文字
|
|
333
|
+
const hitTerminal = toolCalls.some(
|
|
334
|
+
(call) => call.status === 'success' && tools.some((t) => t.name === call.name && t.terminal)
|
|
335
|
+
)
|
|
336
|
+
if (hitTerminal) {
|
|
337
|
+
completeAsFinish(assistant)
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 达到最大轮次上限:停止继续请求,防止模型失控循环调用工具
|
|
342
|
+
if (depth + 1 >= (props.maxToolRounds ?? 10)) {
|
|
343
|
+
completeAsFinish(assistant)
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return runRound(controller, depth + 1)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** 标记本轮对话自然完成(收尾后据此自动消耗待发送队列) */
|
|
351
|
+
const completeAsFinish = (assistant: ChatMessage) => {
|
|
352
|
+
finishedNaturally = true
|
|
353
|
+
emit('finish', assistant)
|
|
206
354
|
}
|
|
207
355
|
|
|
208
356
|
/** 中断当前生成,挂起的工具确认按拒绝处理 */
|
|
@@ -234,12 +382,27 @@ export function useChat(options: UseChatOptions) {
|
|
|
234
382
|
void runConversation()
|
|
235
383
|
}
|
|
236
384
|
|
|
237
|
-
/**
|
|
385
|
+
/** 清空消息与待发送队列,生成中则先中断 */
|
|
238
386
|
const clear = () => {
|
|
239
387
|
abort()
|
|
388
|
+
queue.value = []
|
|
240
389
|
messages.value = []
|
|
241
390
|
snapshot()
|
|
242
391
|
}
|
|
243
392
|
|
|
244
|
-
return {
|
|
393
|
+
return {
|
|
394
|
+
messages,
|
|
395
|
+
model,
|
|
396
|
+
reasoningLevel,
|
|
397
|
+
running,
|
|
398
|
+
queue,
|
|
399
|
+
send,
|
|
400
|
+
abort,
|
|
401
|
+
regenerate,
|
|
402
|
+
clear,
|
|
403
|
+
respondToolCall,
|
|
404
|
+
enqueue,
|
|
405
|
+
startQueued,
|
|
406
|
+
removeQueued
|
|
407
|
+
}
|
|
245
408
|
}
|