@veltra/ai 1.4.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/index.d.ts +3 -3
- package/dist/chat/transports/index.d.ts +2 -2
- package/dist/chat/transports/openai.d.ts +16 -12
- package/dist/chat/transports/openai.js +56 -15
- package/dist/chat/transports/openai.js.map +1 -1
- package/dist/chat/types.d.ts +41 -2
- package/dist/chat/use-chat.d.ts +26 -1
- package/dist/chat/use-chat.js +132 -13
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +222 -41
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/ask-question.js +2 -2
- package/dist/components/ai-chat/ask-question.js.map +1 -1
- package/dist/components/ai-chat/chat-input.js +81 -44
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/di.js.map +1 -1
- package/dist/components/ai-chat/message-item.js +101 -23
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +217 -32
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/model-picker.js +140 -0
- package/dist/components/ai-chat/model-picker.js.map +1 -0
- package/dist/components/ai-chat/queue-list.js +70 -0
- package/dist/components/ai-chat/queue-list.js.map +1 -0
- package/dist/components/ai-chat/side-panel.js +96 -0
- package/dist/components/ai-chat/side-panel.js.map +1 -0
- package/dist/components/ai-chat/style.d.ts +6 -0
- package/dist/components/ai-chat/style.js +6 -0
- package/dist/components/ai-chat/style2.css +552 -41
- package/dist/components/ai-chat/tool-call.js +61 -27
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-orb/ai-orb.js +94 -0
- package/dist/components/ai-orb/ai-orb.js.map +1 -0
- package/dist/components/ai-orb/orb-renderer.js +411 -0
- package/dist/components/ai-orb/orb-renderer.js.map +1 -0
- package/dist/components/ai-orb/style.d.ts +1 -0
- package/dist/components/ai-orb/style.js +1 -0
- package/dist/components/ai-orb/style2.css +23 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +2 -2
- package/dist/providers/types.d.ts +47 -0
- package/dist/style.js +1 -0
- package/dist/tools/ask-question/ask-question.d.ts +2 -17
- package/dist/tools/ask-question/index.d.ts +2 -2
- package/dist/tools/index.d.ts +3 -2
- package/dist/tools/index.js +10 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/types/ai-chat.d.ts +35 -7
- package/dist/types/ai-orb.d.ts +27 -0
- package/dist/types/index.d.ts +2 -1
- package/package.json +8 -8
- package/src/chat/__test__/use-chat.test.ts +295 -9
- package/src/chat/transports/__test__/openai.test.ts +317 -0
- package/src/chat/transports/openai.ts +88 -26
- package/src/chat/types.ts +41 -1
- package/src/chat/use-chat.ts +177 -14
- package/src/components/ai-chat/__test__/ai-chat.test.ts +690 -12
- package/src/components/ai-chat/ai-chat.vue +241 -31
- package/src/components/ai-chat/chat-input.vue +78 -31
- package/src/components/ai-chat/di.ts +2 -0
- package/src/components/ai-chat/message-item.vue +98 -11
- package/src/components/ai-chat/message-list.vue +261 -21
- package/src/components/ai-chat/model-picker.vue +177 -0
- package/src/components/ai-chat/queue-list.vue +63 -0
- package/src/components/ai-chat/side-panel.vue +76 -0
- package/src/components/ai-chat/style.scss +621 -43
- package/src/components/ai-chat/style.ts +6 -0
- package/src/components/ai-chat/tool-call.vue +86 -42
- package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
- package/src/components/ai-orb/ai-orb.vue +107 -0
- package/src/components/ai-orb/index.ts +1 -0
- package/src/components/ai-orb/orb-renderer.ts +505 -0
- package/src/components/ai-orb/style.scss +10 -0
- package/src/components/ai-orb/style.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/index.ts +8 -1
- package/src/providers/index.ts +1 -0
- package/src/providers/types.ts +46 -0
- package/src/style.ts +1 -0
- package/src/tools/ask-question/__test__/ask-question.test.ts +4 -4
- package/src/tools/index.ts +14 -1
- package/src/types/ai-chat.ts +35 -6
- package/src/types/ai-orb.ts +27 -0
- package/src/types/index.ts +1 -0
- package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
- package/dist/components/ai-chat/index.d.ts +0 -2
- package/dist/components/index.d.ts +0 -3
package/dist/chat/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
6
|
-
|
|
7
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
22
|
+
* 按 request.model 路由到对应 Provider 的 endpoint;手写 fetch + SSE,无第三方依赖。
|
|
19
23
|
*/
|
|
20
|
-
declare function createOpenAITransport(options: OpenAITransportOptions):
|
|
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
|
|
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
|
-
*
|
|
155
|
+
* 按 request.model 路由到对应 Provider 的 endpoint;手写 fetch + SSE,无第三方依赖。
|
|
130
156
|
*/
|
|
131
157
|
function createOpenAITransport(options) {
|
|
132
|
-
const {
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
...
|
|
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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"}
|
package/dist/chat/types.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
package/dist/chat/use-chat.d.ts
CHANGED
|
@@ -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 };
|
package/dist/chat/use-chat.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|