@veltra/ai 2.0.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 (70) hide show
  1. package/dist/chat/index.d.ts +2 -2
  2. package/dist/chat/transports/openai.js +1 -1
  3. package/dist/chat/transports/openai.js.map +1 -1
  4. package/dist/chat/types.d.ts +37 -2
  5. package/dist/chat/use-chat.d.ts +23 -1
  6. package/dist/chat/use-chat.js +83 -8
  7. package/dist/chat/use-chat.js.map +1 -1
  8. package/dist/components/ai-chat/ai-chat.js +207 -46
  9. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  10. package/dist/components/ai-chat/chat-input.js +37 -12
  11. package/dist/components/ai-chat/chat-input.js.map +1 -1
  12. package/dist/components/ai-chat/di.js.map +1 -1
  13. package/dist/components/ai-chat/message-item.js +101 -23
  14. package/dist/components/ai-chat/message-item.js.map +1 -1
  15. package/dist/components/ai-chat/message-list.js +217 -32
  16. package/dist/components/ai-chat/message-list.js.map +1 -1
  17. package/dist/components/ai-chat/model-picker.js +69 -46
  18. package/dist/components/ai-chat/model-picker.js.map +1 -1
  19. package/dist/components/ai-chat/queue-list.js +70 -0
  20. package/dist/components/ai-chat/queue-list.js.map +1 -0
  21. package/dist/components/ai-chat/side-panel.js +96 -0
  22. package/dist/components/ai-chat/side-panel.js.map +1 -0
  23. package/dist/components/ai-chat/style.d.ts +4 -0
  24. package/dist/components/ai-chat/style.js +4 -0
  25. package/dist/components/ai-chat/style2.css +457 -74
  26. package/dist/components/ai-chat/tool-call.js +60 -26
  27. package/dist/components/ai-chat/tool-call.js.map +1 -1
  28. package/dist/components/ai-orb/ai-orb.js +94 -0
  29. package/dist/components/ai-orb/ai-orb.js.map +1 -0
  30. package/dist/components/ai-orb/orb-renderer.js +411 -0
  31. package/dist/components/ai-orb/orb-renderer.js.map +1 -0
  32. package/dist/components/ai-orb/style.d.ts +1 -0
  33. package/dist/components/ai-orb/style.js +1 -0
  34. package/dist/components/ai-orb/style2.css +23 -0
  35. package/dist/index.d.ts +4 -2
  36. package/dist/index.js +2 -1
  37. package/dist/style.js +1 -0
  38. package/dist/types/ai-chat.d.ts +21 -7
  39. package/dist/types/ai-orb.d.ts +27 -0
  40. package/dist/types/index.d.ts +2 -1
  41. package/package.json +5 -5
  42. package/src/chat/__test__/use-chat.test.ts +224 -9
  43. package/src/chat/transports/__test__/openai.test.ts +78 -0
  44. package/src/chat/transports/openai.ts +2 -2
  45. package/src/chat/types.ts +37 -1
  46. package/src/chat/use-chat.ts +112 -9
  47. package/src/components/ai-chat/__test__/ai-chat.test.ts +588 -11
  48. package/src/components/ai-chat/ai-chat.vue +227 -31
  49. package/src/components/ai-chat/chat-input.vue +41 -11
  50. package/src/components/ai-chat/di.ts +2 -0
  51. package/src/components/ai-chat/message-item.vue +98 -11
  52. package/src/components/ai-chat/message-list.vue +261 -21
  53. package/src/components/ai-chat/model-picker.vue +110 -59
  54. package/src/components/ai-chat/queue-list.vue +63 -0
  55. package/src/components/ai-chat/side-panel.vue +76 -0
  56. package/src/components/ai-chat/style.scss +527 -74
  57. package/src/components/ai-chat/style.ts +4 -0
  58. package/src/components/ai-chat/tool-call.vue +86 -42
  59. package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
  60. package/src/components/ai-orb/ai-orb.vue +107 -0
  61. package/src/components/ai-orb/index.ts +1 -0
  62. package/src/components/ai-orb/orb-renderer.ts +505 -0
  63. package/src/components/ai-orb/style.scss +10 -0
  64. package/src/components/ai-orb/style.ts +1 -0
  65. package/src/components/index.ts +1 -0
  66. package/src/style.ts +1 -0
  67. package/src/tools/ask-question/__test__/ask-question.test.ts +1 -1
  68. package/src/types/ai-chat.ts +21 -6
  69. package/src/types/ai-orb.ts +27 -0
  70. package/src/types/index.ts +1 -0
