@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,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
- import { OpenAITransportOptions, createOpenAITransport } from "./transports/openai.js";
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, OpenAITransportOptions, ToolCallStatus, UseChatOptions, createOpenAITransport, useChat };
5
+ export { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, UseChatOptions, createOpenAITransport, useChat };
@@ -1,2 +1,2 @@
1
- import { OpenAITransportOptions, createOpenAITransport } from "./openai.js";
2
- export { OpenAITransportOptions, createOpenAITransport };
1
+ import { OpenAITransport, OpenAITransportOptions, createOpenAITransport } from "./openai.js";
2
+ export { OpenAITransport, OpenAITransportOptions, createOpenAITransport };
@@ -1,23 +1,27 @@
1
1
  import { ChatTransport } from "../types.js";
2
+ import { ChatModelOption, ChatProvider } from "../../providers/types.js";
2
3
  //#region src/chat/transports/openai.d.ts
3
- /** createOpenAITransport 的配置项 */
4
+ /** createOpenAITransport 的配置项(多 Provider) */
4
5
  interface OpenAITransportOptions {
5
- /** chat completions 端点,如 https://api.openai.com/v1/chat/completions */
6
- endpoint: string;
7
- /** API Key(Bearer 鉴权) */
8
- apiKey?: string;
9
- /** 模型名 */
10
- model: string;
11
- /** 额外请求头 */
6
+ /** 至少一个 Provider;模型 id 须跨 Provider 全局唯一 */
7
+ providers: ChatProvider[];
8
+ /** 全局额外请求头(与各 Provider headers 合并,Provider 优先) */
12
9
  headers?: Record<string, string>;
13
- /** 额外请求体字段,如 temperature、top_p */
10
+ /** 全局额外请求体字段,如 temperature、top_p */
14
11
  body?: Record<string, unknown>;
15
12
  }
13
+ /** 带扁平模型列表元数据的 OpenAI 兼容 transport */
14
+ type OpenAITransport = ChatTransport & {
15
+ /** 供 UI 使用的扁平模型列表 */
16
+ readonly models: ChatModelOption[];
17
+ /** 默认模型 id(首个 Provider 的首个模型) */
18
+ readonly defaultModel: string;
19
+ };
16
20
  /**
17
21
  * 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。
18
- * 手写 fetch + SSE 解析,无任何第三方依赖。
22
+ * 按 request.model 路由到对应 Provider 的 endpoint;手写 fetch + SSE,无第三方依赖。
19
23
  */
20
- declare function createOpenAITransport(options: OpenAITransportOptions): ChatTransport;
24
+ declare function createOpenAITransport(options: OpenAITransportOptions): OpenAITransport;
21
25
  //#endregion
22
- export { OpenAITransportOptions, createOpenAITransport };
26
+ export { OpenAITransport, OpenAITransportOptions, createOpenAITransport };
23
27
  //# sourceMappingURL=openai.d.ts.map
@@ -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) {
@@ -124,31 +124,68 @@ async function parseSSE(body, signal, handlers) {
124
124
  }
125
125
  flushToolCalls();
126
126
  }
127
+ /** 校验 providers 并展开扁平模型列表 */
128
+ function flattenModels(providers) {
129
+ if (!providers.length) throw new Error("[createOpenAITransport] providers 不能为空");
130
+ const models = [];
131
+ const byModelId = /* @__PURE__ */ new Map();
132
+ for (const provider of providers) {
133
+ if (!provider.models?.length) throw new Error(`[createOpenAITransport] Provider "${provider.id}" 未配置 models`);
134
+ for (const model of provider.models) {
135
+ if (byModelId.has(model.id)) throw new Error(`[createOpenAITransport] 模型 id "${model.id}" 重复(跨 Provider 须全局唯一)`);
136
+ byModelId.set(model.id, provider);
137
+ models.push({
138
+ ...model,
139
+ providerId: provider.id,
140
+ providerLabel: provider.label
141
+ });
142
+ }
143
+ }
144
+ return {
145
+ models,
146
+ byModelId
147
+ };
148
+ }
149
+ /** 缺省:写入 OpenAI 兼容的 reasoning_effort */
150
+ function defaultApplyReasoning(level, body) {
151
+ body.reasoning_effort = level;
152
+ }
127
153
  /**
128
154
  * 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。
129
- * 手写 fetch + SSE 解析,无任何第三方依赖。
155
+ * 按 request.model 路由到对应 Provider 的 endpoint;手写 fetch + SSE,无第三方依赖。
130
156
  */
