@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.
Files changed (87) hide show
  1. package/dist/chat/index.d.ts +3 -3
  2. package/dist/chat/transports/index.d.ts +2 -2
  3. package/dist/chat/transports/openai.d.ts +16 -12
  4. package/dist/chat/transports/openai.js +56 -15
  5. package/dist/chat/transports/openai.js.map +1 -1
  6. package/dist/chat/types.d.ts +41 -2
  7. package/dist/chat/use-chat.d.ts +26 -1
  8. package/dist/chat/use-chat.js +132 -13
  9. package/dist/chat/use-chat.js.map +1 -1
  10. package/dist/components/ai-chat/ai-chat.js +222 -41
  11. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  12. package/dist/components/ai-chat/ask-question.js +2 -2
  13. package/dist/components/ai-chat/ask-question.js.map +1 -1
  14. package/dist/components/ai-chat/chat-input.js +81 -44
  15. package/dist/components/ai-chat/chat-input.js.map +1 -1
  16. package/dist/components/ai-chat/di.js.map +1 -1
  17. package/dist/components/ai-chat/message-item.js +101 -23
  18. package/dist/components/ai-chat/message-item.js.map +1 -1
  19. package/dist/components/ai-chat/message-list.js +217 -32
  20. package/dist/components/ai-chat/message-list.js.map +1 -1
  21. package/dist/components/ai-chat/model-picker.js +140 -0
  22. package/dist/components/ai-chat/model-picker.js.map +1 -0
  23. package/dist/components/ai-chat/queue-list.js +70 -0
  24. package/dist/components/ai-chat/queue-list.js.map +1 -0
  25. package/dist/components/ai-chat/side-panel.js +96 -0
  26. package/dist/components/ai-chat/side-panel.js.map +1 -0
  27. package/dist/components/ai-chat/style.d.ts +6 -0
  28. package/dist/components/ai-chat/style.js +6 -0
  29. package/dist/components/ai-chat/style2.css +552 -41
  30. package/dist/components/ai-chat/tool-call.js +61 -27
  31. package/dist/components/ai-chat/tool-call.js.map +1 -1
  32. package/dist/components/ai-orb/ai-orb.js +94 -0
  33. package/dist/components/ai-orb/ai-orb.js.map +1 -0
  34. package/dist/components/ai-orb/orb-renderer.js +411 -0
  35. package/dist/components/ai-orb/orb-renderer.js.map +1 -0
  36. package/dist/components/ai-orb/style.d.ts +1 -0
  37. package/dist/components/ai-orb/style.js +1 -0
  38. package/dist/components/ai-orb/style2.css +23 -0
  39. package/dist/index.d.ts +8 -6
  40. package/dist/index.js +2 -2
  41. package/dist/providers/types.d.ts +47 -0
  42. package/dist/style.js +1 -0
  43. package/dist/tools/ask-question/ask-question.d.ts +2 -17
  44. package/dist/tools/ask-question/index.d.ts +2 -2
  45. package/dist/tools/index.d.ts +3 -2
  46. package/dist/tools/index.js +10 -0
  47. package/dist/tools/index.js.map +1 -0
  48. package/dist/types/ai-chat.d.ts +35 -7
  49. package/dist/types/ai-orb.d.ts +27 -0
  50. package/dist/types/index.d.ts +2 -1
  51. package/package.json +8 -8
  52. package/src/chat/__test__/use-chat.test.ts +295 -9
  53. package/src/chat/transports/__test__/openai.test.ts +317 -0
  54. package/src/chat/transports/openai.ts +88 -26
  55. package/src/chat/types.ts +41 -1
  56. package/src/chat/use-chat.ts +177 -14
  57. package/src/components/ai-chat/__test__/ai-chat.test.ts +690 -12
  58. package/src/components/ai-chat/ai-chat.vue +241 -31
  59. package/src/components/ai-chat/chat-input.vue +78 -31
  60. package/src/components/ai-chat/di.ts +2 -0
  61. package/src/components/ai-chat/message-item.vue +98 -11
  62. package/src/components/ai-chat/message-list.vue +261 -21
  63. package/src/components/ai-chat/model-picker.vue +177 -0
  64. package/src/components/ai-chat/queue-list.vue +63 -0
  65. package/src/components/ai-chat/side-panel.vue +76 -0
  66. package/src/components/ai-chat/style.scss +621 -43
  67. package/src/components/ai-chat/style.ts +6 -0
  68. package/src/components/ai-chat/tool-call.vue +86 -42
  69. package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
  70. package/src/components/ai-orb/ai-orb.vue +107 -0
  71. package/src/components/ai-orb/index.ts +1 -0
  72. package/src/components/ai-orb/orb-renderer.ts +505 -0
  73. package/src/components/ai-orb/style.scss +10 -0
  74. package/src/components/ai-orb/style.ts +1 -0
  75. package/src/components/index.ts +1 -0
  76. package/src/index.ts +8 -1
  77. package/src/providers/index.ts +1 -0
  78. package/src/providers/types.ts +46 -0
  79. package/src/style.ts +1 -0
  80. package/src/tools/ask-question/__test__/ask-question.test.ts +4 -4
  81. package/src/tools/index.ts +14 -1
  82. package/src/types/ai-chat.ts +35 -6
  83. package/src/types/ai-orb.ts +27 -0
  84. package/src/types/index.ts +1 -0
  85. package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
  86. package/dist/components/ai-chat/index.d.ts +0 -2
  87. package/dist/components/index.d.ts +0 -3
@@ -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 { ChatAttachment, ChatMessage, ChatToolCall } from './types'
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 (toolCall: ChatToolCall, signal: AbortSignal) => {
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 = props.tools?.find((t) => t.name === toolCall.name)
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: props.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
- emit('finish', assistant)
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
- return runRound(controller)
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 { messages, running, send, abort, regenerate, clear, respondToolCall }
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
  }