@@ -1,5 +1,5 @@
1
- import { ChatAttachment, ChatMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./types.js";
1
+ import { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./types.js";
2
2
  import { UseChatOptions, useChat } from "./use-chat.js";
3
3
  import { OpenAITransport, OpenAITransportOptions, createOpenAITransport } from "./transports/openai.js";
4
4
  import "./transports/index.js";
5
- export { ChatAttachment, ChatMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, UseChatOptions, createOpenAITransport, useChat };
5
+ export { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, UseChatOptions, createOpenAITransport, useChat };
@@ -88,7 +88,7 @@ async function parseSSE(body, signal, handlers) {
88
88
  if (typeof reasoning === "string" && reasoning) handlers.onReasoningDelta?.(reasoning);
89
89
  if (Array.isArray(delta.tool_calls)) for (const tc of delta.tool_calls) {
90
90
  const index = tc.index ?? 0;
91
- if (index <= lastIndex && accumulated.size > 0) flushToolCalls();
91
+ if (index > lastIndex && accumulated.size > 0) flushToolCalls();
92
92
  lastIndex = Math.max(lastIndex, index);
93
93
  let call = accumulated.get(index);
94
94
  if (!call) {
@@ -1 +1 @@
1
- {"version":3,"file":"openai.js","names":[],"sources":["../../../src/chat/transports/openai.ts"],"sourcesContent":["import type { ChatModelOption, ChatProvider } from '../../providers'\nimport type {\n ChatMessage,\n ChatTool,\n ChatTransport,\n ChatTransportHandlers,\n ChatTransportRequest\n} from '../types'\n\n/** createOpenAITransport 的配置项(多 Provider) */\nexport interface OpenAITransportOptions {\n /** 至少一个 Provider;模型 id 须跨 Provider 全局唯一 */\n providers: ChatProvider[]\n /** 全局额外请求头(与各 Provider headers 合并,Provider 优先) */\n headers?: Record<string, string>\n /** 全局额外请求体字段,如 temperature、top_p */\n body?: Record<string, unknown>\n}\n\n/** 带扁平模型列表元数据的 OpenAI 兼容 transport */\nexport type OpenAITransport = ChatTransport & {\n /** 供 UI 使用的扁平模型列表 */\n readonly models: ChatModelOption[]\n /** 默认模型 id(首个 Provider 的首个模型) */\n readonly defaultModel: string\n}\n\ninterface OpenAIMessage {\n role: string\n content?: unknown\n tool_calls?: unknown[]\n tool_call_id?: string\n}\n\nfunction toOpenAIMessages(messages: ChatMessage[], systemPrompt?: string): OpenAIMessage[] {\n const result: OpenAIMessage[] = []\n\n if (systemPrompt) {\n result.push({ role: 'system', content: systemPrompt })\n }\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n if (msg.attachments?.length) {\n const parts: unknown[] = []\n if (msg.content) parts.push({ type: 'text', text: msg.content })\n for (const att of msg.attachments) {\n parts.push({ type: 'image_url', image_url: { url: att.dataUrl } })\n }\n result.push({ role: 'user', content: parts })\n } else {\n result.push({ role: 'user', content: msg.content })\n }\n } else if (msg.role === 'assistant') {\n const item: OpenAIMessage = { role: 'assistant', content: msg.content || '' }\n if (msg.toolCalls?.length) {\n item.tool_calls = msg.toolCalls.map((call) => ({\n id: call.id,\n type: 'function',\n function: { name: call.name, arguments: call.arguments }\n }))\n }\n result.push(item)\n } else if (msg.role === 'tool') {\n result.push({ role: 'tool', tool_call_id: msg.toolCallId, content: msg.content })\n }\n }\n\n return result\n}\n\nfunction toOpenAITools(tools: ChatTool[]): unknown[] {\n return tools.map((tool) => ({\n type: 'function',\n function: { name: tool.name, description: tool.description, parameters: tool.parameters }\n }))\n}\n\ninterface AccumulatedToolCall {\n id: string\n name: string\n arguments: string\n}\n\n/**\n * 逐行解析 SSE 流。\n * 兼容 OpenAI 及 DeepSeek 等 reasoning_content 风格的兼容端点。\n */\nasync function parseSSE(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n handlers: ChatTransportHandlers\n): Promise<void> {\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n\n // 按 OpenAI 流式协议中 tool_calls 的 index 累积参数片段\n const accumulated = new Map<number, AccumulatedToolCall>()\n let lastIndex = -1\n\n const flushToolCalls = () => {\n if (!handlers.onToolCall) return\n for (const call of accumulated.values()) {\n if (call.id && call.name) {\n handlers.onToolCall({ id: call.id, name: call.name, arguments: call.arguments })\n }\n }\n accumulated.clear()\n }\n\n const handleData = (data: string) => {\n if (data === '[DONE]') return\n\n let chunk: any\n try {\n chunk = JSON.parse(data)\n } catch {\n return\n }\n\n const delta = chunk.choices?.[0]?.delta\n if (!delta) return\n\n if (typeof delta.content === 'string' && delta.content) {\n handlers.onTextDelta(delta.content)\n }\n\n // 兼容 reasoning_content(DeepSeek)与 reasoning(部分 OpenAI 兼容端点)\n const reasoning = delta.reasoning_content ?? delta.reasoning\n if (typeof reasoning === 'string' && reasoning) {\n handlers.onReasoningDelta?.(reasoning)\n }\n\n if (Array.isArray(delta.tool_calls)) {\n for (const tc of delta.tool_calls) {\n const index: number = tc.index ?? 0\n // index 回退说明新的一轮 tool_calls 开始,先把已累积的抛出去\n if (index <= lastIndex && accumulated.size > 0) flushToolCalls()\n lastIndex = Math.max(lastIndex, index)\n\n let call = accumulated.get(index)\n if (!call) {\n call = { id: '', name: '', arguments: '' }\n accumulated.set(index, call)\n }\n if (tc.id) call.id = tc.id\n if (tc.function?.name) call.name += tc.function.name\n if (tc.function?.arguments) call.arguments += tc.function.arguments\n }\n }\n }\n\n try {\n // 递归泵取代替 while 循环,避免 await-in-loop\n const pump = async (): Promise<void> => {\n const { done, value } = await reader.read()\n if (done || signal.aborted) return\n\n buffer += decoder.decode(value, { stream: true })\n\n let newlineIndex: number\n while ((newlineIndex = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newlineIndex).trim()\n buffer = buffer.slice(newlineIndex + 1)\n if (line.startsWith('data:')) {\n handleData(line.slice(5).trim())\n }\n }\n\n return pump()\n }\n\n await pump()\n\n if (buffer.trim().startsWith('data:')) {\n handleData(buffer.trim().slice(5).trim())\n }\n } finally {\n reader.releaseLock()\n }\n\n flushToolCalls()\n}\n\n/** 校验 providers 并展开扁平模型列表 */\nfunction flattenModels(providers: ChatProvider[]): {\n models: ChatModelOption[]\n byModelId: Map<string, ChatProvider>\n} {\n if (!providers.length) {\n throw new Error('[createOpenAITransport] providers 不能为空')\n }\n\n const models: ChatModelOption[] = []\n const byModelId = new Map<string, ChatProvider>()\n\n for (const provider of providers) {\n if (!provider.models?.length) {\n throw new Error(`[createOpenAITransport] Provider \"${provider.id}\" 未配置 models`)\n }\n for (const model of provider.models) {\n if (byModelId.has(model.id)) {\n throw new Error(\n `[createOpenAITransport] 模型 id \"${model.id}\" 重复(跨 Provider 须全局唯一)`\n )\n }\n byModelId.set(model.id, provider)\n models.push({ ...model, providerId: provider.id, providerLabel: provider.label })\n }\n }\n\n return { models, byModelId }\n}\n\n/** 缺省:写入 OpenAI 兼容的 reasoning_effort */\nfunction defaultApplyReasoning(level: string, body: Record<string, unknown>) {\n body.reasoning_effort = level\n}\n\n/**\n * 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。\n * 按 request.model 路由到对应 Provider 的 endpoint;手写 fetch + SSE,无第三方依赖。\n */\nexport function createOpenAITransport(options: OpenAITransportOptions): OpenAITransport {\n const { providers, headers: globalHeaders, body: globalBody } = options\n const { models, byModelId } = flattenModels(providers)\n const defaultModel = models[0]!.id\n\n const send: ChatTransport = async (\n request: ChatTransportRequest,\n handlers: ChatTransportHandlers\n ) => {\n const { messages, systemPrompt, tools, signal, reasoningLevel } = request\n const modelId = request.model ?? defaultModel\n const provider = byModelId.get(modelId)\n\n if (!provider) {\n handlers.onError?.(new Error(`未找到模型 \"${modelId}\" 对应的 Provider`))\n return\n }\n\n const requestBody: Record<string, unknown> = {\n ...globalBody,\n model: modelId,\n stream: true,\n messages: toOpenAIMessages(messages, systemPrompt),\n ...(tools?.length ? { tools: toOpenAITools(tools) } : {})\n }\n\n if (reasoningLevel) {\n const apply = provider.applyReasoning ?? defaultApplyReasoning\n apply(reasoningLevel, requestBody)\n }\n\n let response: Response\n try {\n response = await fetch(provider.endpoint, {\n method: 'POST',\n signal,\n headers: {\n 'Content-Type': 'application/json',\n ...(provider.apiKey ? { Authorization: `Bearer ${provider.apiKey}` } : {}),\n ...globalHeaders,\n ...provider.headers\n },\n body: JSON.stringify(requestBody)\n })\n } catch (error) {\n if (!signal.aborted) {\n handlers.onError?.(error instanceof Error ? error : new Error(String(error)))\n }\n return\n }\n\n if (!response.ok) {\n const text = await response.text().catch(() => '')\n handlers.onError?.(\n new Error(`请求失败(${response.status}):${text || response.statusText}`)\n )\n return\n }\n\n if (!response.body) {\n handlers.onError?.(new Error('响应不包含可读取的流'))\n return\n }\n\n try {\n await parseSSE(response.body, signal, handlers)\n } catch (error) {\n if (!signal.aborted) {\n handlers.onError?.(error instanceof Error ? error : new Error(String(error)))\n }\n }\n }\n\n return Object.assign(send, { models, defaultModel })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"openai.js","names":[],"sources":["../../../src/chat/transports/openai.ts"],"sourcesContent":["import type { ChatModelOption, ChatProvider } from '../../providers'\nimport type {\n ChatMessage,\n ChatTool,\n ChatTransport,\n ChatTransportHandlers,\n ChatTransportRequest\n} from '../types'\n\n/** createOpenAITransport 的配置项(多 Provider) */\nexport interface OpenAITransportOptions {\n /** 至少一个 Provider;模型 id 须跨 Provider 全局唯一 */\n providers: ChatProvider[]\n /** 全局额外请求头(与各 Provider headers 合并,Provider 优先) */\n headers?: Record<string, string>\n /** 全局额外请求体字段,如 temperature、top_p */\n body?: Record<string, unknown>\n}\n\n/** 带扁平模型列表元数据的 OpenAI 兼容 transport */\nexport type OpenAITransport = ChatTransport & {\n /** 供 UI 使用的扁平模型列表 */\n readonly models: ChatModelOption[]\n /** 默认模型 id(首个 Provider 的首个模型) */\n readonly defaultModel: string\n}\n\ninterface OpenAIMessage {\n role: string\n content?: unknown\n tool_calls?: unknown[]\n tool_call_id?: string\n}\n\nfunction toOpenAIMessages(messages: ChatMessage[], systemPrompt?: string): OpenAIMessage[] {\n const result: OpenAIMessage[] = []\n\n if (systemPrompt) {\n result.push({ role: 'system', content: systemPrompt })\n }\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n if (msg.attachments?.length) {\n const parts: unknown[] = []\n if (msg.content) parts.push({ type: 'text', text: msg.content })\n for (const att of msg.attachments) {\n parts.push({ type: 'image_url', image_url: { url: att.dataUrl } })\n }\n result.push({ role: 'user', content: parts })\n } else {\n result.push({ role: 'user', content: msg.content })\n }\n } else if (msg.role === 'assistant') {\n const item: OpenAIMessage = { role: 'assistant', content: msg.content || '' }\n if (msg.toolCalls?.length) {\n item.tool_calls = msg.toolCalls.map((call) => ({\n id: call.id,\n type: 'function',\n function: { name: call.name, arguments: call.arguments }\n }))\n }\n result.push(item)\n } else if (msg.role === 'tool') {\n result.push({ role: 'tool', tool_call_id: msg.toolCallId, content: msg.content })\n }\n }\n\n return result\n}\n\nfunction toOpenAITools(tools: ChatTool[]): unknown[] {\n return tools.map((tool) => ({\n type: 'function',\n function: { name: tool.name, description: tool.description, parameters: tool.parameters }\n }))\n}\n\ninterface AccumulatedToolCall {\n id: string\n name: string\n arguments: string\n}\n\n/**\n * 逐行解析 SSE 流。\n * 兼容 OpenAI 及 DeepSeek 等 reasoning_content 风格的兼容端点。\n */\nasync function parseSSE(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n handlers: ChatTransportHandlers\n): Promise<void> {\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n\n // 按 OpenAI 流式协议中 tool_calls 的 index 累积参数片段\n const accumulated = new Map<number, AccumulatedToolCall>()\n let lastIndex = -1\n\n const flushToolCalls = () => {\n if (!handlers.onToolCall) return\n for (const call of accumulated.values()) {\n if (call.id && call.name) {\n handlers.onToolCall({ id: call.id, name: call.name, arguments: call.arguments })\n }\n }\n accumulated.clear()\n }\n\n const handleData = (data: string) => {\n if (data === '[DONE]') return\n\n let chunk: any\n try {\n chunk = JSON.parse(data)\n } catch {\n return\n }\n\n const delta = chunk.choices?.[0]?.delta\n if (!delta) return\n\n if (typeof delta.content === 'string' && delta.content) {\n handlers.onTextDelta(delta.content)\n }\n\n // 兼容 reasoning_content(DeepSeek)与 reasoning(部分 OpenAI 兼容端点)\n const reasoning = delta.reasoning_content ?? delta.reasoning\n if (typeof reasoning === 'string' && reasoning) {\n handlers.onReasoningDelta?.(reasoning)\n }\n\n if (Array.isArray(delta.tool_calls)) {\n for (const tc of delta.tool_calls) {\n const index: number = tc.index ?? 0\n // 同一调用的参数分片共享 index;index 变大才说明前一个调用已完整,先抛出去\n if (index > lastIndex && accumulated.size > 0) flushToolCalls()\n lastIndex = Math.max(lastIndex, index)\n\n let call = accumulated.get(index)\n if (!call) {\n call = { id: '', name: '', arguments: '' }\n accumulated.set(index, call)\n }\n if (tc.id) call.id = tc.id\n if (tc.function?.name) call.name += tc.function.name\n if (tc.function?.arguments) call.arguments += tc.function.arguments\n }\n }\n }\n\n try {\n // 递归泵取代替 while 循环,避免 await-in-loop\n const pump = async (): Promise<void> => {\n const { done, value } = await reader.read()\n if (done || signal.aborted) return\n\n buffer += decoder.decode(value, { stream: true })\n\n let newlineIndex: number\n while ((newlineIndex = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newlineIndex).trim()\n buffer = buffer.slice(newlineIndex + 1)\n if (line.startsWith('data:')) {\n handleData(line.slice(5).trim())\n }\n }\n\n return pump()\n }\n\n await pump()\n\n if (buffer.trim().startsWith('data:')) {\n handleData(buffer.trim().slice(5).trim())\n }\n } finally {\n reader.releaseLock()\n }\n\n flushToolCalls()\n}\n\n/** 校验 providers 并展开扁平模型列表 */\nfunction flattenModels(providers: ChatProvider[]): {\n models: ChatModelOption[]\n byModelId: Map<string, ChatProvider>\n} {\n if (!providers.length) {\n throw new Error('[createOpenAITransport] providers 不能为空')\n }\n\n const models: ChatModelOption[] = []\n const byModelId = new Map<string, ChatProvider>()\n\n for (const provider of providers) {\n if (!provider.models?.length) {\n throw new Error(`[createOpenAITransport] Provider \"${provider.id}\" 未配置 models`)\n }\n for (const model of provider.models) {\n if (byModelId.has(model.id)) {\n throw new Error(\n `[createOpenAITransport] 模型 id \"${model.id}\" 重复(跨 Provider 须全局唯一)`\n )\n }\n byModelId.set(model.id, provider)\n models.push({ ...model, providerId: provider.id, providerLabel: provider.label })\n }\n }\n\n return { models, byModelId }\n}\n\n/** 缺省:写入 OpenAI 兼容的 reasoning_effort */\nfunction defaultApplyReasoning(level: string, body: Record<string, unknown>) {\n body.reasoning_effort = level\n}\n\n/**\n * 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。\n * 按 request.model 路由到对应 Provider 的 endpoint;手写 fetch + SSE,无第三方依赖。\n */\nexport function createOpenAITransport(options: OpenAITransportOptions): OpenAITransport {\n const { providers, headers: globalHeaders, body: globalBody } = options\n const { models, byModelId } = flattenModels(providers)\n const defaultModel = models[0]!.id\n\n const send: ChatTransport = async (\n request: ChatTransportRequest,\n handlers: ChatTransportHandlers\n ) => {\n const { messages, systemPrompt, tools, signal, reasoningLevel } = request\n const modelId = request.model ?? defaultModel\n const provider = byModelId.get(modelId)\n\n if (!provider) {\n handlers.onError?.(new Error(`未找到模型 \"${modelId}\" 对应的 Provider`))\n return\n }\n\n const requestBody: Record<string, unknown> = {\n ...globalBody,\n model: modelId,\n stream: true,\n messages: toOpenAIMessages(messages, systemPrompt),\n ...(tools?.length ? { tools: toOpenAITools(tools) } : {})\n }\n\n if (reasoningLevel) {\n const apply = provider.applyReasoning ?? defaultApplyReasoning\n apply(reasoningLevel, requestBody)\n }\n\n let response: Response\n try {\n response = await fetch(provider.endpoint, {\n method: 'POST',\n signal,\n headers: {\n 'Content-Type': 'application/json',\n ...(provider.apiKey ? { Authorization: `Bearer ${provider.apiKey}` } : {}),\n ...globalHeaders,\n ...provider.headers\n },\n body: JSON.stringify(requestBody)\n })\n } catch (error) {\n if (!signal.aborted) {\n handlers.onError?.(error instanceof Error ? error : new Error(String(error)))\n }\n return\n }\n\n if (!response.ok) {\n const text = await response.text().catch(() => '')\n handlers.onError?.(\n new Error(`请求失败(${response.status}):${text || response.statusText}`)\n )\n return\n }\n\n if (!response.body) {\n handlers.onError?.(new Error('响应不包含可读取的流'))\n return\n }\n\n try {\n await parseSSE(response.body, signal, handlers)\n } catch (error) {\n if (!signal.aborted) {\n handlers.onError?.(error instanceof Error ? error : new Error(String(error)))\n }\n }\n }\n\n return Object.assign(send, { models, defaultModel })\n}\n"],"mappings":"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"}
@@ -47,6 +47,15 @@ interface ChatMessage {
47
47
  /** 消息状态,仅 assistant 消息使用 */
48
48
  status?: 'streaming' | 'done' | 'error' | 'aborted';
49
49
  }
50
+ /** 队列中的待发送消息(会话进行中提交的消息按序排队) */
51
+ interface ChatQueuedMessage {
52
+ /** 队列项 id */
53
+ id: string;
54
+ /** 消息内容 */
55
+ content: string;
56
+ /** 携带的附件 */
57
+ attachments?: ChatAttachment[];
58
+ }
50
59
  /** 工具执行上下文 */
51
60
  interface ChatToolContext {
52
61
  /** 本次工具调用 */
@@ -74,10 +83,36 @@ interface ChatTool<A = any> {
74
83
  /**
75
84
  * 自定义工具卡片内容渲染(组件或渲染函数),props 为 ChatToolRenderProps。
76
85
  * 设置后替换卡片 body 的默认参数/结果展示;优先级高于 tool-<name> 插槽。
86
+ * renderTo 为 'panel' 时改为在右侧侧边面板中渲染。
77
87
  */
78
88
  render?: Component;
79
- /** 执行完成后是否自动折叠。缺省:设置了 render 时为 false,否则为 true */
89
+ /**
90
+ * 渲染位置。缺省 'inline':render 组件展示在会话内的工具卡片中;
91
+ * 'panel':render 组件展示在对话区右侧的侧边面板(新调用自动打开面板,
92
+ * 工具卡片内仅保留「查看面板」入口,面板与会话区宽度可拖拽调节)。
93
+ * 适合打开后台页面、表单、图表、列表等需要较大交互区域的工具。
94
+ */
95
+ renderTo?: 'inline' | 'panel';
96
+ /**
97
+ * 侧边面板默认宽度(px,最小 320),仅 renderTo: 'panel' 时生效。
98
+ * 聚焦到该工具的调用时应用;缺省时面板打开取「容器宽 - 860」(即默认尽可能大,
99
+ * 给会话区保留 860px);面板已打开时切换聚焦保持当前宽度(含用户拖拽结果)。
100
+ */
101
+ panelWidth?: number;
102
+ /**
103
+ * 侧边面板标题,仅 renderTo: 'panel' 时生效。
104
+ * 面板标题通常是「业务对象 + 动作」(如「编辑用户 · 张三」)而非工具名,
105
+ * 可传固定字符串,或传函数按本次调用的参数动态生成。缺省取 label ?? name。
106
+ */
107
+ panelTitle?: string | ((toolCall: ChatToolCall) => string);
108
+ /** 执行完成后是否自动折叠。缺省:renderTo 为 'panel' 时为 true;否则设置了 render 时为 false,否则为 true */
80
109
  autoCollapse?: boolean;
110
+ /**
111
+ * 终结工具:执行成功后对话即结束,结果不再回灌模型生成额外文字(工具 UI 即最终答复)。
112
+ * 适合天气卡片这类"UI 即答案"的工具,需配合 render 或 tool-<name> 插槽提供完整结果 UI。
113
+ * 工具结果仍会记录到消息历史供后续轮次使用;执行失败/被拒绝时错误照常回灌模型。
114
+ */
115
+ terminal?: boolean;
81
116
  /** 工具实现,返回值(或 Promise 返回值)会被 JSON 序列化后回灌给模型 */
82
117
  execute: (args: A, ctx: ChatToolContext) => unknown;
83
118
  }
@@ -125,5 +160,5 @@ interface ChatTransportHandlers {
125
160
  */
126
161
  type ChatTransport = (request: ChatTransportRequest, handlers: ChatTransportHandlers) => Promise<void> | void;
127
162
  //#endregion
128
- export { ChatAttachment, ChatMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus };
163
+ export { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus };
129
164
  //# sourceMappingURL=types.d.ts.map
@@ -1,4 +1,4 @@
1
- import { ChatAttachment, ChatMessage } from "./types.js";
1
+ import { ChatAttachment, ChatMessage, ChatQueuedMessage } from "./types.js";
2
2
  import { AiChatEmits, AiChatProps } from "../types/ai-chat.js";
3
3
  import "../types/index.js";
4
4
  import { Ref } from "vue";
@@ -17,11 +17,33 @@ declare function useChat(options: UseChatOptions): {
17
17
  model: Ref<string | undefined, string | undefined>;
18
18
  reasoningLevel: Ref<string | undefined, string | undefined>;
19
19
  running: Ref<boolean, boolean>;
20
+ queue: Ref<{
21
+ id: string;
22
+ content: string;
23
+ attachments?: {
24
+ name: string;
25
+ mimeType: string;
26
+ size: number;
27
+ dataUrl: string;
28
+ }[] | undefined;
29
+ }[], ChatQueuedMessage[] | {
30
+ id: string;
31
+ content: string;
32
+ attachments?: {
33
+ name: string;
34
+ mimeType: string;
35
+ size: number;
36
+ dataUrl: string;
37
+ }[] | undefined;
38
+ }[]>;
20
39
  send: (content: string, attachments?: ChatAttachment[]) => void;
21
40
  abort: () => void;
22
41
  regenerate: () => void;
23
42
  clear: () => void;
24
43
  respondToolCall: (toolCallId: string, approved: boolean) => void;
44
+ enqueue: (content: string, attachments?: ChatAttachment[], beforeId?: string) => ChatQueuedMessage;
45
+ startQueued: (id: string) => void;
46
+ removeQueued: (id: string) => ChatQueuedMessage | undefined;
25
47
  };
26
48
  //#endregion
27
49
  export { UseChatOptions, useChat };
@@ -47,9 +47,15 @@ function useChat(options) {
47
47
  });
48
48
  /** 是否正在生成中(含工具执行与多轮循环) */
49
49
  const running = ref(false);
50
+ /** 待发送队列:会话进行中提交的新消息按序排队,收尾后先进先出自动接续 */
51
+ const queue = ref([]);
50
52
  let abortController = null;
51
53
  /** needsConfirm 工具的挂起确认器,key 为 toolCallId */
52
54
  const confirmResolvers = /* @__PURE__ */ new Map();
55
+ /** 当前会话是否自然完成(finish);决定收尾后是否自动消耗队列 */
56
+ let finishedNaturally = false;
57
+ /** startQueued 触发的中断需要在收尾后立即接续队首 */
58
+ let resumeAfterAbort = false;
53
59
  /** 按当前模型校正推理等级(无 levels 则清空;值不合法则落到默认/首项) */
54
60
  const syncReasoningForModel = (modelId) => {
55
61
  const option = props.models?.find((m) => m.id === modelId);
@@ -73,10 +79,56 @@ function useChat(options) {
73
79
  const snapshot = () => {
74
80
  emit("update:messages", messages.value.slice());
75
81
  };
76
- /** 发送一条用户消息并启动对话循环 */
82
+ /**
83
+ * 发送一条用户消息。会话进行中时进入待发送队列(不丢失),
84
+ * 会话自然结束后按 FIFO 自动接续;空闲时立即开始新会话。
85
+ */
77
86
  const send = (content, attachments) => {
78
- if (running.value) return;
79
87
  if (!content.trim() && !attachments?.length) return;
88
+ enqueue(content, attachments);
89
+ };
90
+ /**
91
+ * 向队列插入一条消息;beforeId 指定插到某条之前(缺省追加到尾部)。
92
+ * 空闲时立即消耗队首(保持先来先发的合理顺序)。
93
+ */
94
+ const enqueue = (content, attachments, beforeId) => {
95
+ const item = {
96
+ id: uid(),
97
+ content,
98
+ attachments
99
+ };
100
+ const anchorIndex = beforeId ? queue.value.findIndex((q) => q.id === beforeId) : -1;
101
+ if (anchorIndex === -1) queue.value.push(item);
102
+ else queue.value.splice(anchorIndex, 0, item);
103
+ if (!running.value) drainQueue();
104
+ return item;
105
+ };
106
+ /** 消耗队首并开始新一轮会话 */
107
+ const drainQueue = () => {
108
+ const next = queue.value.shift();
109
+ if (next) startUserTurn(next.content, next.attachments);
110
+ };
111
+ /** 立即执行队列中的某条:中断当前会话,该条插队为下一条(其余保持原顺序) */
112
+ const startQueued = (id) => {
113
+ const item = removeQueued(id);
114
+ if (!item) return;
115
+ if (!running.value) {
116
+ startUserTurn(item.content, item.attachments);
117
+ return;
118
+ }
119
+ queue.value.unshift(item);
120
+ resumeAfterAbort = true;
121
+ abort();
122
+ };
123
+ /** 从队列移除某条(返回被移除项;编辑场景由 UI 取回内容) */
124
+ const removeQueued = (id) => {
125
+ const index = queue.value.findIndex((item) => item.id === id);
126
+ if (index === -1) return void 0;
127
+ const [item] = queue.value.splice(index, 1);
128
+ return item;
129
+ };
130
+ /** 推送用户消息并启动对话循环 */
131
+ const startUserTurn = (content, attachments) => {
80
132
  const message = reactive({
81
133
  id: uid(),
82
134
  role: "user",
@@ -138,18 +190,23 @@ function useChat(options) {
138
190
  const runConversation = async () => {
139
191
  if (!props.transport) return;
140
192
  running.value = true;
193
+ finishedNaturally = false;
141
194
  const controller = new AbortController();
142
195
  abortController = controller;
143
196
  try {
144
- await runRound(controller);
197
+ await runRound(controller, 0);
145
198
  } finally {
146
199
  running.value = false;
147
200
  abortController = null;
148
201
  snapshot();
202
+ const shouldResume = finishedNaturally || resumeAfterAbort;
203
+ finishedNaturally = false;
204
+ resumeAfterAbort = false;
205
+ if (shouldResume) drainQueue();
149
206
  }
150
207
  };
151
208
  /** 单轮生成;存在工具调用时执行后递归进入下一轮(递归避免 await-in-loop) */
152
- const runRound = async (controller) => {
209
+ const runRound = async (controller, depth) => {
153
210
  if (!props.transport) return;
154
211
  const { signal } = controller;
155
212
  const tools = resolveTools(props.tools);
@@ -204,13 +261,26 @@ function useChat(options) {
204
261
  assistant.status = "done";
205
262
  const toolCalls = assistant.toolCalls ?? [];
206
263
  if (!toolCalls.length) {
207
- emit("finish", assistant);
264
+ completeAsFinish(assistant);
208
265
  return;
209
266
  }
210
267
  await toolCalls.reduce((prev, toolCall) => prev.then(() => signal.aborted ? void 0 : executeToolCall(toolCall, signal, tools)), Promise.resolve());
211
268
  snapshot();
212
269
  if (signal.aborted) return;
213
- return runRound(controller);
270
+ if (toolCalls.some((call) => call.status === "success" && tools.some((t) => t.name === call.name && t.terminal))) {
271
+ completeAsFinish(assistant);
272
+ return;
273
+ }
274
+ if (depth + 1 >= (props.maxToolRounds ?? 10)) {
275
+ completeAsFinish(assistant);
276
+ return;
277
+ }
278
+ return runRound(controller, depth + 1);
279
+ };
280
+ /** 标记本轮对话自然完成(收尾后据此自动消耗待发送队列) */
281
+ const completeAsFinish = (assistant) => {
282
+ finishedNaturally = true;
283
+ emit("finish", assistant);
214
284
  };
215
285
  /** 中断当前生成,挂起的工具确认按拒绝处理 */
216
286
  const abort = () => {
@@ -234,9 +304,10 @@ function useChat(options) {
234
304
  snapshot();
235
305
  runConversation();
236
306
  };
237
- /** 清空消息,生成中则先中断 */
307
+ /** 清空消息与待发送队列,生成中则先中断 */
238
308
  const clear = () => {
239
309
  abort();
310
+ queue.value = [];
240
311
  messages.value = [];
241
312
  snapshot();
242
313
  };
@@ -245,11 +316,15 @@ function useChat(options) {
245
316
  model,
246
317
  reasoningLevel,
247
318
  running,
319
+ queue,
248
320
  send,
249
321
  abort,
250
322
  regenerate,
251
323
  clear,
252
- respondToolCall
324
+ respondToolCall,
325
+ enqueue,
326
+ startQueued,
327
+ removeQueued
253
328
  };
254
329
  }
255
330
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"use-chat.js","names":[],"sources":["../../src/chat/use-chat.ts"],"sourcesContent":["import { useModel } from '@veltra/compositions'\nimport { reactive, ref, watch, type Ref } from 'vue'\n\nimport { createBuiltinTools } from '../tools'\nimport type { AiChatEmits, AiChatProps } from '../types'\nimport type { ChatAttachment, ChatMessage, ChatTool, ChatToolCall } from './types'\n\nexport interface UseChatOptions {\n props: AiChatProps\n emit: AiChatEmits\n}\n\nconst uid = () => {\n return typeof crypto !== 'undefined' && crypto.randomUUID\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`\n}\n\nfunction serializeResult(result: unknown): string {\n if (typeof result === 'string') return result\n try {\n return JSON.stringify(result) ?? 'null'\n } catch {\n return String(result)\n }\n}\n\n/** 内置工具 + 用户工具,同名时内置优先 */\nfunction resolveTools(userTools?: ChatTool[]): ChatTool[] {\n const builtins = createBuiltinTools()\n const names = new Set(builtins.map((t) => t.name))\n return [...builtins, ...(userTools ?? []).filter((t) => !names.has(t.name))]\n}\n\n/**\n * AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。\n * 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。\n * 始终注入内置工具(如 askQuestion),无需经 props 传入。\n */\nexport function useChat(options: UseChatOptions) {\n const { props, emit } = options\n\n const messages = useModel({\n props,\n propName: 'messages',\n emit,\n local: true,\n defaultValue: []\n }) as Ref<ChatMessage[]>\n\n const model = useModel({ props, propName: 'model', emit, local: true }) as Ref<string | undefined>\n\n const reasoningLevel = useModel({ props, propName: 'reasoningLevel', emit, local: true }) as Ref<\n string | undefined\n >\n\n /** 是否正在生成中(含工具执行与多轮循环) */\n const running = ref(false)\n\n let abortController: AbortController | null = null\n /** needsConfirm 工具的挂起确认器,key 为 toolCallId */\n const confirmResolvers = new Map<string, (approved: boolean) => void>()\n\n /** 按当前模型校正推理等级(无 levels 则清空;值不合法则落到默认/首项) */\n const syncReasoningForModel = (modelId: string | undefined) => {\n const option = props.models?.find((m) => m.id === modelId)\n if (!option?.reasoningLevels?.length) {\n if (reasoningLevel.value !== undefined) {\n reasoningLevel.value = undefined\n }\n return\n }\n const levels = option.reasoningLevels\n if (reasoningLevel.value && levels.some((l) => l.value === reasoningLevel.value)) return\n reasoningLevel.value = option.defaultReasoningLevel ?? levels[0]?.value\n }\n\n // models / model 变化时:确保有合法选中模型,并校正推理等级\n watch(\n () => [props.models, model.value] as const,\n ([models, current]) => {\n if (!models?.length) return\n if (!current || !models.some((m) => m.id === current)) {\n model.value = models[0]!.id\n return\n }\n syncReasoningForModel(current)\n },\n { immediate: true }\n )\n\n /** 向父组件同步消息快照(流式 delta 过于频繁,不同步,只在关键节点同步) */\n const snapshot = () => {\n emit('update:messages', messages.value.slice())\n }\n\n /** 发送一条用户消息并启动对话循环 */\n const send = (content: string, attachments?: ChatAttachment[]) => {\n if (running.value) return\n if (!content.trim() && !attachments?.length) return\n\n const message = reactive<ChatMessage>({ id: uid(), role: 'user', content, attachments })\n messages.value.push(message)\n snapshot()\n emit('send', message)\n\n void runConversation()\n }\n\n /** 执行单个工具调用(含确认门控),结果以 tool 消息追加 */\n const executeToolCall = async (\n toolCall: ChatToolCall,\n signal: AbortSignal,\n tools: ChatTool[]\n ) => {\n const appendToolMessage = (content: string) => {\n messages.value.push(\n reactive<ChatMessage>({ id: uid(), role: 'tool', toolCallId: toolCall.id, content })\n )\n }\n\n const tool = tools.find((t) => t.name === toolCall.name)\n\n if (!tool) {\n toolCall.status = 'error'\n toolCall.error = `未找到名为 \"${toolCall.name}\" 的工具`\n appendToolMessage(`Error: tool \"${toolCall.name}\" is not available.`)\n return\n }\n\n if (tool.needsConfirm) {\n toolCall.status = 'awaiting-confirm'\n const approved = await new Promise<boolean>((resolve) => {\n confirmResolvers.set(toolCall.id, resolve)\n signal.addEventListener('abort', () => resolve(false), { once: true })\n })\n confirmResolvers.delete(toolCall.id)\n\n if (!approved) {\n toolCall.status = 'rejected'\n appendToolMessage(`The user rejected the execution of tool \"${toolCall.name}\".`)\n return\n }\n }\n\n toolCall.status = 'running'\n try {\n const args = toolCall.arguments ? JSON.parse(toolCall.arguments) : {}\n const result = await tool.execute(args, { toolCall, signal })\n toolCall.status = 'success'\n toolCall.result = serializeResult(result)\n appendToolMessage(toolCall.result)\n } catch (error) {\n toolCall.status = 'error'\n toolCall.error = error instanceof Error ? error.message : String(error)\n appendToolMessage(`Error: ${toolCall.error}`)\n }\n }\n\n /** 对话主循环入口:生成 → 工具执行 → 再生成,直至模型不再调用工具 */\n const runConversation = async () => {\n if (!props.transport) return\n\n running.value = true\n const controller = new AbortController()\n abortController = controller\n\n try {\n await runRound(controller)\n } finally {\n running.value = false\n abortController = null\n snapshot()\n }\n }\n\n /** 单轮生成;存在工具调用时执行后递归进入下一轮(递归避免 await-in-loop) */\n const runRound = async (controller: AbortController): Promise<void> => {\n if (!props.transport) return\n const { signal } = controller\n const tools = resolveTools(props.tools)\n\n const assistant = reactive<ChatMessage>({\n id: uid(),\n role: 'assistant',\n content: '',\n status: 'streaming',\n toolCalls: []\n })\n messages.value.push(assistant)\n\n let requestError: Error | null = null\n\n try {\n await props.transport(\n {\n // 排除刚追加的 assistant 占位消息\n messages: messages.value.slice(0, -1),\n systemPrompt: props.systemPrompt,\n tools,\n model: model.value,\n reasoningLevel: reasoningLevel.value,\n signal\n },\n {\n onTextDelta: (delta) => {\n assistant.content += delta\n },\n onReasoningDelta: (delta) => {\n assistant.reasoning = (assistant.reasoning ?? '') + delta\n },\n onToolCall: (call) => {\n const toolCall = reactive<ChatToolCall>({ ...call, status: 'pending' })\n assistant.toolCalls!.push(toolCall)\n emit('tool-call', toolCall)\n },\n onError: (error) => {\n requestError = error\n }\n }\n )\n } catch (error) {\n requestError = error instanceof Error ? error : new Error(String(error))\n }\n\n if (signal.aborted) {\n assistant.status = 'aborted'\n return\n }\n\n if (requestError) {\n assistant.status = 'error'\n emit('error', requestError)\n return\n }\n\n assistant.status = 'done'\n\n const toolCalls = assistant.toolCalls ?? []\n if (!toolCalls.length) {\n emit('finish', assistant)\n return\n }\n\n // 工具串行执行(保持结果消息与调用顺序一致),用 Promise 链避免 await-in-loop\n await toolCalls.reduce<Promise<void>>(\n (prev, toolCall) =>\n prev.then(() => (signal.aborted ? undefined : executeToolCall(toolCall, signal, tools))),\n Promise.resolve()\n )\n snapshot()\n\n if (signal.aborted) return\n\n return runRound(controller)\n }\n\n /** 中断当前生成,挂起的工具确认按拒绝处理 */\n const abort = () => {\n abortController?.abort()\n }\n\n /** 响应 needsConfirm 工具的用户确认 */\n const respondToolCall = (toolCallId: string, approved: boolean) => {\n confirmResolvers.get(toolCallId)?.(approved)\n }\n\n /** 重新生成:移除最后一条用户消息之后的所有消息,重新跑对话循环 */\n const regenerate = () => {\n if (running.value) return\n\n const list = messages.value\n let lastUserIndex = -1\n for (let i = list.length - 1; i >= 0; i--) {\n if (list[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1 || lastUserIndex === list.length - 1) return\n\n messages.value = list.slice(0, lastUserIndex + 1)\n snapshot()\n void runConversation()\n }\n\n /** 清空消息,生成中则先中断 */\n const clear = () => {\n abort()\n messages.value = []\n snapshot()\n }\n\n return {\n messages,\n model,\n reasoningLevel,\n running,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-chat.js","names":[],"sources":["../../src/chat/use-chat.ts"],"sourcesContent":["import { useModel } from '@veltra/compositions'\nimport { reactive, ref, watch, type Ref } from 'vue'\n\nimport { createBuiltinTools } from '../tools'\nimport type { AiChatEmits, AiChatProps } from '../types'\nimport type {\n ChatAttachment,\n ChatMessage,\n ChatQueuedMessage,\n ChatTool,\n ChatToolCall\n} from './types'\n\nexport interface UseChatOptions {\n props: AiChatProps\n emit: AiChatEmits\n}\n\nconst uid = () => {\n return typeof crypto !== 'undefined' && crypto.randomUUID\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`\n}\n\nfunction serializeResult(result: unknown): string {\n if (typeof result === 'string') return result\n try {\n return JSON.stringify(result) ?? 'null'\n } catch {\n return String(result)\n }\n}\n\n/** 内置工具 + 用户工具,同名时内置优先 */\nfunction resolveTools(userTools?: ChatTool[]): ChatTool[] {\n const builtins = createBuiltinTools()\n const names = new Set(builtins.map((t) => t.name))\n return [...builtins, ...(userTools ?? []).filter((t) => !names.has(t.name))]\n}\n\n/**\n * AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。\n * 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。\n * 始终注入内置工具(如 askQuestion),无需经 props 传入。\n */\nexport function useChat(options: UseChatOptions) {\n const { props, emit } = options\n\n const messages = useModel({\n props,\n propName: 'messages',\n emit,\n local: true,\n defaultValue: []\n }) as Ref<ChatMessage[]>\n\n const model = useModel({ props, propName: 'model', emit, local: true }) as Ref<string | undefined>\n\n const reasoningLevel = useModel({ props, propName: 'reasoningLevel', emit, local: true }) as Ref<\n string | undefined\n >\n\n /** 是否正在生成中(含工具执行与多轮循环) */\n const running = ref(false)\n\n /** 待发送队列:会话进行中提交的新消息按序排队,收尾后先进先出自动接续 */\n const queue = ref<ChatQueuedMessage[]>([])\n\n let abortController: AbortController | null = null\n /** needsConfirm 工具的挂起确认器,key 为 toolCallId */\n const confirmResolvers = new Map<string, (approved: boolean) => void>()\n /** 当前会话是否自然完成(finish);决定收尾后是否自动消耗队列 */\n let finishedNaturally = false\n /** startQueued 触发的中断需要在收尾后立即接续队首 */\n let resumeAfterAbort = false\n\n /** 按当前模型校正推理等级(无 levels 则清空;值不合法则落到默认/首项) */\n const syncReasoningForModel = (modelId: string | undefined) => {\n const option = props.models?.find((m) => m.id === modelId)\n if (!option?.reasoningLevels?.length) {\n if (reasoningLevel.value !== undefined) {\n reasoningLevel.value = undefined\n }\n return\n }\n const levels = option.reasoningLevels\n if (reasoningLevel.value && levels.some((l) => l.value === reasoningLevel.value)) return\n reasoningLevel.value = option.defaultReasoningLevel ?? levels[0]?.value\n }\n\n // models / model 变化时:确保有合法选中模型,并校正推理等级\n watch(\n () => [props.models, model.value] as const,\n ([models, current]) => {\n if (!models?.length) return\n if (!current || !models.some((m) => m.id === current)) {\n model.value = models[0]!.id\n return\n }\n syncReasoningForModel(current)\n },\n { immediate: true }\n )\n\n /** 向父组件同步消息快照(流式 delta 过于频繁,不同步,只在关键节点同步) */\n const snapshot = () => {\n emit('update:messages', messages.value.slice())\n }\n\n /**\n * 发送一条用户消息。会话进行中时进入待发送队列(不丢失),\n * 会话自然结束后按 FIFO 自动接续;空闲时立即开始新会话。\n */\n const send = (content: string, attachments?: ChatAttachment[]) => {\n if (!content.trim() && !attachments?.length) return\n enqueue(content, attachments)\n }\n\n /**\n * 向队列插入一条消息;beforeId 指定插到某条之前(缺省追加到尾部)。\n * 空闲时立即消耗队首(保持先来先发的合理顺序)。\n */\n const enqueue = (\n content: string,\n attachments?: ChatAttachment[],\n beforeId?: string\n ): ChatQueuedMessage => {\n const item: ChatQueuedMessage = { id: uid(), content, attachments }\n const anchorIndex = beforeId ? queue.value.findIndex((q) => q.id === beforeId) : -1\n if (anchorIndex === -1) {\n queue.value.push(item)\n } else {\n queue.value.splice(anchorIndex, 0, item)\n }\n if (!running.value) drainQueue()\n return item\n }\n\n /** 消耗队首并开始新一轮会话 */\n const drainQueue = () => {\n const next = queue.value.shift()\n if (next) startUserTurn(next.content, next.attachments)\n }\n\n /** 立即执行队列中的某条:中断当前会话,该条插队为下一条(其余保持原顺序) */\n const startQueued = (id: string) => {\n const item = removeQueued(id)\n if (!item) return\n\n if (!running.value) {\n startUserTurn(item.content, item.attachments)\n return\n }\n\n // 插回队首并中断当前会话,收尾逻辑会自动接上它\n queue.value.unshift(item)\n resumeAfterAbort = true\n abort()\n }\n\n /** 从队列移除某条(返回被移除项;编辑场景由 UI 取回内容) */\n const removeQueued = (id: string): ChatQueuedMessage | undefined => {\n const index = queue.value.findIndex((item) => item.id === id)\n if (index === -1) return undefined\n const [item] = queue.value.splice(index, 1)\n return item\n }\n\n /** 推送用户消息并启动对话循环 */\n const startUserTurn = (content: string, attachments?: ChatAttachment[]) => {\n const message = reactive<ChatMessage>({ id: uid(), role: 'user', content, attachments })\n messages.value.push(message)\n snapshot()\n emit('send', message)\n\n void runConversation()\n }\n\n /** 执行单个工具调用(含确认门控),结果以 tool 消息追加 */\n const executeToolCall = async (\n toolCall: ChatToolCall,\n signal: AbortSignal,\n tools: ChatTool[]\n ) => {\n const appendToolMessage = (content: string) => {\n messages.value.push(\n reactive<ChatMessage>({ id: uid(), role: 'tool', toolCallId: toolCall.id, content })\n )\n }\n\n const tool = tools.find((t) => t.name === toolCall.name)\n\n if (!tool) {\n toolCall.status = 'error'\n toolCall.error = `未找到名为 \"${toolCall.name}\" 的工具`\n appendToolMessage(`Error: tool \"${toolCall.name}\" is not available.`)\n return\n }\n\n if (tool.needsConfirm) {\n toolCall.status = 'awaiting-confirm'\n const approved = await new Promise<boolean>((resolve) => {\n confirmResolvers.set(toolCall.id, resolve)\n signal.addEventListener('abort', () => resolve(false), { once: true })\n })\n confirmResolvers.delete(toolCall.id)\n\n if (!approved) {\n toolCall.status = 'rejected'\n appendToolMessage(`The user rejected the execution of tool \"${toolCall.name}\".`)\n return\n }\n }\n\n toolCall.status = 'running'\n try {\n const args = toolCall.arguments ? JSON.parse(toolCall.arguments) : {}\n const result = await tool.execute(args, { toolCall, signal })\n toolCall.status = 'success'\n toolCall.result = serializeResult(result)\n appendToolMessage(toolCall.result)\n } catch (error) {\n toolCall.status = 'error'\n toolCall.error = error instanceof Error ? error.message : String(error)\n appendToolMessage(`Error: ${toolCall.error}`)\n }\n }\n\n /** 对话主循环入口:生成 → 工具执行 → 再生成,直至模型不再调用工具 */\n const runConversation = async () => {\n if (!props.transport) return\n\n running.value = true\n finishedNaturally = false\n const controller = new AbortController()\n abortController = controller\n\n try {\n await runRound(controller, 0)\n } finally {\n running.value = false\n abortController = null\n snapshot()\n\n // 队列接续:自然完成或 startQueued 中断插队时自动发下一条;\n // 手动停止 / 出错时保留队列,交由用户处置\n const shouldResume = finishedNaturally || resumeAfterAbort\n finishedNaturally = false\n resumeAfterAbort = false\n if (shouldResume) drainQueue()\n }\n }\n\n /** 单轮生成;存在工具调用时执行后递归进入下一轮(递归避免 await-in-loop) */\n const runRound = async (controller: AbortController, depth: number): Promise<void> => {\n if (!props.transport) return\n const { signal } = controller\n const tools = resolveTools(props.tools)\n\n const assistant = reactive<ChatMessage>({\n id: uid(),\n role: 'assistant',\n content: '',\n status: 'streaming',\n toolCalls: []\n })\n messages.value.push(assistant)\n\n let requestError: Error | null = null\n\n try {\n await props.transport(\n {\n // 排除刚追加的 assistant 占位消息\n messages: messages.value.slice(0, -1),\n systemPrompt: props.systemPrompt,\n tools,\n model: model.value,\n reasoningLevel: reasoningLevel.value,\n signal\n },\n {\n onTextDelta: (delta) => {\n assistant.content += delta\n },\n onReasoningDelta: (delta) => {\n assistant.reasoning = (assistant.reasoning ?? '') + delta\n },\n onToolCall: (call) => {\n const toolCall = reactive<ChatToolCall>({ ...call, status: 'pending' })\n assistant.toolCalls!.push(toolCall)\n emit('tool-call', toolCall)\n },\n onError: (error) => {\n requestError = error\n }\n }\n )\n } catch (error) {\n requestError = error instanceof Error ? error : new Error(String(error))\n }\n\n if (signal.aborted) {\n assistant.status = 'aborted'\n return\n }\n\n if (requestError) {\n assistant.status = 'error'\n emit('error', requestError)\n return\n }\n\n assistant.status = 'done'\n\n const toolCalls = assistant.toolCalls ?? []\n if (!toolCalls.length) {\n completeAsFinish(assistant)\n return\n }\n\n // 工具串行执行(保持结果消息与调用顺序一致),用 Promise 链避免 await-in-loop\n await toolCalls.reduce<Promise<void>>(\n (prev, toolCall) =>\n prev.then(() => (signal.aborted ? undefined : executeToolCall(toolCall, signal, tools))),\n Promise.resolve()\n )\n snapshot()\n\n if (signal.aborted) return\n\n // 终结工具执行成功:工具 UI 即最终答复,不再回灌模型生成文字\n const hitTerminal = toolCalls.some(\n (call) => call.status === 'success' && tools.some((t) => t.name === call.name && t.terminal)\n )\n if (hitTerminal) {\n completeAsFinish(assistant)\n return\n }\n\n // 达到最大轮次上限:停止继续请求,防止模型失控循环调用工具\n if (depth + 1 >= (props.maxToolRounds ?? 10)) {\n completeAsFinish(assistant)\n return\n }\n\n return runRound(controller, depth + 1)\n }\n\n /** 标记本轮对话自然完成(收尾后据此自动消耗待发送队列) */\n const completeAsFinish = (assistant: ChatMessage) => {\n finishedNaturally = true\n emit('finish', assistant)\n }\n\n /** 中断当前生成,挂起的工具确认按拒绝处理 */\n const abort = () => {\n abortController?.abort()\n }\n\n /** 响应 needsConfirm 工具的用户确认 */\n const respondToolCall = (toolCallId: string, approved: boolean) => {\n confirmResolvers.get(toolCallId)?.(approved)\n }\n\n /** 重新生成:移除最后一条用户消息之后的所有消息,重新跑对话循环 */\n const regenerate = () => {\n if (running.value) return\n\n const list = messages.value\n let lastUserIndex = -1\n for (let i = list.length - 1; i >= 0; i--) {\n if (list[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1 || lastUserIndex === list.length - 1) return\n\n messages.value = list.slice(0, lastUserIndex + 1)\n snapshot()\n void runConversation()\n }\n\n /** 清空消息与待发送队列,生成中则先中断 */\n const clear = () => {\n abort()\n queue.value = []\n messages.value = []\n snapshot()\n }\n\n return {\n messages,\n model,\n reasoningLevel,\n running,\n queue,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall,\n enqueue,\n startQueued,\n removeQueued\n }\n}\n"],"mappings":"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"}