131
157
  function createOpenAITransport(options) {
132
- const { endpoint, apiKey, model, headers, body } = options;
133
- return async (request, handlers) => {
134
- const { messages, systemPrompt, tools, signal } = request;
158
+ const { providers, headers: globalHeaders, body: globalBody } = options;
159
+ const { models, byModelId } = flattenModels(providers);
160
+ const defaultModel = models[0].id;
161
+ const send = async (request, handlers) => {
162
+ const { messages, systemPrompt, tools, signal, reasoningLevel } = request;
163
+ const modelId = request.model ?? defaultModel;
164
+ const provider = byModelId.get(modelId);
165
+ if (!provider) {
166
+ handlers.onError?.(/* @__PURE__ */ new Error(`未找到模型 "${modelId}" 对应的 Provider`));
167
+ return;
168
+ }
169
+ const requestBody = {
170
+ ...globalBody,
171
+ model: modelId,
172
+ stream: true,
173
+ messages: toOpenAIMessages(messages, systemPrompt),
174
+ ...tools?.length ? { tools: toOpenAITools(tools) } : {}
175
+ };
176
+ if (reasoningLevel) (provider.applyReasoning ?? defaultApplyReasoning)(reasoningLevel, requestBody);
135
177
  let response;
136
178
  try {
137
- response = await fetch(endpoint, {
179
+ response = await fetch(provider.endpoint, {
138
180
  method: "POST",
139
181
  signal,
140
182
  headers: {
141
183
  "Content-Type": "application/json",
142
- ...apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
143
- ...headers
184
+ ...provider.apiKey ? { Authorization: `Bearer ${provider.apiKey}` } : {},
185
+ ...globalHeaders,
186
+ ...provider.headers
144
187
  },
145
- body: JSON.stringify({
146
- model,
147
- stream: true,
148
- ...body,
149
- messages: toOpenAIMessages(messages, systemPrompt),
150
- ...tools?.length ? { tools: toOpenAITools(tools) } : {}
151
- })
188
+ body: JSON.stringify(requestBody)
152
189
  });
153
190
  } catch (error) {
154
191
  if (!signal.aborted) handlers.onError?.(error instanceof Error ? error : new Error(String(error)));
@@ -169,6 +206,10 @@ function createOpenAITransport(options) {
169
206
  if (!signal.aborted) handlers.onError?.(error instanceof Error ? error : new Error(String(error)));
170
207
  }
171
208
  };
209
+ return Object.assign(send, {
210
+ models,
211
+ defaultModel
212
+ });
172
213
  }
173
214
  //#endregion
174
215
  export { createOpenAITransport };
@@ -1 +1 @@
1
- {"version":3,"file":"openai.js","names":[],"sources":["../../../src/chat/transports/openai.ts"],"sourcesContent":["import type {\n ChatMessage,\n ChatTool,\n ChatTransport,\n ChatTransportHandlers,\n ChatTransportRequest\n} from '../types'\n\n/** createOpenAITransport 的配置项 */\nexport interface OpenAITransportOptions {\n /** chat completions 端点,如 https://api.openai.com/v1/chat/completions */\n endpoint: string\n /** API Key(Bearer 鉴权) */\n apiKey?: string\n /** 模型名 */\n model: string\n /** 额外请求头 */\n headers?: Record<string, string>\n /** 额外请求体字段,如 temperature、top_p */\n body?: Record<string, unknown>\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/**\n * 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。\n * 手写 fetch + SSE 解析,无任何第三方依赖。\n */\nexport function createOpenAITransport(options: OpenAITransportOptions): ChatTransport {\n const { endpoint, apiKey, model, headers, body } = options\n\n return async (request: ChatTransportRequest, handlers: ChatTransportHandlers) => {\n const { messages, systemPrompt, tools, signal } = request\n\n let response: Response\n try {\n response = await fetch(endpoint, {\n method: 'POST',\n signal,\n headers: {\n 'Content-Type': 'application/json',\n ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),\n ...headers\n },\n body: JSON.stringify({\n model,\n stream: true,\n ...body,\n messages: toOpenAIMessages(messages, systemPrompt),\n ...(tools?.length ? { tools: toOpenAITools(tools) } : {})\n })\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"],"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
  }
@@ -94,6 +129,10 @@ interface ChatTransportRequest {
94
129
  systemPrompt?: string;
95
130
  /** 可用工具 */
96
131
  tools?: ChatTool[];
132
+ /** 选中的模型 id(内置 OpenAI transport 按此路由 Provider) */
133
+ model?: string;
134
+ /** 选中的推理等级(不透明字符串,由 Provider.applyReasoning 写入请求体) */
135
+ reasoningLevel?: string;
97
136
  /** 中断信号 */
98
137
  signal: AbortSignal;
99
138
  }
@@ -121,5 +160,5 @@ interface ChatTransportHandlers {
121
160
  */
122
161
  type ChatTransport = (request: ChatTransportRequest, handlers: ChatTransportHandlers) => Promise<void> | void;
123
162
  //#endregion
124
- export { ChatAttachment, ChatMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus };
163
+ export { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus };
125
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";
@@ -10,15 +10,40 @@ interface UseChatOptions {
10
10
  /**
11
11
  * AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。
12
12
  * 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。
13
+ * 始终注入内置工具(如 askQuestion),无需经 props 传入。
13
14
  */
14
15
  declare function useChat(options: UseChatOptions): {
15
16
  messages: Ref<ChatMessage[], ChatMessage[]>;
17
+ model: Ref<string | undefined, string | undefined>;
18
+ reasoningLevel: Ref<string | undefined, string | undefined>;
16
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
+ }[]>;
17
39
  send: (content: string, attachments?: ChatAttachment[]) => void;
18
40
  abort: () => void;
19
41
  regenerate: () => void;
20
42
  clear: () => void;
21
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;
22
47
  };
23
48
  //#endregion
24
49
  export { UseChatOptions, useChat };
@@ -1,5 +1,6 @@
1
+ import { createBuiltinTools } from "../tools/index.js";
1
2
  import { useModel } from "@veltra/compositions";
2
- import { reactive, ref } from "vue";
3
+ import { reactive, ref, watch } from "vue";
3
4
  //#region src/chat/use-chat.ts
4
5
  const uid = () => {
5
6
  return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
@@ -12,9 +13,16 @@ function serializeResult(result) {
12
13
  return String(result);
13
14
  }
14
15
  }
16
+ /** 内置工具 + 用户工具,同名时内置优先 */
17
+ function resolveTools(userTools) {
18
+ const builtins = createBuiltinTools();
19
+ const names = new Set(builtins.map((t) => t.name));
20
+ return [...builtins, ...(userTools ?? []).filter((t) => !names.has(t.name))];
21
+ }
15
22
  /**
16
23
  * AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。
17
24
  * 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。
25
+ * 始终注入内置工具(如 askQuestion),无需经 props 传入。
18
26
  */
19
27
  function useChat(options) {
20
28
  const { props, emit } = options;
@@ -25,19 +33,102 @@ function useChat(options) {
25
33
  local: true,
26
34
  defaultValue: []
27
35
  });
36
+ const model = useModel({
37
+ props,
38
+ propName: "model",
39
+ emit,
40
+ local: true
41
+ });
42
+ const reasoningLevel = useModel({
43
+ props,
44
+ propName: "reasoningLevel",
45
+ emit,
46
+ local: true
47
+ });
28
48
  /** 是否正在生成中(含工具执行与多轮循环) */
29
49
  const running = ref(false);
50
+ /** 待发送队列:会话进行中提交的新消息按序排队,收尾后先进先出自动接续 */
51
+ const queue = ref([]);
30
52
  let abortController = null;
31
53
  /** needsConfirm 工具的挂起确认器,key 为 toolCallId */
32
54
  const confirmResolvers = /* @__PURE__ */ new Map();
55
+ /** 当前会话是否自然完成(finish);决定收尾后是否自动消耗队列 */
56
+ let finishedNaturally = false;
57
+ /** startQueued 触发的中断需要在收尾后立即接续队首 */
58
+ let resumeAfterAbort = false;
59
+ /** 按当前模型校正推理等级(无 levels 则清空;值不合法则落到默认/首项) */
60
+ const syncReasoningForModel = (modelId) => {
61
+ const option = props.models?.find((m) => m.id === modelId);
62
+ if (!option?.reasoningLevels?.length) {
63
+ if (reasoningLevel.value !== void 0) reasoningLevel.value = void 0;
64
+ return;
65
+ }
66
+ const levels = option.reasoningLevels;
67
+ if (reasoningLevel.value && levels.some((l) => l.value === reasoningLevel.value)) return;
68
+ reasoningLevel.value = option.defaultReasoningLevel ?? levels[0]?.value;
69
+ };
70
+ watch(() => [props.models, model.value], ([models, current]) => {
71
+ if (!models?.length) return;
72
+ if (!current || !models.some((m) => m.id === current)) {
73
+ model.value = models[0].id;
74
+ return;
75
+ }
76
+ syncReasoningForModel(current);
77
+ }, { immediate: true });
33
78
  /** 向父组件同步消息快照(流式 delta 过于频繁,不同步,只在关键节点同步) */
34
79
  const snapshot = () => {
35
80
  emit("update:messages", messages.value.slice());
36
81
  };
37
- /** 发送一条用户消息并启动对话循环 */
82
+ /**
83
+ * 发送一条用户消息。会话进行中时进入待发送队列(不丢失),
84
+ * 会话自然结束后按 FIFO 自动接续;空闲时立即开始新会话。
85
+ */
38
86
  const send = (content, attachments) => {
39
- if (running.value) return;
40
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) => {
41
132
  const message = reactive({
42
133
  id: uid(),
43
134
  role: "user",
@@ -50,7 +141,7 @@ function useChat(options) {
50
141
  runConversation();
51
142
  };
52
143
  /** 执行单个工具调用(含确认门控),结果以 tool 消息追加 */
53
- const executeToolCall = async (toolCall, signal) => {
144
+ const executeToolCall = async (toolCall, signal, tools) => {
54
145
  const appendToolMessage = (content) => {
55
146
  messages.value.push(reactive({
56
147
  id: uid(),
@@ -59,7 +150,7 @@ function useChat(options) {
59
150
  content
60
151
  }));
61
152
  };
62
- const tool = props.tools?.find((t) => t.name === toolCall.name);
153
+ const tool = tools.find((t) => t.name === toolCall.name);
63
154
  if (!tool) {
64
155
  toolCall.status = "error";
65
156
  toolCall.error = `未找到名为 "${toolCall.name}" 的工具`;
@@ -99,20 +190,26 @@ function useChat(options) {
99
190
  const runConversation = async () => {
100
191
  if (!props.transport) return;
101
192
  running.value = true;
193
+ finishedNaturally = false;
102
194
  const controller = new AbortController();
103
195
  abortController = controller;
104
196
  try {
105
- await runRound(controller);
197
+ await runRound(controller, 0);
106
198
  } finally {
107
199
  running.value = false;
108
200
  abortController = null;
109
201
  snapshot();
202
+ const shouldResume = finishedNaturally || resumeAfterAbort;
203
+ finishedNaturally = false;
204
+ resumeAfterAbort = false;
205
+ if (shouldResume) drainQueue();
110
206
  }
111
207
  };
112
208
  /** 单轮生成;存在工具调用时执行后递归进入下一轮(递归避免 await-in-loop) */
113
- const runRound = async (controller) => {
209
+ const runRound = async (controller, depth) => {
114
210
  if (!props.transport) return;
115
211
  const { signal } = controller;
212
+ const tools = resolveTools(props.tools);
116
213
  const assistant = reactive({
117
214
  id: uid(),
118
215
  role: "assistant",
@@ -126,7 +223,9 @@ function useChat(options) {
126
223
  await props.transport({
127
224
  messages: messages.value.slice(0, -1),
128
225
  systemPrompt: props.systemPrompt,
129
- tools: props.tools,
226
+ tools,
227
+ model: model.value,
228
+ reasoningLevel: reasoningLevel.value,
130
229
  signal
131
230
  }, {
132
231
  onTextDelta: (delta) => {
@@ -162,13 +261,26 @@ function useChat(options) {
162
261
  assistant.status = "done";
163
262
  const toolCalls = assistant.toolCalls ?? [];
164
263
  if (!toolCalls.length) {
165
- emit("finish", assistant);
264
+ completeAsFinish(assistant);
166
265
  return;
167
266
  }
168
- await toolCalls.reduce((prev, toolCall) => prev.then(() => signal.aborted ? void 0 : executeToolCall(toolCall, signal)), Promise.resolve());
267
+ await toolCalls.reduce((prev, toolCall) => prev.then(() => signal.aborted ? void 0 : executeToolCall(toolCall, signal, tools)), Promise.resolve());
169
268
  snapshot();
170
269
  if (signal.aborted) return;
171
- 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);
172
284
  };
173
285
  /** 中断当前生成,挂起的工具确认按拒绝处理 */
174
286
  const abort = () => {
@@ -192,20 +304,27 @@ function useChat(options) {
192
304
  snapshot();
193
305
  runConversation();
194
306
  };
195
- /** 清空消息,生成中则先中断 */
307
+ /** 清空消息与待发送队列,生成中则先中断 */
196
308
  const clear = () => {
197
309
  abort();
310
+ queue.value = [];
198
311
  messages.value = [];
199
312
  snapshot();
200
313
  };
201
314
  return {
202
315
  messages,
316
+ model,
317
+ reasoningLevel,
203
318
  running,
319
+ queue,
204
320
  send,
205
321
  abort,
206
322
  regenerate,
207
323
  clear,
208
- respondToolCall
324
+ respondToolCall,
325
+ enqueue,
326
+ startQueued,
327
+ removeQueued
209
328
  };
210
329
  }
211
330
  //#endregion