@veltra/ai 1.4.0 → 2.0.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 (56) hide show
  1. package/dist/chat/index.d.ts +2 -2
  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 +55 -14
  5. package/dist/chat/transports/openai.js.map +1 -1
  6. package/dist/chat/types.d.ts +4 -0
  7. package/dist/chat/use-chat.d.ts +3 -0
  8. package/dist/chat/use-chat.js +49 -5
  9. package/dist/chat/use-chat.js.map +1 -1
  10. package/dist/components/ai-chat/ai-chat.js +27 -7
  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 +50 -38
  15. package/dist/components/ai-chat/chat-input.js.map +1 -1
  16. package/dist/components/ai-chat/message-item.js +2 -2
  17. package/dist/components/ai-chat/message-item.js.map +1 -1
  18. package/dist/components/ai-chat/message-list.js +1 -1
  19. package/dist/components/ai-chat/message-list.js.map +1 -1
  20. package/dist/components/ai-chat/model-picker.js +117 -0
  21. package/dist/components/ai-chat/model-picker.js.map +1 -0
  22. package/dist/components/ai-chat/style.d.ts +2 -0
  23. package/dist/components/ai-chat/style.js +2 -0
  24. package/dist/components/ai-chat/style2.css +128 -0
  25. package/dist/components/ai-chat/tool-call.js +2 -2
  26. package/dist/components/ai-chat/tool-call.js.map +1 -1
  27. package/dist/index.d.ts +5 -5
  28. package/dist/index.js +1 -2
  29. package/dist/providers/types.d.ts +47 -0
  30. package/dist/tools/ask-question/ask-question.d.ts +2 -17
  31. package/dist/tools/ask-question/index.d.ts +2 -2
  32. package/dist/tools/index.d.ts +3 -2
  33. package/dist/tools/index.js +10 -0
  34. package/dist/tools/index.js.map +1 -0
  35. package/dist/types/ai-chat.d.ts +14 -0
  36. package/package.json +8 -8
  37. package/src/chat/__test__/use-chat.test.ts +71 -0
  38. package/src/chat/transports/__test__/openai.test.ts +239 -0
  39. package/src/chat/transports/openai.ts +86 -24
  40. package/src/chat/types.ts +4 -0
  41. package/src/chat/use-chat.ts +67 -7
  42. package/src/components/ai-chat/__test__/ai-chat.test.ts +102 -1
  43. package/src/components/ai-chat/ai-chat.vue +22 -8
  44. package/src/components/ai-chat/chat-input.vue +43 -26
  45. package/src/components/ai-chat/model-picker.vue +126 -0
  46. package/src/components/ai-chat/style.scss +125 -0
  47. package/src/components/ai-chat/style.ts +2 -0
  48. package/src/index.ts +8 -1
  49. package/src/providers/index.ts +1 -0
  50. package/src/providers/types.ts +46 -0
  51. package/src/tools/ask-question/__test__/ask-question.test.ts +3 -3
  52. package/src/tools/index.ts +14 -1
  53. package/src/types/ai-chat.ts +14 -0
  54. package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
  55. package/dist/components/ai-chat/index.d.ts +0 -2
  56. package/dist/components/index.d.ts +0 -3
@@ -1,5 +1,5 @@
1
1
  import { ChatAttachment, ChatMessage, 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, 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
@@ -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 回退说明新的一轮 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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"}
@@ -94,6 +94,10 @@ interface ChatTransportRequest {
94
94
  systemPrompt?: string;
95
95
  /** 可用工具 */
96
96
  tools?: ChatTool[];
97
+ /** 选中的模型 id(内置 OpenAI transport 按此路由 Provider) */
98
+ model?: string;
99
+ /** 选中的推理等级(不透明字符串,由 Provider.applyReasoning 写入请求体) */
100
+ reasoningLevel?: string;
97
101
  /** 中断信号 */
98
102
  signal: AbortSignal;
99
103
  }
@@ -10,9 +10,12 @@ 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>;
17
20
  send: (content: string, attachments?: ChatAttachment[]) => void;
18
21
  abort: () => void;
@@ -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,11 +33,42 @@ 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);
30
50
  let abortController = null;
31
51
  /** needsConfirm 工具的挂起确认器,key 为 toolCallId */
32
52
  const confirmResolvers = /* @__PURE__ */ new Map();
53
+ /** 按当前模型校正推理等级(无 levels 则清空;值不合法则落到默认/首项) */
54
+ const syncReasoningForModel = (modelId) => {
55
+ const option = props.models?.find((m) => m.id === modelId);
56
+ if (!option?.reasoningLevels?.length) {
57
+ if (reasoningLevel.value !== void 0) reasoningLevel.value = void 0;
58
+ return;
59
+ }
60
+ const levels = option.reasoningLevels;
61
+ if (reasoningLevel.value && levels.some((l) => l.value === reasoningLevel.value)) return;
62
+ reasoningLevel.value = option.defaultReasoningLevel ?? levels[0]?.value;
63
+ };
64
+ watch(() => [props.models, model.value], ([models, current]) => {
65
+ if (!models?.length) return;
66
+ if (!current || !models.some((m) => m.id === current)) {
67
+ model.value = models[0].id;
68
+ return;
69
+ }
70
+ syncReasoningForModel(current);
71
+ }, { immediate: true });
33
72
  /** 向父组件同步消息快照(流式 delta 过于频繁,不同步,只在关键节点同步) */
34
73
  const snapshot = () => {
35
74
  emit("update:messages", messages.value.slice());
@@ -50,7 +89,7 @@ function useChat(options) {
50
89
  runConversation();
51
90
  };
52
91
  /** 执行单个工具调用(含确认门控),结果以 tool 消息追加 */
53
- const executeToolCall = async (toolCall, signal) => {
92
+ const executeToolCall = async (toolCall, signal, tools) => {
54
93
  const appendToolMessage = (content) => {
55
94
  messages.value.push(reactive({
56
95
  id: uid(),
@@ -59,7 +98,7 @@ function useChat(options) {
59
98
  content
60
99
  }));
61
100
  };
62
- const tool = props.tools?.find((t) => t.name === toolCall.name);
101
+ const tool = tools.find((t) => t.name === toolCall.name);
63
102
  if (!tool) {
64
103
  toolCall.status = "error";
65
104
  toolCall.error = `未找到名为 "${toolCall.name}" 的工具`;
@@ -113,6 +152,7 @@ function useChat(options) {
113
152
  const runRound = async (controller) => {
114
153
  if (!props.transport) return;
115
154
  const { signal } = controller;
155
+ const tools = resolveTools(props.tools);
116
156
  const assistant = reactive({
117
157
  id: uid(),
118
158
  role: "assistant",
@@ -126,7 +166,9 @@ function useChat(options) {
126
166
  await props.transport({
127
167
  messages: messages.value.slice(0, -1),
128
168
  systemPrompt: props.systemPrompt,
129
- tools: props.tools,
169
+ tools,
170
+ model: model.value,
171
+ reasoningLevel: reasoningLevel.value,
130
172
  signal
131
173
  }, {
132
174
  onTextDelta: (delta) => {
@@ -165,7 +207,7 @@ function useChat(options) {
165
207
  emit("finish", assistant);
166
208
  return;
167
209
  }
168
- await toolCalls.reduce((prev, toolCall) => prev.then(() => signal.aborted ? void 0 : executeToolCall(toolCall, signal)), Promise.resolve());
210
+ await toolCalls.reduce((prev, toolCall) => prev.then(() => signal.aborted ? void 0 : executeToolCall(toolCall, signal, tools)), Promise.resolve());
169
211
  snapshot();
170
212
  if (signal.aborted) return;
171
213
  return runRound(controller);
@@ -200,6 +242,8 @@ function useChat(options) {
200
242
  };
201
243
  return {
202
244
  messages,
245
+ model,
246
+ reasoningLevel,
203
247
  running,
204
248
  send,
205
249
  abort,
@@ -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, type Ref } from 'vue'\n\nimport type { AiChatEmits, AiChatProps } from '../types'\nimport type { ChatAttachment, ChatMessage, 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/**\n * AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。\n * 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。\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 /** 是否正在生成中(含工具执行与多轮循环) */\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 /** 向父组件同步消息快照(流式 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 (toolCall: ChatToolCall, signal: AbortSignal) => {\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 = props.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\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: props.tools,\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))),\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 { messages, running, send, abort, regenerate, clear, respondToolCall }\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 { 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,8 +1,9 @@
1
- import { useChat } from "../../chat/use-chat.js";
2
1
  import { AiChatDIKey } from "./di.js";
2
+ import { createBuiltinTools } from "../../tools/index.js";
3
+ import { useChat } from "../../chat/use-chat.js";
3
4
  import _sfc_main$1 from "./chat-input.js";
4
5
  import _sfc_main$2 from "./message-list.js";
5
- import { computed, createElementBlock, createSlots, createVNode, defineComponent, normalizeClass, openBlock, provide, renderSlot, unref, useSlots, withCtx } from "vue";
6
+ import { computed, createElementBlock, createSlots, createVNode, defineComponent, isRef, normalizeClass, openBlock, provide, renderSlot, unref, useSlots, withCtx } from "vue";
6
7
  import { bem } from "@veltra/utils";
7
8
  //#region src/components/ai-chat/ai-chat.vue
8
9
  const _sfc_main = /*@__PURE__*/ defineComponent({
@@ -13,6 +14,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
13
14
  tools: {},
14
15
  systemPrompt: {},
15
16
  messages: {},
17
+ models: {},
18
+ model: {},
19
+ reasoningLevel: {},
16
20
  welcome: {},
17
21
  placeholder: {},
18
22
  accept: {},
@@ -21,6 +25,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
21
25
  },
22
26
  emits: [
23
27
  "update:messages",
28
+ "update:model",
29
+ "update:reasoningLevel",
24
30
  "send",
25
31
  "finish",
26
32
  "error",
@@ -30,14 +36,20 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
30
36
  const props = __props;
31
37
  const emit = __emit;
32
38
  const cls = bem("ai-chat");
39
+ const slots = useSlots();
40
+ /** 内置 + 用户工具(同名内置优先),供工具卡片解析 icon/label/render/autoCollapse */
41
+ const toolMap = computed(() => {
42
+ const builtins = createBuiltinTools();
43
+ const names = new Set(builtins.map((t) => t.name));
44
+ const tools = [...builtins, ...(props.tools ?? []).filter((t) => !names.has(t.name))];
45
+ return Object.fromEntries(tools.map((t) => [t.name, t]));
46
+ });
33
47
  provide(AiChatDIKey, {
34
48
  cls,
35
- slots: useSlots(),
36
- tools: computed(() => {
37
- return Object.fromEntries((props.tools ?? []).map((tool) => [tool.name, tool]));
38
- })
49
+ slots,
50
+ tools: toolMap
39
51
  });
40
- const { messages, running, send, abort, regenerate, clear, respondToolCall } = useChat({
52
+ const { messages, model, reasoningLevel, running, send, abort, regenerate, clear, respondToolCall } = useChat({
41
53
  props,
42
54
  emit
43
55
  });
@@ -65,14 +77,22 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
65
77
  "onRespond",
66
78
  "onRegenerate"
67
79
  ]), createVNode(_sfc_main$1, {
80
+ model: unref(model),
81
+ "onUpdate:model": _cache[0] || (_cache[0] = ($event) => isRef(model) ? model.value = $event : null),
82
+ "reasoning-level": unref(reasoningLevel),
83
+ "onUpdate:reasoningLevel": _cache[1] || (_cache[1] = ($event) => isRef(reasoningLevel) ? reasoningLevel.value = $event : null),
68
84
  running: unref(running),
85
+ models: __props.models,
69
86
  placeholder: __props.placeholder,
70
87
  accept: __props.accept,
71
88
  "max-attachment-size": __props.maxAttachmentSize,
72
89
  onSend: unref(send),
73
90
  onAbort: unref(abort)
74
91
  }, null, 8, [
92
+ "model",
93
+ "reasoning-level",
75
94
  "running",
95
+ "models",
76
96
  "placeholder",
77
97
  "accept",
78
98
  "max-attachment-size",
@@ -1 +1 @@
1
- {"version":3,"file":"ai-chat.js","names":["$slots"],"sources":["../../../src/components/ai-chat/ai-chat.vue"],"sourcesContent":["<template>\n <div :class=\"cls.b\">\n <MessageList\n :messages=\"messages\"\n :welcome=\"welcome\"\n :renderer-props=\"rendererProps\"\n @respond=\"respondToolCall\"\n @regenerate=\"regenerate\"\n >\n <template v-if=\"$slots.welcome\" #welcome>\n <slot name=\"welcome\" />\n </template>\n </MessageList>\n\n <ChatInput\n :running=\"running\"\n :placeholder=\"placeholder\"\n :accept=\"accept\"\n :max-attachment-size=\"maxAttachmentSize\"\n @send=\"send\"\n @abort=\"abort\"\n />\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { bem } from '@veltra/utils'\nimport { computed, provide, useSlots } from 'vue'\n\nimport type { ChatTool, ChatToolCall } from '../../chat/types'\nimport { useChat } from '../../chat/use-chat'\nimport type { _AiChatExposed, AiChatEmits, AiChatProps } from '../../types'\nimport ChatInput from './chat-input.vue'\nimport { AiChatDIKey } from './di'\nimport MessageList from './message-list.vue'\n\ndefineOptions({ name: 'UAiChat' })\n\nconst props = defineProps<AiChatProps>()\n\nconst emit = defineEmits<AiChatEmits>()\n\ndefineSlots<{\n /** 空状态欢迎区插槽 */\n welcome(): any\n /** 按工具名自定义工具结果展示,如 tool-getWeather */\n [name: `tool-${string}`]: (scope: { toolCall: ChatToolCall }) => any\n}>()\n\nconst cls = bem('ai-chat')\n\nconst slots = useSlots()\n\n/** 按 name 索引的工具定义,供工具卡片解析 icon/label/render/autoCollapse */\nconst toolMap = computed<Record<string, ChatTool | undefined>>(() => {\n return Object.fromEntries((props.tools ?? []).map((tool) => [tool.name, tool]))\n})\n\nprovide(AiChatDIKey, { cls, slots, tools: toolMap })\n\nconst { messages, running, send, abort, regenerate, clear, respondToolCall } = useChat({\n props,\n emit\n})\n\ndefineExpose<_AiChatExposed>({ send, abort, regenerate, clear })\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsCA,MAAM,QAAQ;EAEd,MAAM,OAAO;EASb,MAAM,MAAM,IAAI,SAAS;EASzB,QAAQ,aAAa;GAAE;GAAK,OAPd,SAOc;GAAO,OAJnB,eAAqD;IACnE,OAAO,OAAO,aAAa,MAAM,SAAS,CAAC,EAAA,CAAG,KAAK,SAAS,CAAC,KAAK,MAAM,IAAI,CAAC,CAAC;GAChF,CAE0C;EAAQ,CAAC;EAEnD,MAAM,EAAE,UAAU,SAAS,MAAM,OAAO,YAAY,OAAO,oBAAoB,QAAQ;GACrF;GACA;EACF,CAAC;EAED,SAA6B;GAAE;GAAM;GAAO;GAAY;EAAM,CAAC;;uBAhE7D,mBAqBM,OAAA,EArBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,CAAC,EAAA,GAAA,CAChB,YAUc,aAAA;IATX,UAAU,MAAA,QAAA;IACV,SAAS,QAAA;IACT,kBAAgB,QAAA;IAChB,WAAS,MAAA,eAAA;IACT,cAAY,MAAA,UAAA;6BAEGA,KAAAA,OAAO,UAAA;UAAU;sBACR,CAAvB,WAAuB,KAAA,QAAA,SAAA,CAAA,CAAA;;;;;;;;OAI3B,YAOE,aAAA;IANC,SAAS,MAAA,OAAA;IACT,aAAa,QAAA;IACb,QAAQ,QAAA;IACR,uBAAqB,QAAA;IACrB,QAAM,MAAA,IAAA;IACN,SAAO,MAAA,KAAA"}
1
+ {"version":3,"file":"ai-chat.js","names":["$slots"],"sources":["../../../src/components/ai-chat/ai-chat.vue"],"sourcesContent":["<template>\n <div :class=\"cls.b\">\n <MessageList\n :messages=\"messages\"\n :welcome=\"welcome\"\n :renderer-props=\"rendererProps\"\n @respond=\"respondToolCall\"\n @regenerate=\"regenerate\"\n >\n <template v-if=\"$slots.welcome\" #welcome>\n <slot name=\"welcome\" />\n </template>\n </MessageList>\n\n <ChatInput\n v-model:model=\"model\"\n v-model:reasoning-level=\"reasoningLevel\"\n :running=\"running\"\n :models=\"models\"\n :placeholder=\"placeholder\"\n :accept=\"accept\"\n :max-attachment-size=\"maxAttachmentSize\"\n @send=\"send\"\n @abort=\"abort\"\n />\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { bem } from '@veltra/utils'\nimport { computed, provide, useSlots } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport { useChat } from '../../chat/use-chat'\nimport { createBuiltinTools } from '../../tools'\nimport type { _AiChatExposed, AiChatEmits, AiChatProps } from '../../types'\nimport ChatInput from './chat-input.vue'\nimport { AiChatDIKey } from './di'\nimport MessageList from './message-list.vue'\n\ndefineOptions({ name: 'UAiChat' })\n\nconst props = defineProps<AiChatProps>()\n\nconst emit = defineEmits<AiChatEmits>()\n\ndefineSlots<{\n /** 空状态欢迎区插槽 */\n welcome(): any\n /** 按工具名自定义工具结果展示,如 tool-getWeather */\n [name: `tool-${string}`]: (scope: { toolCall: ChatToolCall }) => any\n}>()\n\nconst cls = bem('ai-chat')\n\nconst slots = useSlots()\n\n/** 内置 + 用户工具(同名内置优先),供工具卡片解析 icon/label/render/autoCollapse */\nconst toolMap = computed(() => {\n const builtins = createBuiltinTools()\n const names = new Set(builtins.map((t) => t.name))\n const tools = [...builtins, ...(props.tools ?? []).filter((t) => !names.has(t.name))]\n return Object.fromEntries(tools.map((t) => [t.name, t]))\n})\n\nprovide(AiChatDIKey, { cls, slots, tools: toolMap })\n\nconst {\n messages,\n model,\n reasoningLevel,\n running,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall\n} = useChat({ props, emit })\n\ndefineExpose<_AiChatExposed>({ send, abort, regenerate, clear })\n</script>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA0CA,MAAM,QAAQ;EAEd,MAAM,OAAO;EASb,MAAM,MAAM,IAAI,SAAS;EAEzB,MAAM,QAAQ,SAAS;;EAGvB,MAAM,UAAU,eAAe;GAC7B,MAAM,WAAW,mBAAmB;GACpC,MAAM,QAAQ,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,IAAI,CAAC;GACjD,MAAM,QAAQ,CAAC,GAAG,UAAU,IAAI,MAAM,SAAS,CAAC,EAAA,CAAG,QAAQ,MAAM,CAAC,MAAM,IAAI,EAAE,IAAI,CAAC,CAAC;GACpF,OAAO,OAAO,YAAY,MAAM,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;EACzD,CAAC;EAED,QAAQ,aAAa;GAAE;GAAK;GAAO,OAAO;EAAQ,CAAC;EAEnD,MAAM,EACJ,UACA,OACA,gBACA,SACA,MACA,OACA,YACA,OACA,oBACE,QAAQ;GAAE;GAAO;EAAK,CAAC;EAE3B,SAA6B;GAAE;GAAM;GAAO;GAAY;EAAM,CAAC;;GA9E7D,OAAA,UAAA,GAAA,mBAwBM,OAAA,EAxBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,CAAC,EAAA,GAChB,CAAA,YAUc,aAAA;IATX,UAAU,MAAA,QAAA;IACV,SAAS,QAAA;IACT,kBAAgB,QAAA;IAChB,WAAS,MAAA,eAAA;IACT,cAAY,MAAA,UAAA;GAEGA,GAAAA,YAAAA,EAAAA,GAAAA,EAAAA,GAAAA,CAAAA,KAAAA,OAAO,UAAA;IAAU,MAAA;IACR,IAAA,cAAA,CAAvB,WAAuB,KAAA,QAAA,SAAA,CAAA,CAAA;;;;;;;;GAI3B,CAAA,GAAA,YAUE,aAAA;IATQ,OAAO,MAAA,KAAA;IAAA,kBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,MAAA,KAAA,IAAA,MAAK,QAAA,SAAA;IACZ,mBAAiB,MAAA,cAAA;IAAA,2BAAA,OAAA,OAAA,OAAA,MAAA,WAAA,MAAA,cAAA,IAAA,eAAc,QAAA,SAAA;IACtC,SAAS,MAAA,OAAA;IACT,QAAQ,QAAA;IACR,aAAa,QAAA;IACb,QAAQ,QAAA;IACR,uBAAqB,QAAA;IACrB,QAAM,MAAA,IAAA;IACN,SAAO,MAAA,KAAA"}
@@ -1,8 +1,8 @@
1
- import { AiChatDIKey } from "./di.js";
2
1
  import { resolveAskQuestion } from "../../tools/ask-question/deferred.js";
2
+ import { AiChatDIKey } from "./di.js";
3
3
  import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, inject, normalizeClass, openBlock, ref, renderList, toDisplayString, unref, watch, withCtx } from "vue";
4
- import { bem } from "@veltra/utils";
5
4
  import { UButton, UInput } from "@veltra/desktop";
5
+ import { bem } from "@veltra/utils";
6
6
  //#region src/components/ai-chat/ask-question.vue
7
7
  const _hoisted_1 = ["onClick"];
8
8
  const _hoisted_2 = ["aria-checked", "onClick"];
@@ -1 +1 @@
1
- {"version":3,"file":"ask-question.js","names":[],"sources":["../../../src/components/ai-chat/ask-question.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('ask-question')\">\n <!-- 作答中:分页向导 -->\n <template v-if=\"answering\">\n <div v-if=\"questions.length > 1\" :class=\"cls.e('ask-question-progress')\">\n <span :class=\"cls.e('ask-question-count')\">{{ current + 1 }} / {{ questions.length }}</span>\n <span :class=\"cls.e('ask-question-dots')\">\n <span\n v-for=\"(_, i) in questions\"\n :key=\"i\"\n :class=\"[\n cls.e('ask-question-dot'),\n bem.is('current', i === current),\n bem.is('answered', isAnswered(i))\n ]\"\n @click=\"goTo(i)\"\n />\n </span>\n </div>\n\n <div :class=\"cls.e('ask-question-q')\">\n <span v-if=\"questions.length > 1\" :class=\"cls.e('ask-question-no')\" aria-hidden=\"true\">\n {{ current + 1 }}\n </span>\n {{ currentQuestion?.question }}\n </div>\n\n <div\n v-if=\"currentQuestion?.options?.length\"\n :class=\"cls.e('ask-question-options')\"\n role=\"radiogroup\"\n >\n <button\n v-for=\"option in currentQuestion.options\"\n :key=\"option\"\n type=\"button\"\n role=\"radio\"\n :aria-checked=\"selections[current] === option\"\n :class=\"[cls.e('ask-question-opt'), bem.is('selected', selections[current] === option)]\"\n @click=\"pickOption(option)\"\n >\n <span\n :class=\"[\n cls.e('ask-question-glyph'),\n bem.is('selected', selections[current] === option)\n ]\"\n aria-hidden=\"true\"\n />\n <span :class=\"cls.e('ask-question-opt-text')\">{{ option }}</span>\n </button>\n </div>\n\n <UInput\n :model-value=\"customs[current]\"\n :placeholder=\"currentQuestion?.placeholder ?? '自定义回答…'\"\n @update:model-value=\"onCustomInput\"\n />\n\n <footer :class=\"cls.e('ask-question-actions')\">\n <UButton\n v-if=\"questions.length > 1\"\n size=\"small\"\n text\n :disabled=\"current === 0\"\n @click=\"goTo(current - 1)\"\n >\n 上一个\n </UButton>\n <UButton\n v-if=\"current < questions.length - 1\"\n size=\"small\"\n type=\"primary\"\n @click=\"goTo(current + 1)\"\n >\n 下一个\n </UButton>\n <UButton v-else size=\"small\" type=\"primary\" :disabled=\"!allAnswered\" @click=\"handleSubmit\">\n 提交\n </UButton>\n </footer>\n </template>\n\n <!-- 提交后:问答摘要 -->\n <template v-else-if=\"toolCall.status === 'success'\">\n <div v-for=\"(item, i) in doneAnswers\" :key=\"i\" :class=\"cls.e('ask-question-result')\">\n <div :class=\"cls.e('ask-question-result-q')\">\n <span\n v-if=\"doneAnswers.length > 1\"\n :class=\"cls.e('ask-question-result-no')\"\n aria-hidden=\"true\"\n >\n {{ i + 1 }}\n </span>\n {{ item.question }}\n </div>\n <div :class=\"cls.e('ask-question-result-a')\">\n <span v-if=\"item.isOption\" :class=\"cls.e('ask-question-chip')\">{{ item.answer }}</span>\n <template v-else>{{ item.answer }}</template>\n </div>\n </div>\n </template>\n\n <!-- 异常 / 取消 -->\n <div v-else :class=\"cls.e('ask-question-status')\">{{ statusText }}</div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UInput } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport type {\n AskQuestionArgs,\n AskQuestionItem,\n AskQuestionResult\n} from '../../tools/ask-question/ask-question'\nimport { resolveAskQuestion } from '../../tools/ask-question/deferred'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatAskQuestion' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 模型输出的提问参数 */\nconst questions = computed<AskQuestionItem[]>(() => {\n try {\n const args = JSON.parse(props.toolCall.arguments || '{}') as Partial<AskQuestionArgs>\n return Array.isArray(args.questions) ? args.questions : []\n } catch {\n return []\n }\n})\n\n/** 作答中:execute 挂起等待用户提交 */\nconst answering = computed(() => {\n return (\n props.toolCall.result == null &&\n (props.toolCall.status === 'running' || props.toolCall.status === 'pending')\n )\n})\n\n/** 当前题索引 */\nconst current = ref(0)\n/** 各题选中的预设选项 */\nconst selections = ref<(string | null)[]>([])\n/** 各题的自定义输入 */\nconst customs = ref<string[]>([])\n\nwatch(\n questions,\n (list) => {\n if (props.toolCall.result != null) return\n selections.value = list.map(() => null)\n customs.value = list.map(() => '')\n },\n { immediate: true }\n)\n\nconst currentQuestion = computed(() => questions.value[current.value])\n\n/** 某题是否已作答:自定义输入非空优先,否则看选中项 */\nconst isAnswered = (index: number) => {\n return Boolean(customs.value[index]?.trim() || selections.value[index])\n}\n\nconst allAnswered = computed(() => {\n return questions.value.length > 0 && questions.value.every((_, i) => isAnswered(i))\n})\n\nconst goTo = (index: number) => {\n if (index < 0 || index >= questions.value.length) return\n current.value = index\n}\n\n/** 点选选项:再次点击取消选择;选中时清空自定义输入(两者互斥) */\nconst pickOption = (option: string) => {\n const i = current.value\n const selected = selections.value[i] === option ? null : option\n selections.value[i] = selected\n if (selected) customs.value[i] = ''\n}\n\n/** 自定义输入:有内容时清除已选选项(两者互斥) */\nconst onCustomInput = (value: string) => {\n customs.value[current.value] = value\n if (value) selections.value[current.value] = null\n}\n\nconst handleSubmit = () => {\n const firstUnanswered = questions.value.findIndex((_, i) => !isAnswered(i))\n if (firstUnanswered >= 0) {\n current.value = firstUnanswered\n return\n }\n const answers = questions.value.map((item, i) => ({\n question: item.question,\n answer: customs.value[i]?.trim() || selections.value[i] || ''\n }))\n resolveAskQuestion(props.toolCall.id, { answers })\n}\n\n/** 提交后的问答摘要(含历史恢复:从序列化结果解析) */\nconst doneAnswers = computed(() => {\n if (props.toolCall.status !== 'success' || !props.toolCall.result) return []\n let result: AskQuestionResult\n try {\n result = JSON.parse(props.toolCall.result)\n } catch {\n return []\n }\n if (!Array.isArray(result?.answers)) return []\n return result.answers.map((item) => ({\n ...item,\n /** 命中预设选项的回答渲染为 chip */\n isOption: questions.value.some(\n (q) => q.question === item.question && q.options?.includes(item.answer)\n )\n }))\n})\n\nconst statusText = computed(() => {\n if (props.toolCall.status !== 'error') return ''\n const err = props.toolCall.error ?? ''\n if (err.includes('Abort') || err.includes('取消')) return '已取消'\n return err || '提问失败'\n})\n</script>\n"],"mappings":"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"}
1
+ {"version":3,"file":"ask-question.js","names":[],"sources":["../../../src/components/ai-chat/ask-question.vue"],"sourcesContent":["<template>\n <div :class=\"cls.e('ask-question')\">\n <!-- 作答中:分页向导 -->\n <template v-if=\"answering\">\n <div v-if=\"questions.length > 1\" :class=\"cls.e('ask-question-progress')\">\n <span :class=\"cls.e('ask-question-count')\">{{ current + 1 }} / {{ questions.length }}</span>\n <span :class=\"cls.e('ask-question-dots')\">\n <span\n v-for=\"(_, i) in questions\"\n :key=\"i\"\n :class=\"[\n cls.e('ask-question-dot'),\n bem.is('current', i === current),\n bem.is('answered', isAnswered(i))\n ]\"\n @click=\"goTo(i)\"\n />\n </span>\n </div>\n\n <div :class=\"cls.e('ask-question-q')\">\n <span v-if=\"questions.length > 1\" :class=\"cls.e('ask-question-no')\" aria-hidden=\"true\">\n {{ current + 1 }}\n </span>\n {{ currentQuestion?.question }}\n </div>\n\n <div\n v-if=\"currentQuestion?.options?.length\"\n :class=\"cls.e('ask-question-options')\"\n role=\"radiogroup\"\n >\n <button\n v-for=\"option in currentQuestion.options\"\n :key=\"option\"\n type=\"button\"\n role=\"radio\"\n :aria-checked=\"selections[current] === option\"\n :class=\"[cls.e('ask-question-opt'), bem.is('selected', selections[current] === option)]\"\n @click=\"pickOption(option)\"\n >\n <span\n :class=\"[\n cls.e('ask-question-glyph'),\n bem.is('selected', selections[current] === option)\n ]\"\n aria-hidden=\"true\"\n />\n <span :class=\"cls.e('ask-question-opt-text')\">{{ option }}</span>\n </button>\n </div>\n\n <UInput\n :model-value=\"customs[current]\"\n :placeholder=\"currentQuestion?.placeholder ?? '自定义回答…'\"\n @update:model-value=\"onCustomInput\"\n />\n\n <footer :class=\"cls.e('ask-question-actions')\">\n <UButton\n v-if=\"questions.length > 1\"\n size=\"small\"\n text\n :disabled=\"current === 0\"\n @click=\"goTo(current - 1)\"\n >\n 上一个\n </UButton>\n <UButton\n v-if=\"current < questions.length - 1\"\n size=\"small\"\n type=\"primary\"\n @click=\"goTo(current + 1)\"\n >\n 下一个\n </UButton>\n <UButton v-else size=\"small\" type=\"primary\" :disabled=\"!allAnswered\" @click=\"handleSubmit\">\n 提交\n </UButton>\n </footer>\n </template>\n\n <!-- 提交后:问答摘要 -->\n <template v-else-if=\"toolCall.status === 'success'\">\n <div v-for=\"(item, i) in doneAnswers\" :key=\"i\" :class=\"cls.e('ask-question-result')\">\n <div :class=\"cls.e('ask-question-result-q')\">\n <span\n v-if=\"doneAnswers.length > 1\"\n :class=\"cls.e('ask-question-result-no')\"\n aria-hidden=\"true\"\n >\n {{ i + 1 }}\n </span>\n {{ item.question }}\n </div>\n <div :class=\"cls.e('ask-question-result-a')\">\n <span v-if=\"item.isOption\" :class=\"cls.e('ask-question-chip')\">{{ item.answer }}</span>\n <template v-else>{{ item.answer }}</template>\n </div>\n </div>\n </template>\n\n <!-- 异常 / 取消 -->\n <div v-else :class=\"cls.e('ask-question-status')\">{{ statusText }}</div>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { UButton, UInput } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, inject, ref, watch } from 'vue'\n\nimport type { ChatToolCall } from '../../chat/types'\nimport type {\n AskQuestionArgs,\n AskQuestionItem,\n AskQuestionResult\n} from '../../tools/ask-question/ask-question'\nimport { resolveAskQuestion } from '../../tools/ask-question/deferred'\nimport { AiChatDIKey } from './di'\n\ndefineOptions({ name: 'UAiChatAskQuestion' })\n\nconst props = defineProps<{ toolCall: ChatToolCall }>()\n\nconst di = inject(AiChatDIKey)\nconst cls = di?.cls ?? bem('ai-chat')\n\n/** 模型输出的提问参数 */\nconst questions = computed<AskQuestionItem[]>(() => {\n try {\n const args = JSON.parse(props.toolCall.arguments || '{}') as Partial<AskQuestionArgs>\n return Array.isArray(args.questions) ? args.questions : []\n } catch {\n return []\n }\n})\n\n/** 作答中:execute 挂起等待用户提交 */\nconst answering = computed(() => {\n return (\n props.toolCall.result == null &&\n (props.toolCall.status === 'running' || props.toolCall.status === 'pending')\n )\n})\n\n/** 当前题索引 */\nconst current = ref(0)\n/** 各题选中的预设选项 */\nconst selections = ref<(string | null)[]>([])\n/** 各题的自定义输入 */\nconst customs = ref<string[]>([])\n\nwatch(\n questions,\n (list) => {\n if (props.toolCall.result != null) return\n selections.value = list.map(() => null)\n customs.value = list.map(() => '')\n },\n { immediate: true }\n)\n\nconst currentQuestion = computed(() => questions.value[current.value])\n\n/** 某题是否已作答:自定义输入非空优先,否则看选中项 */\nconst isAnswered = (index: number) => {\n return Boolean(customs.value[index]?.trim() || selections.value[index])\n}\n\nconst allAnswered = computed(() => {\n return questions.value.length > 0 && questions.value.every((_, i) => isAnswered(i))\n})\n\nconst goTo = (index: number) => {\n if (index < 0 || index >= questions.value.length) return\n current.value = index\n}\n\n/** 点选选项:再次点击取消选择;选中时清空自定义输入(两者互斥) */\nconst pickOption = (option: string) => {\n const i = current.value\n const selected = selections.value[i] === option ? null : option\n selections.value[i] = selected\n if (selected) customs.value[i] = ''\n}\n\n/** 自定义输入:有内容时清除已选选项(两者互斥) */\nconst onCustomInput = (value: string) => {\n customs.value[current.value] = value\n if (value) selections.value[current.value] = null\n}\n\nconst handleSubmit = () => {\n const firstUnanswered = questions.value.findIndex((_, i) => !isAnswered(i))\n if (firstUnanswered >= 0) {\n current.value = firstUnanswered\n return\n }\n const answers = questions.value.map((item, i) => ({\n question: item.question,\n answer: customs.value[i]?.trim() || selections.value[i] || ''\n }))\n resolveAskQuestion(props.toolCall.id, { answers })\n}\n\n/** 提交后的问答摘要(含历史恢复:从序列化结果解析) */\nconst doneAnswers = computed(() => {\n if (props.toolCall.status !== 'success' || !props.toolCall.result) return []\n let result: AskQuestionResult\n try {\n result = JSON.parse(props.toolCall.result)\n } catch {\n return []\n }\n if (!Array.isArray(result?.answers)) return []\n return result.answers.map((item) => ({\n ...item,\n /** 命中预设选项的回答渲染为 chip */\n isOption: questions.value.some(\n (q) => q.question === item.question && q.options?.includes(item.answer)\n )\n }))\n})\n\nconst statusText = computed(() => {\n if (props.toolCall.status !== 'error') return ''\n const err = props.toolCall.error ?? ''\n if (err.includes('Abort') || err.includes('取消')) return '已取消'\n return err || '提问失败'\n})\n</script>\n"],"mappings":";;;;;;;;;;;;;EA2HA,MAAM,QAAQ;EAGd,MAAM,MADK,OAAO,WACN,CAAA,EAAI,OAAO,IAAI,SAAS;;EAGpC,MAAM,YAAY,eAAkC;GAClD,IAAI;IACF,MAAM,OAAO,KAAK,MAAM,MAAM,SAAS,aAAa,IAAI;IACxD,OAAO,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK,YAAY,CAAC;GAC3D,QAAQ;IACN,OAAO,CAAC;GACV;EACF,CAAC;;EAGD,MAAM,YAAY,eAAe;GAC/B,OACE,MAAM,SAAS,UAAU,SACxB,MAAM,SAAS,WAAW,aAAa,MAAM,SAAS,WAAW;EAEtE,CAAC;;EAGD,MAAM,UAAU,IAAI,CAAC;;EAErB,MAAM,aAAa,IAAuB,CAAC,CAAC;;EAE5C,MAAM,UAAU,IAAc,CAAC,CAAC;EAEhC,MACE,YACC,SAAS;GACR,IAAI,MAAM,SAAS,UAAU,MAAM;GACnC,WAAW,QAAQ,KAAK,UAAU,IAAI;GACtC,QAAQ,QAAQ,KAAK,UAAU,EAAE;EACnC,GACA,EAAE,WAAW,KAAK,CACpB;EAEA,MAAM,kBAAkB,eAAe,UAAU,MAAM,QAAQ,MAAM;;EAGrE,MAAM,cAAc,UAAkB;GACpC,OAAO,QAAQ,QAAQ,MAAM,MAAM,EAAE,KAAK,KAAK,WAAW,MAAM,MAAM;EACxE;EAEA,MAAM,cAAc,eAAe;GACjC,OAAO,UAAU,MAAM,SAAS,KAAK,UAAU,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,CAAC;EACpF,CAAC;EAED,MAAM,QAAQ,UAAkB;GAC9B,IAAI,QAAQ,KAAK,SAAS,UAAU,MAAM,QAAQ;GAClD,QAAQ,QAAQ;EAClB;;EAGA,MAAM,cAAc,WAAmB;GACrC,MAAM,IAAI,QAAQ;GAClB,MAAM,WAAW,WAAW,MAAM,OAAO,SAAS,OAAO;GACzD,WAAW,MAAM,KAAK;GACtB,IAAI,UAAU,QAAQ,MAAM,KAAK;EACnC;;EAGA,MAAM,iBAAiB,UAAkB;GACvC,QAAQ,MAAM,QAAQ,SAAS;GAC/B,IAAI,OAAO,WAAW,MAAM,QAAQ,SAAS;EAC/C;EAEA,MAAM,qBAAqB;GACzB,MAAM,kBAAkB,UAAU,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;GAC1E,IAAI,mBAAmB,GAAG;IACxB,QAAQ,QAAQ;IAChB;GACF;GACA,MAAM,UAAU,UAAU,MAAM,KAAK,MAAM,OAAO;IAChD,UAAU,KAAK;IACf,QAAQ,QAAQ,MAAM,EAAE,EAAE,KAAK,KAAK,WAAW,MAAM,MAAM;GAC7D,EAAE;GACF,mBAAmB,MAAM,SAAS,IAAI,EAAE,QAAQ,CAAC;EACnD;;EAGA,MAAM,cAAc,eAAe;GACjC,IAAI,MAAM,SAAS,WAAW,aAAa,CAAC,MAAM,SAAS,QAAQ,OAAO,CAAC;GAC3E,IAAI;GACJ,IAAI;IACF,SAAS,KAAK,MAAM,MAAM,SAAS,MAAM;GAC3C,QAAQ;IACN,OAAO,CAAC;GACV;GACA,IAAI,CAAC,MAAM,QAAQ,QAAQ,OAAO,GAAG,OAAO,CAAC;GAC7C,OAAO,OAAO,QAAQ,KAAK,UAAU;IACnC,GAAG;;IAEH,UAAU,UAAU,MAAM,MACvB,MAAM,EAAE,aAAa,KAAK,YAAY,EAAE,SAAS,SAAS,KAAK,MAAM,CACxE;GACF,EAAE;EACJ,CAAC;EAED,MAAM,aAAa,eAAe;GAChC,IAAI,MAAM,SAAS,WAAW,SAAS,OAAO;GAC9C,MAAM,MAAM,MAAM,SAAS,SAAS;GACpC,IAAI,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,IAAI,GAAG,OAAO;GACxD,OAAO,OAAO;EAChB,CAAC;;GArOC,OAAA,UAAA,GAAA,mBAuGM,OAAA,EAvGA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,cAAA,CAAA,EAAA,GAChB,CAAA,mBAAA,YAAA,GACgB,UAAA,SAAA,UAAA,GAAhB,mBA6EW,UAAA,EAAA,KAAA,EAAA,GAAA;IA5EE,UAAA,MAAU,SAAM,KAAA,UAAA,GAA3B,mBAcM,OAAA;;KAd4B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,uBAAA,CAAA;IAC5C,GAAA,CAAA,mBAA4F,QAAA,EAArF,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,oBAAA,CAAA,EAAA,GAA2B,gBAAA,QAAA,QAAO,CAAA,IAAO,QAAG,gBAAG,UAAA,MAAU,MAAM,GAAA,CAAA,GAClF,mBAWO,QAAA,EAXA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA,EAAA,GACjB,EAAA,UAAA,IAAA,GAAA,mBASE,UAAA,MAAA,WARiB,UAAA,QAAT,GAAG,MAAC;KADd,OAAA,UAAA,GAAA,mBASE,QAAA;MAPC,KAAK;MACL,OAAK,eAAA;OAAkB,MAAA,GAAA,CAAG,CAAC,EAAC,kBAAA;OAAoC,MAAA,GAAA,CAAG,CAAC,GAAE,WAAY,MAAM,QAAA,KAAO;OAAiB,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,WAAW,CAAC,CAAA;;MAK/I,UAAK,WAAE,KAAK,CAAC;;;IAKpB,mBAKM,OAAA,EALA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,gBAAA,CAAA,EAAA,GACJ,CAAA,UAAA,MAAU,SAAM,KAAA,UAAA,GAA5B,mBAEO,QAAA;;KAF4B,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,iBAAA,CAAA;KAAqB,eAAY;IAC3E,GAAA,gBAAA,QAAA,QAAO,CAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,GAAA,gBACL,MACP,gBAAG,gBAAA,OAAiB,QAAQ,GAAA,CAAA,CAAA,GAAA,CAAA;IAItB,gBAAA,OAAiB,SAAS,UAAA,UAAA,GADlC,mBAuBM,OAAA;;KArBH,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,sBAAA,CAAA;KACb,MAAK;IAEL,GAAA,EAAA,UAAA,IAAA,GAAA,mBAiBS,UAAA,MAAA,WAhBU,gBAAA,MAAgB,UAA1B,WAAM;KADf,OAAA,UAAA,GAAA,mBAiBS,UAAA;MAfN,KAAK;MACN,MAAK;MACL,MAAK;MACJ,gBAAc,WAAA,MAAW,QAAA,WAAa;MACtC,OAAK,eAAA,CAAG,MAAA,GAAA,CAAG,CAAC,EAAC,kBAAA,GAAsB,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,WAAA,MAAW,QAAA,WAAa,MAAM,CAAA,CAAA;MACpF,UAAK,WAAE,WAAW,MAAM;KAEzB,GAAA,CAAA,mBAME,QAAA;MALC,OAAK,eAAA,CAAkB,MAAA,GAAA,CAAG,CAAC,EAAC,oBAAA,GAAsC,MAAA,GAAA,CAAG,CAAC,GAAE,YAAa,WAAA,MAAW,QAAA,WAAa,MAAM,CAAA,CAAA;MAIpH,eAAY;KAEd,GAAA,MAAA,CAAA,GAAA,mBAAiE,QAAA,EAA1D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,uBAAA,CAAA,EAAA,GAA8B,gBAAA,MAAM,GAAA,CAAA,CAAA,GAAA,IAAA,UAAA;;IAI3D,YAIE,MAAA,MAAA,GAAA;KAHC,eAAa,QAAA,MAAQ,QAAA;KACrB,aAAa,gBAAA,OAAiB,eAAW;KACzC,uBAAoB;;IAGvB,mBAqBS,UAAA,EArBA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,sBAAA,CAAA,EAAA,GAEX,CAAA,UAAA,MAAU,SAAM,KAAA,UAAA,GADxB,YAQU,MAAA,OAAA,GAAA;;KANR,MAAK;KACL,MAAA;KACC,UAAU,QAAA,UAAO;KACjB,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAK,QAAA,QAAO,CAAA;;KAGtB,SAAA,cAAA,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,gBAFC,SAED,EAAA,CAAA,EAAA,CAAA;;IAEQ,GAAA,GAAA,CAAA,UAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,GAAA,QAAA,QAAU,UAAA,MAAU,SAAM,KAAA,UAAA,GADlC,YAOU,MAAA,OAAA,GAAA;;KALR,MAAK;KACL,MAAK;KACJ,SAAK,OAAA,OAAA,OAAA,MAAA,WAAE,KAAK,QAAA,QAAO,CAAA;;KAGtB,SAAA,cAAA,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,gBAFC,SAED,EAAA,CAAA,EAAA,CAAA;;IACA,CAAA,MAAA,UAAA,GAAA,YAEU,MAAA,OAAA,GAAA;;KAFM,MAAK;KAAQ,MAAK;KAAW,UAAQ,CAAG,YAAA;KAAc,SAAO;;KAE7E,SAAA,cAAA,CAAA,GAAA,OAAA,OAAA,OAAA,KAAA,CAAA,gBAF2F,QAE3F,EAAA,CAAA,EAAA,CAAA;;;GAKiB,GAAA,EAAA,KAAA,QAAA,SAAS,WAAM,aAAA,UAAA,GAApC,mBAiBW,UAAA,EAAA,KAAA,EAAA,GAAA,CAlBX,mBAAA,YAAA,IAAA,UAAA,IAAA,GAEE,mBAeM,UAAA,MAAA,WAfmB,YAAA,QAAZ,MAAM,MAAC;IAApB,OAAA,UAAA,GAAA,mBAeM,OAAA;KAfiC,KAAK;KAAI,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,qBAAA,CAAA;IAC1D,GAAA,CAAA,mBASM,OAAA,EATA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,uBAAA,CAAA,EAAA,GAER,CAAA,YAAA,MAAY,SAAM,KAAA,UAAA,GAD1B,mBAMO,QAAA;;KAJJ,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,wBAAA,CAAA;KACb,eAAY;IAET,GAAA,gBAAA,IAAC,CAAA,GAAA,CAAA,KAAA,mBAAA,QAAA,IAAA,GAAA,gBACC,MACP,gBAAG,KAAK,QAAQ,GAAA,CAAA,CAAA,GAAA,CAElB,GAAA,mBAGM,OAAA,EAHA,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,uBAAA,CAAA,EAAA,GACJ,CAAA,KAAK,YAAA,UAAA,GAAjB,mBAAuF,QAAA;;KAA3D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,mBAAA,CAAA;IAA0B,GAAA,gBAAA,KAAK,MAAM,GAAA,CAAA,MAAA,UAAA,GAC7E,mBAA6C,UAAA,EAAA,KAAA,EAAA,GAAA,CAAA,gBAAzB,gBAAA,KAAK,MAAM,GAAA,CAAA,CAAA,GAAA,EAAA,EAAA,GAAA,CAAA,CAAA,GAAA,CAAA;GAMrC,CAAA,GAAA,GAAA,EAAA,GAAA,EAAA,MAAA,UAAA,GAAA,mBAAwE,UAAA,EAAA,KAAA,EAAA,GAAA,CADxE,mBAAA,WAAA,GACA,mBAAwE,OAAA,EAA3D,OAAK,eAAE,MAAA,GAAA,CAAG,CAAC,EAAC,qBAAA,CAAA,EAAA,GAA4B,gBAAA,WAAA,KAAU,GAAA,CAAA,CAAA,GAAA,IAAA,EAAA,GAAA,CAAA"}