@veltra/ai 2.1.3 → 2.1.4
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 +2 -2
- package/dist/chat/transports/openai.js +52 -6
- package/dist/chat/transports/openai.js.map +1 -1
- package/dist/chat/types.d.ts +31 -1
- package/dist/chat/use-chat.d.ts +31 -1
- package/dist/chat/use-chat.js +33 -1
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +17 -4
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/chat-input.js +75 -20
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/message-list.js +15 -0
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/style.d.ts +1 -0
- package/dist/components/ai-chat/style.js +1 -0
- package/dist/components/ai-chat/style2.css +28 -1
- package/dist/index.d.ts +2 -2
- package/dist/types/ai-chat.d.ts +11 -2
- package/package.json +1 -1
- package/src/chat/__test__/use-chat.test.ts +67 -0
- package/src/chat/transports/__test__/openai.test.ts +140 -0
- package/src/chat/transports/openai.ts +73 -7
- package/src/chat/types.ts +31 -0
- package/src/chat/use-chat.ts +39 -1
- package/src/components/ai-chat/__test__/ai-chat.test.ts +121 -1
- package/src/components/ai-chat/ai-chat.vue +13 -0
- package/src/components/ai-chat/chat-input.vue +64 -3
- package/src/components/ai-chat/message-list.vue +22 -0
- package/src/components/ai-chat/style.scss +31 -1
- package/src/components/ai-chat/style.ts +1 -0
- package/src/types/ai-chat.ts +11 -1
package/dist/chat/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./types.js";
|
|
1
|
+
import { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./types.js";
|
|
2
2
|
import { UseChatOptions, useChat } from "./use-chat.js";
|
|
3
3
|
import { OpenAITransport, OpenAITransportOptions, createOpenAITransport } from "./transports/openai.js";
|
|
4
4
|
import "./transports/index.js";
|
|
5
|
-
export { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, UseChatOptions, createOpenAITransport, useChat };
|
|
5
|
+
export { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, UseChatOptions, createOpenAITransport, useChat };
|
|
@@ -55,6 +55,40 @@ function toOpenAITools(tools) {
|
|
|
55
55
|
}
|
|
56
56
|
}));
|
|
57
57
|
}
|
|
58
|
+
function isRecord(value) {
|
|
59
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
/** 非负有限整数;非法或缺省返回 undefined(不把缺失当成 0) */
|
|
62
|
+
function readCount(value) {
|
|
63
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
|
|
64
|
+
return Math.floor(value);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 从 OpenAI 兼容 usage 对象解析。
|
|
68
|
+
* 缓存字段只在源数据出现时写入;未命中可在「有命中 + prompt」时由差值得出。
|
|
69
|
+
*/
|
|
70
|
+
function parseOpenAIUsage(raw) {
|
|
71
|
+
if (!isRecord(raw)) return null;
|
|
72
|
+
const promptTokens = readCount(raw.prompt_tokens) ?? readCount(raw.input_tokens);
|
|
73
|
+
const completionTokens = readCount(raw.completion_tokens) ?? readCount(raw.output_tokens);
|
|
74
|
+
if (promptTokens == null && completionTokens == null) return null;
|
|
75
|
+
const prompt = promptTokens ?? 0;
|
|
76
|
+
const completion = completionTokens ?? 0;
|
|
77
|
+
const usage = {
|
|
78
|
+
promptTokens: prompt,
|
|
79
|
+
completionTokens: completion,
|
|
80
|
+
totalTokens: readCount(raw.total_tokens) ?? prompt + completion
|
|
81
|
+
};
|
|
82
|
+
const details = isRecord(raw.prompt_tokens_details) ? raw.prompt_tokens_details : void 0;
|
|
83
|
+
const inputDetails = isRecord(raw.input_tokens_details) ? raw.input_tokens_details : void 0;
|
|
84
|
+
const cacheHitTokens = readCount(details?.cached_tokens) ?? readCount(inputDetails?.cached_tokens) ?? readCount(raw.prompt_cache_hit_tokens) ?? readCount(raw.cache_read_input_tokens) ?? readCount(raw.cached_tokens);
|
|
85
|
+
if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens;
|
|
86
|
+
const cacheMissTokens = readCount(raw.prompt_cache_miss_tokens) ?? (cacheHitTokens != null && promptTokens != null ? Math.max(0, promptTokens - cacheHitTokens) : void 0);
|
|
87
|
+
if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens;
|
|
88
|
+
const cacheCreationTokens = readCount(raw.cache_creation_input_tokens);
|
|
89
|
+
if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens;
|
|
90
|
+
return usage;
|
|
91
|
+
}
|
|
58
92
|
/**
|
|
59
93
|
* 逐行解析 SSE 流。
|
|
60
94
|
* 兼容 OpenAI 及 DeepSeek 等 reasoning_content 风格的兼容端点。
|
|
@@ -82,13 +116,19 @@ async function parseSSE(body, signal, handlers) {
|
|
|
82
116
|
} catch {
|
|
83
117
|
return;
|
|
84
118
|
}
|
|
85
|
-
|
|
86
|
-
|
|
119
|
+
if (!isRecord(chunk)) return;
|
|
120
|
+
const usage = parseOpenAIUsage(chunk.usage);
|
|
121
|
+
if (usage) handlers.onUsage?.(usage);
|
|
122
|
+
const choices = chunk.choices;
|
|
123
|
+
const first = Array.isArray(choices) ? choices[0] : void 0;
|
|
124
|
+
const delta = isRecord(first) ? first.delta : void 0;
|
|
125
|
+
if (!isRecord(delta)) return;
|
|
87
126
|
if (typeof delta.content === "string" && delta.content) handlers.onTextDelta(delta.content);
|
|
88
127
|
const reasoning = delta.reasoning_content ?? delta.reasoning;
|
|
89
128
|
if (typeof reasoning === "string" && reasoning) handlers.onReasoningDelta?.(reasoning);
|
|
90
129
|
if (Array.isArray(delta.tool_calls)) for (const tc of delta.tool_calls) {
|
|
91
|
-
|
|
130
|
+
if (!isRecord(tc)) continue;
|
|
131
|
+
const index = typeof tc.index === "number" ? tc.index : 0;
|
|
92
132
|
if (index > lastIndex && accumulated.size > 0) flushToolCalls();
|
|
93
133
|
lastIndex = Math.max(lastIndex, index);
|
|
94
134
|
let call = accumulated.get(index);
|
|
@@ -100,9 +140,10 @@ async function parseSSE(body, signal, handlers) {
|
|
|
100
140
|
};
|
|
101
141
|
accumulated.set(index, call);
|
|
102
142
|
}
|
|
103
|
-
if (tc.id) call.id = tc.id;
|
|
104
|
-
|
|
105
|
-
if (
|
|
143
|
+
if (typeof tc.id === "string") call.id = tc.id;
|
|
144
|
+
const fn = isRecord(tc.function) ? tc.function : void 0;
|
|
145
|
+
if (typeof fn?.name === "string") call.name += fn.name;
|
|
146
|
+
if (typeof fn?.arguments === "string") call.arguments += fn.arguments;
|
|
106
147
|
}
|
|
107
148
|
};
|
|
108
149
|
try {
|
|
@@ -167,10 +208,15 @@ function createOpenAITransport(options) {
|
|
|
167
208
|
handlers.onError?.(/* @__PURE__ */ new Error(`未找到模型 "${modelId}" 对应的 Provider`));
|
|
168
209
|
return;
|
|
169
210
|
}
|
|
211
|
+
const extraStreamOptions = isRecord(globalBody?.stream_options) ? globalBody.stream_options : void 0;
|
|
170
212
|
const requestBody = {
|
|
171
213
|
...globalBody,
|
|
172
214
|
model: modelId,
|
|
173
215
|
stream: true,
|
|
216
|
+
stream_options: {
|
|
217
|
+
...extraStreamOptions,
|
|
218
|
+
include_usage: true
|
|
219
|
+
},
|
|
174
220
|
messages: toOpenAIMessages(messages, systemPrompt),
|
|
175
221
|
...tools?.length ? { tools: toOpenAITools(tools) } : {}
|
|
176
222
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai.js","names":[],"sources":["../../../src/chat/transports/openai.ts"],"sourcesContent":["import type { ChatModelOption, ChatProvider } from '../../providers'\nimport type {\n ChatMessage,\n ChatTool,\n ChatTransport,\n ChatTransportHandlers,\n ChatTransportRequest\n} from '../types'\n\n/** createOpenAITransport 的配置项(多 Provider) */\nexport interface OpenAITransportOptions {\n /** 至少一个 Provider;模型 id 须跨 Provider 全局唯一 */\n providers: ChatProvider[]\n /** 全局额外请求头(与各 Provider headers 合并,Provider 优先) */\n headers?: Record<string, string>\n /** 全局额外请求体字段,如 temperature、top_p */\n body?: Record<string, unknown>\n}\n\n/** 带扁平模型列表元数据的 OpenAI 兼容 transport */\nexport type OpenAITransport = ChatTransport & {\n /** 供 UI 使用的扁平模型列表 */\n readonly models: ChatModelOption[]\n /** 默认模型 id(首个 Provider 的首个模型) */\n readonly defaultModel: string\n}\n\ninterface OpenAIMessage {\n role: string\n content?: unknown\n tool_calls?: unknown[]\n tool_call_id?: string\n}\n\nfunction toOpenAIMessages(messages: ChatMessage[], systemPrompt?: string): OpenAIMessage[] {\n const result: OpenAIMessage[] = []\n\n if (systemPrompt) {\n result.push({ role: 'system', content: systemPrompt })\n }\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n if (msg.attachments?.length) {\n const parts: unknown[] = []\n if (msg.content) parts.push({ type: 'text', text: msg.content })\n for (const att of msg.attachments) {\n parts.push({ type: 'image_url', image_url: { url: att.dataUrl } })\n }\n result.push({ role: 'user', content: parts })\n } else {\n result.push({ role: 'user', content: msg.content })\n }\n } else if (msg.role === 'assistant') {\n const item: OpenAIMessage = { role: 'assistant', content: msg.content || '' }\n if (msg.toolCalls?.length) {\n item.tool_calls = msg.toolCalls.map((call) => ({\n id: call.id,\n type: 'function',\n function: { name: call.name, arguments: call.arguments }\n }))\n }\n result.push(item)\n } else if (msg.role === 'tool') {\n result.push({ role: 'tool', tool_call_id: msg.toolCallId, content: msg.content })\n }\n }\n\n return result\n}\n\nfunction toOpenAITools(tools: ChatTool[]): unknown[] {\n return tools.map((tool) => ({\n type: 'function',\n function: { name: tool.name, description: tool.description, parameters: tool.parameters }\n }))\n}\n\ninterface AccumulatedToolCall {\n id: string\n name: string\n arguments: string\n}\n\n/**\n * 逐行解析 SSE 流。\n * 兼容 OpenAI 及 DeepSeek 等 reasoning_content 风格的兼容端点。\n */\nasync function parseSSE(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n handlers: ChatTransportHandlers\n): Promise<void> {\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n\n // 按 OpenAI 流式协议中 tool_calls 的 index 累积参数片段\n const accumulated = new Map<number, AccumulatedToolCall>()\n let lastIndex = -1\n\n const flushToolCalls = () => {\n if (!handlers.onToolCall) return\n for (const call of accumulated.values()) {\n if (call.id && call.name) {\n handlers.onToolCall({ id: call.id, name: call.name, arguments: call.arguments })\n }\n }\n accumulated.clear()\n }\n\n const handleData = (data: string) => {\n if (data === '[DONE]') return\n\n let chunk: any\n try {\n chunk = JSON.parse(data)\n } catch {\n return\n }\n\n const delta = chunk.choices?.[0]?.delta\n if (!delta) return\n\n if (typeof delta.content === 'string' && delta.content) {\n handlers.onTextDelta(delta.content)\n }\n\n // 兼容 reasoning_content(DeepSeek)与 reasoning(部分 OpenAI 兼容端点)\n const reasoning = delta.reasoning_content ?? delta.reasoning\n if (typeof reasoning === 'string' && reasoning) {\n handlers.onReasoningDelta?.(reasoning)\n }\n\n if (Array.isArray(delta.tool_calls)) {\n for (const tc of delta.tool_calls) {\n const index: number = tc.index ?? 0\n // 同一调用的参数分片共享 index;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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"}
|
|
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 ChatTokenUsage,\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\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** 非负有限整数;非法或缺省返回 undefined(不把缺失当成 0) */\nfunction readCount(value: unknown): number | undefined {\n if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return undefined\n return Math.floor(value)\n}\n\n/**\n * 从 OpenAI 兼容 usage 对象解析。\n * 缓存字段只在源数据出现时写入;未命中可在「有命中 + prompt」时由差值得出。\n */\nfunction parseOpenAIUsage(raw: unknown): ChatTokenUsage | null {\n if (!isRecord(raw)) return null\n\n const promptTokens = readCount(raw.prompt_tokens) ?? readCount(raw.input_tokens)\n const completionTokens = readCount(raw.completion_tokens) ?? readCount(raw.output_tokens)\n if (promptTokens == null && completionTokens == null) return null\n\n const prompt = promptTokens ?? 0\n const completion = completionTokens ?? 0\n const usage: ChatTokenUsage = {\n promptTokens: prompt,\n completionTokens: completion,\n totalTokens: readCount(raw.total_tokens) ?? prompt + completion\n }\n\n const details = isRecord(raw.prompt_tokens_details) ? raw.prompt_tokens_details : undefined\n const inputDetails = isRecord(raw.input_tokens_details) ? raw.input_tokens_details : undefined\n const cacheHitTokens =\n readCount(details?.cached_tokens) ??\n readCount(inputDetails?.cached_tokens) ??\n readCount(raw.prompt_cache_hit_tokens) ??\n readCount(raw.cache_read_input_tokens) ??\n readCount(raw.cached_tokens)\n if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens\n\n const cacheMissTokens =\n readCount(raw.prompt_cache_miss_tokens) ??\n (cacheHitTokens != null && promptTokens != null\n ? Math.max(0, promptTokens - cacheHitTokens)\n : undefined)\n if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens\n\n const cacheCreationTokens = readCount(raw.cache_creation_input_tokens)\n if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens\n\n return usage\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: unknown\n try {\n chunk = JSON.parse(data)\n } catch {\n return\n }\n if (!isRecord(chunk)) return\n\n const usage = parseOpenAIUsage(chunk.usage)\n if (usage) handlers.onUsage?.(usage)\n\n const choices = chunk.choices\n const first = Array.isArray(choices) ? choices[0] : undefined\n const delta = isRecord(first) ? first.delta : undefined\n if (!isRecord(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 if (!isRecord(tc)) continue\n const index = typeof tc.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 (typeof tc.id === 'string') call.id = tc.id\n const fn = isRecord(tc.function) ? tc.function : undefined\n if (typeof fn?.name === 'string') call.name += fn.name\n if (typeof fn?.arguments === 'string') call.arguments += fn.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 extraStreamOptions = isRecord(globalBody?.stream_options)\n ? globalBody.stream_options\n : undefined\n const requestBody: Record<string, unknown> = {\n ...globalBody,\n model: modelId,\n stream: true,\n // 流式默认要末包 usage;OpenAI / DeepSeek 等兼容端点靠这个字段才会返回\n stream_options: { ...extraStreamOptions, include_usage: 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
|
@@ -136,6 +136,31 @@ interface ChatTransportRequest {
|
|
|
136
136
|
/** 中断信号 */
|
|
137
137
|
signal: AbortSignal;
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* 一次模型请求的 token 用量。
|
|
141
|
+
* 仅包含接口实际返回的字段;缓存类数字缺省为 undefined,不补 0。
|
|
142
|
+
*/
|
|
143
|
+
interface ChatTokenUsage {
|
|
144
|
+
/** 输入 token */
|
|
145
|
+
promptTokens: number;
|
|
146
|
+
/** 输出 token */
|
|
147
|
+
completionTokens: number;
|
|
148
|
+
/** 合计 token */
|
|
149
|
+
totalTokens: number;
|
|
150
|
+
/**
|
|
151
|
+
* 缓存命中。
|
|
152
|
+
* 来源:`prompt_tokens_details.cached_tokens` / `prompt_cache_hit_tokens` /
|
|
153
|
+
* `cache_read_input_tokens` / `cached_tokens`。
|
|
154
|
+
*/
|
|
155
|
+
cacheHitTokens?: number;
|
|
156
|
+
/**
|
|
157
|
+
* 缓存未命中。
|
|
158
|
+
* 来源:`prompt_cache_miss_tokens`;若无该字段但有命中与 prompt,则为 prompt − 命中。
|
|
159
|
+
*/
|
|
160
|
+
cacheMissTokens?: number;
|
|
161
|
+
/** 写入缓存(Anthropic 风格 `cache_creation_input_tokens`),有才有 */
|
|
162
|
+
cacheCreationTokens?: number;
|
|
163
|
+
}
|
|
139
164
|
/** transport 流式事件回调 */
|
|
140
165
|
interface ChatTransportHandlers {
|
|
141
166
|
/** 文本增量 */
|
|
@@ -151,6 +176,11 @@ interface ChatTransportHandlers {
|
|
|
151
176
|
name: string;
|
|
152
177
|
arguments: string;
|
|
153
178
|
}): void;
|
|
179
|
+
/**
|
|
180
|
+
* 本次请求的 token 用量(流式末包或非流式 usage)。
|
|
181
|
+
* 接口未返回 usage 时不要调用,不要填 0 充数。
|
|
182
|
+
*/
|
|
183
|
+
onUsage?(usage: ChatTokenUsage): void;
|
|
154
184
|
/** 请求错误 */
|
|
155
185
|
onError?(error: Error): void;
|
|
156
186
|
}
|
|
@@ -160,5 +190,5 @@ interface ChatTransportHandlers {
|
|
|
160
190
|
*/
|
|
161
191
|
type ChatTransport = (request: ChatTransportRequest, handlers: ChatTransportHandlers) => Promise<void> | void;
|
|
162
192
|
//#endregion
|
|
163
|
-
export { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus };
|
|
193
|
+
export { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus };
|
|
164
194
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/chat/use-chat.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChatAttachment, ChatMessage, ChatQueuedMessage } from "./types.js";
|
|
1
|
+
import { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage } 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";
|
|
@@ -36,6 +36,36 @@ declare function useChat(options: UseChatOptions): {
|
|
|
36
36
|
dataUrl: string;
|
|
37
37
|
}[] | undefined;
|
|
38
38
|
}[]>;
|
|
39
|
+
tokenUsage: Ref<{
|
|
40
|
+
promptTokens: number;
|
|
41
|
+
completionTokens: number;
|
|
42
|
+
totalTokens: number;
|
|
43
|
+
cacheHitTokens?: number | undefined;
|
|
44
|
+
cacheMissTokens?: number | undefined;
|
|
45
|
+
cacheCreationTokens?: number | undefined;
|
|
46
|
+
} | null, ChatTokenUsage | {
|
|
47
|
+
promptTokens: number;
|
|
48
|
+
completionTokens: number;
|
|
49
|
+
totalTokens: number;
|
|
50
|
+
cacheHitTokens?: number | undefined;
|
|
51
|
+
cacheMissTokens?: number | undefined;
|
|
52
|
+
cacheCreationTokens?: number | undefined;
|
|
53
|
+
} | null>;
|
|
54
|
+
lastTurnUsage: Ref<{
|
|
55
|
+
promptTokens: number;
|
|
56
|
+
completionTokens: number;
|
|
57
|
+
totalTokens: number;
|
|
58
|
+
cacheHitTokens?: number | undefined;
|
|
59
|
+
cacheMissTokens?: number | undefined;
|
|
60
|
+
cacheCreationTokens?: number | undefined;
|
|
61
|
+
} | null, ChatTokenUsage | {
|
|
62
|
+
promptTokens: number;
|
|
63
|
+
completionTokens: number;
|
|
64
|
+
totalTokens: number;
|
|
65
|
+
cacheHitTokens?: number | undefined;
|
|
66
|
+
cacheMissTokens?: number | undefined;
|
|
67
|
+
cacheCreationTokens?: number | undefined;
|
|
68
|
+
} | null>;
|
|
39
69
|
send: (content: string, attachments?: ChatAttachment[]) => void;
|
|
40
70
|
abort: () => void;
|
|
41
71
|
regenerate: () => void;
|
package/dist/chat/use-chat.js
CHANGED
|
@@ -19,6 +19,25 @@ function resolveTools(userTools) {
|
|
|
19
19
|
const names = new Set(builtins.map((t) => t.name));
|
|
20
20
|
return [...builtins, ...(userTools ?? []).filter((t) => !names.has(t.name))];
|
|
21
21
|
}
|
|
22
|
+
function mergeOptionalCount(a, b) {
|
|
23
|
+
if (a == null && b == null) return void 0;
|
|
24
|
+
return (a ?? 0) + (b ?? 0);
|
|
25
|
+
}
|
|
26
|
+
/** 累加两次 usage;缓存字段只在至少一侧有值时保留 */
|
|
27
|
+
function addTokenUsage(a, b) {
|
|
28
|
+
const usage = {
|
|
29
|
+
promptTokens: a.promptTokens + b.promptTokens,
|
|
30
|
+
completionTokens: a.completionTokens + b.completionTokens,
|
|
31
|
+
totalTokens: a.totalTokens + b.totalTokens
|
|
32
|
+
};
|
|
33
|
+
const cacheHitTokens = mergeOptionalCount(a.cacheHitTokens, b.cacheHitTokens);
|
|
34
|
+
if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens;
|
|
35
|
+
const cacheMissTokens = mergeOptionalCount(a.cacheMissTokens, b.cacheMissTokens);
|
|
36
|
+
if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens;
|
|
37
|
+
const cacheCreationTokens = mergeOptionalCount(a.cacheCreationTokens, b.cacheCreationTokens);
|
|
38
|
+
if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens;
|
|
39
|
+
return usage;
|
|
40
|
+
}
|
|
22
41
|
/**
|
|
23
42
|
* AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。
|
|
24
43
|
* 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。
|
|
@@ -49,6 +68,10 @@ function useChat(options) {
|
|
|
49
68
|
const running = ref(false);
|
|
50
69
|
/** 待发送队列:会话进行中提交的新消息按序排队,收尾后先进先出自动接续 */
|
|
51
70
|
const queue = ref([]);
|
|
71
|
+
/** 当前会话累计 token;从未收到 usage 时为 null */
|
|
72
|
+
const tokenUsage = ref(null);
|
|
73
|
+
/** 最近一轮用户对话(含工具多轮请求)的 token;该轮无 usage 时为 null */
|
|
74
|
+
const lastTurnUsage = ref(null);
|
|
52
75
|
let abortController = null;
|
|
53
76
|
/** needsConfirm 工具的挂起确认器,key 为 toolCallId */
|
|
54
77
|
const confirmResolvers = /* @__PURE__ */ new Map();
|
|
@@ -197,6 +220,7 @@ function useChat(options) {
|
|
|
197
220
|
if (!props.transport) return;
|
|
198
221
|
running.value = true;
|
|
199
222
|
finishedNaturally = false;
|
|
223
|
+
lastTurnUsage.value = null;
|
|
200
224
|
const controller = new AbortController();
|
|
201
225
|
abortController = controller;
|
|
202
226
|
try {
|
|
@@ -248,6 +272,10 @@ function useChat(options) {
|
|
|
248
272
|
assistant.toolCalls.push(toolCall);
|
|
249
273
|
emit("tool-call", toolCall);
|
|
250
274
|
},
|
|
275
|
+
onUsage: (usage) => {
|
|
276
|
+
lastTurnUsage.value = lastTurnUsage.value ? addTokenUsage(lastTurnUsage.value, usage) : usage;
|
|
277
|
+
tokenUsage.value = tokenUsage.value ? addTokenUsage(tokenUsage.value, usage) : usage;
|
|
278
|
+
},
|
|
251
279
|
onError: (error) => {
|
|
252
280
|
requestError = error;
|
|
253
281
|
}
|
|
@@ -310,11 +338,13 @@ function useChat(options) {
|
|
|
310
338
|
snapshot();
|
|
311
339
|
runConversation();
|
|
312
340
|
};
|
|
313
|
-
/**
|
|
341
|
+
/** 清空消息、待发送队列与 token 统计,生成中则先中断 */
|
|
314
342
|
const clear = () => {
|
|
315
343
|
abort();
|
|
316
344
|
queue.value = [];
|
|
317
345
|
messages.value = [];
|
|
346
|
+
tokenUsage.value = null;
|
|
347
|
+
lastTurnUsage.value = null;
|
|
318
348
|
snapshot();
|
|
319
349
|
};
|
|
320
350
|
return {
|
|
@@ -323,6 +353,8 @@ function useChat(options) {
|
|
|
323
353
|
reasoningLevel,
|
|
324
354
|
running,
|
|
325
355
|
queue,
|
|
356
|
+
tokenUsage,
|
|
357
|
+
lastTurnUsage,
|
|
326
358
|
send,
|
|
327
359
|
abort,
|
|
328
360
|
regenerate,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chat.js","names":[],"sources":["../../src/chat/use-chat.ts"],"sourcesContent":["import { useModel } from '@veltra/compositions'\nimport { reactive, ref, watch, type Ref } from 'vue'\n\nimport { createBuiltinTools } from '../tools'\nimport type { AiChatEmits, AiChatProps } from '../types'\nimport type {\n ChatAttachment,\n ChatMessage,\n ChatQueuedMessage,\n ChatTool,\n ChatToolCall\n} from './types'\n\nexport interface UseChatOptions {\n props: AiChatProps\n emit: AiChatEmits\n}\n\nconst uid = () => {\n return typeof crypto !== 'undefined' && crypto.randomUUID\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`\n}\n\nfunction serializeResult(result: unknown): string {\n if (typeof result === 'string') return result\n try {\n return JSON.stringify(result) ?? 'null'\n } catch {\n return String(result)\n }\n}\n\n/** 内置工具 + 用户工具,同名时内置优先 */\nfunction resolveTools(userTools?: ChatTool[]): ChatTool[] {\n const builtins = createBuiltinTools()\n const names = new Set(builtins.map((t) => t.name))\n return [...builtins, ...(userTools ?? []).filter((t) => !names.has(t.name))]\n}\n\n/**\n * AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。\n * 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。\n * 始终注入内置工具(如 askQuestion),无需经 props 传入。\n */\nexport function useChat(options: UseChatOptions) {\n const { props, emit } = options\n\n const messages = useModel({\n props,\n propName: 'messages',\n emit,\n local: true,\n defaultValue: []\n }) as Ref<ChatMessage[]>\n\n const model = useModel({ props, propName: 'model', emit, local: true }) as Ref<string | undefined>\n\n const reasoningLevel = useModel({ props, propName: 'reasoningLevel', emit, local: true }) as Ref<\n string | undefined\n >\n\n /** 是否正在生成中(含工具执行与多轮循环) */\n const running = ref(false)\n\n /** 待发送队列:会话进行中提交的新消息按序排队,收尾后先进先出自动接续 */\n const queue = ref<ChatQueuedMessage[]>([])\n\n let abortController: AbortController | null = null\n /** needsConfirm 工具的挂起确认器,key 为 toolCallId */\n const confirmResolvers = new Map<string, (approved: boolean) => void>()\n /** 当前会话是否自然完成(finish);决定收尾后是否自动消耗队列 */\n let finishedNaturally = false\n /** startQueued 触发的中断需要在收尾后立即接续队首 */\n let resumeAfterAbort = false\n\n /** 按当前模型校正推理等级(无 levels 则清空;值不合法则落到默认/首项) */\n const syncReasoningForModel = (modelId: string | undefined) => {\n const option = props.models?.find((m) => m.id === modelId)\n if (!option?.reasoningLevels?.length) {\n if (reasoningLevel.value !== undefined) {\n reasoningLevel.value = undefined\n }\n return\n }\n const levels = option.reasoningLevels\n if (reasoningLevel.value && levels.some((l) => l.value === reasoningLevel.value)) return\n reasoningLevel.value = option.defaultReasoningLevel ?? levels[0]?.value\n }\n\n // models / model 变化时:确保有合法选中模型,并校正推理等级\n watch(\n () => [props.models, model.value] as const,\n ([models, current]) => {\n if (!models?.length) return\n if (!current || !models.some((m) => m.id === current)) {\n model.value = models[0]!.id\n return\n }\n syncReasoningForModel(current)\n },\n { immediate: true }\n )\n\n /**\n * 向父组件同步消息快照(流式 delta 过于频繁,不同步,只在关键节点同步)。\n * 必须经 setter 赋值触发 emit,而非直接 emit 切片:setter 让本地数组与下发的\n * 快照保持同一引用,受控父级(v-model:messages)把快照回写到 props 时,\n * useModel 同步回来的仍是这个数组(流式期间新增的 assistant 占位已 push 在其上);\n * 若直接 emit 切片,本地数组与快照是两个对象,回显的旧快照会把本地新增的消息冲掉。\n */\n const snapshot = () => {\n messages.value = messages.value.slice()\n }\n\n /**\n * 发送一条用户消息。会话进行中时进入待发送队列(不丢失),\n * 会话自然结束后按 FIFO 自动接续;空闲时立即开始新会话。\n */\n const send = (content: string, attachments?: ChatAttachment[]) => {\n if (!content.trim() && !attachments?.length) return\n enqueue(content, attachments)\n }\n\n /**\n * 向队列插入一条消息;beforeId 指定插到某条之前(缺省追加到尾部)。\n * 空闲时立即消耗队首(保持先来先发的合理顺序)。\n */\n const enqueue = (\n content: string,\n attachments?: ChatAttachment[],\n beforeId?: string\n ): ChatQueuedMessage => {\n const item: ChatQueuedMessage = { id: uid(), content, attachments }\n const anchorIndex = beforeId ? queue.value.findIndex((q) => q.id === beforeId) : -1\n if (anchorIndex === -1) {\n queue.value.push(item)\n } else {\n queue.value.splice(anchorIndex, 0, item)\n }\n if (!running.value) drainQueue()\n return item\n }\n\n /** 消耗队首并开始新一轮会话 */\n const drainQueue = () => {\n const next = queue.value.shift()\n if (next) startUserTurn(next.content, next.attachments)\n }\n\n /** 立即执行队列中的某条:中断当前会话,该条插队为下一条(其余保持原顺序) */\n const startQueued = (id: string) => {\n const item = removeQueued(id)\n if (!item) return\n\n if (!running.value) {\n startUserTurn(item.content, item.attachments)\n return\n }\n\n // 插回队首并中断当前会话,收尾逻辑会自动接上它\n queue.value.unshift(item)\n resumeAfterAbort = true\n abort()\n }\n\n /** 从队列移除某条(返回被移除项;编辑场景由 UI 取回内容) */\n const removeQueued = (id: string): ChatQueuedMessage | undefined => {\n const index = queue.value.findIndex((item) => item.id === id)\n if (index === -1) return undefined\n const [item] = queue.value.splice(index, 1)\n return item\n }\n\n /** 推送用户消息并启动对话循环 */\n const startUserTurn = (content: string, attachments?: ChatAttachment[]) => {\n const message = reactive<ChatMessage>({ id: uid(), role: 'user', content, attachments })\n messages.value.push(message)\n snapshot()\n emit('send', message)\n\n void runConversation()\n }\n\n /** 执行单个工具调用(含确认门控),结果以 tool 消息追加 */\n const executeToolCall = async (\n toolCall: ChatToolCall,\n signal: AbortSignal,\n tools: ChatTool[]\n ) => {\n const appendToolMessage = (content: string) => {\n messages.value.push(\n reactive<ChatMessage>({ id: uid(), role: 'tool', toolCallId: toolCall.id, content })\n )\n }\n\n const tool = tools.find((t) => t.name === toolCall.name)\n\n if (!tool) {\n toolCall.status = 'error'\n toolCall.error = `未找到名为 \"${toolCall.name}\" 的工具`\n appendToolMessage(`Error: tool \"${toolCall.name}\" is not available.`)\n return\n }\n\n if (tool.needsConfirm) {\n toolCall.status = 'awaiting-confirm'\n const approved = await new Promise<boolean>((resolve) => {\n confirmResolvers.set(toolCall.id, resolve)\n signal.addEventListener('abort', () => resolve(false), { once: true })\n })\n confirmResolvers.delete(toolCall.id)\n\n if (!approved) {\n toolCall.status = 'rejected'\n appendToolMessage(`The user rejected the execution of tool \"${toolCall.name}\".`)\n return\n }\n }\n\n toolCall.status = 'running'\n try {\n const args = toolCall.arguments ? JSON.parse(toolCall.arguments) : {}\n const result = await tool.execute(args, { toolCall, signal })\n toolCall.status = 'success'\n toolCall.result = serializeResult(result)\n appendToolMessage(toolCall.result)\n } catch (error) {\n toolCall.status = 'error'\n toolCall.error = error instanceof Error ? error.message : String(error)\n appendToolMessage(`Error: ${toolCall.error}`)\n }\n }\n\n /** 对话主循环入口:生成 → 工具执行 → 再生成,直至模型不再调用工具 */\n const runConversation = async () => {\n if (!props.transport) return\n\n running.value = true\n finishedNaturally = false\n const controller = new AbortController()\n abortController = controller\n\n try {\n await runRound(controller, 0)\n } finally {\n running.value = false\n abortController = null\n snapshot()\n\n // 队列接续:自然完成或 startQueued 中断插队时自动发下一条;\n // 手动停止 / 出错时保留队列,交由用户处置\n const shouldResume = finishedNaturally || resumeAfterAbort\n finishedNaturally = false\n resumeAfterAbort = false\n if (shouldResume) drainQueue()\n }\n }\n\n /** 单轮生成;存在工具调用时执行后递归进入下一轮(递归避免 await-in-loop) */\n const runRound = async (controller: AbortController, depth: number): Promise<void> => {\n if (!props.transport) return\n const { signal } = controller\n const tools = resolveTools(props.tools)\n\n const assistant = reactive<ChatMessage>({\n id: uid(),\n role: 'assistant',\n content: '',\n status: 'streaming',\n toolCalls: []\n })\n messages.value.push(assistant)\n\n let requestError: Error | null = null\n\n try {\n await props.transport(\n {\n // 排除刚追加的 assistant 占位消息\n messages: messages.value.slice(0, -1),\n systemPrompt: props.systemPrompt,\n tools,\n model: model.value,\n reasoningLevel: reasoningLevel.value,\n signal\n },\n {\n onTextDelta: (delta) => {\n assistant.content += delta\n },\n onReasoningDelta: (delta) => {\n assistant.reasoning = (assistant.reasoning ?? '') + delta\n },\n onToolCall: (call) => {\n const toolCall = reactive<ChatToolCall>({ ...call, status: 'pending' })\n assistant.toolCalls!.push(toolCall)\n emit('tool-call', toolCall)\n },\n onError: (error) => {\n requestError = error\n }\n }\n )\n } catch (error) {\n requestError = error instanceof Error ? error : new Error(String(error))\n }\n\n if (signal.aborted) {\n assistant.status = 'aborted'\n return\n }\n\n if (requestError) {\n assistant.status = 'error'\n emit('error', requestError)\n return\n }\n\n assistant.status = 'done'\n\n const toolCalls = assistant.toolCalls ?? []\n if (!toolCalls.length) {\n completeAsFinish(assistant)\n return\n }\n\n // 工具串行执行(保持结果消息与调用顺序一致),用 Promise 链避免 await-in-loop\n await toolCalls.reduce<Promise<void>>(\n (prev, toolCall) =>\n prev.then(() => (signal.aborted ? undefined : executeToolCall(toolCall, signal, tools))),\n Promise.resolve()\n )\n snapshot()\n\n if (signal.aborted) return\n\n // 终结工具执行成功:工具 UI 即最终答复,不再回灌模型生成文字\n const hitTerminal = toolCalls.some(\n (call) => call.status === 'success' && tools.some((t) => t.name === call.name && t.terminal)\n )\n if (hitTerminal) {\n completeAsFinish(assistant)\n return\n }\n\n // 达到最大轮次上限:停止继续请求,防止模型失控循环调用工具\n if (depth + 1 >= (props.maxToolRounds ?? 10)) {\n completeAsFinish(assistant)\n return\n }\n\n return runRound(controller, depth + 1)\n }\n\n /** 标记本轮对话自然完成(收尾后据此自动消耗待发送队列) */\n const completeAsFinish = (assistant: ChatMessage) => {\n finishedNaturally = true\n emit('finish', assistant)\n }\n\n /** 中断当前生成,挂起的工具确认按拒绝处理 */\n const abort = () => {\n abortController?.abort()\n }\n\n /** 响应 needsConfirm 工具的用户确认 */\n const respondToolCall = (toolCallId: string, approved: boolean) => {\n confirmResolvers.get(toolCallId)?.(approved)\n }\n\n /** 重新生成:移除最后一条用户消息之后的所有消息,重新跑对话循环 */\n const regenerate = () => {\n if (running.value) return\n\n const list = messages.value\n let lastUserIndex = -1\n for (let i = list.length - 1; i >= 0; i--) {\n if (list[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1 || lastUserIndex === list.length - 1) return\n\n messages.value = list.slice(0, lastUserIndex + 1)\n snapshot()\n void runConversation()\n }\n\n /** 清空消息与待发送队列,生成中则先中断 */\n const clear = () => {\n abort()\n queue.value = []\n messages.value = []\n snapshot()\n }\n\n return {\n messages,\n model,\n reasoningLevel,\n running,\n queue,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall,\n enqueue,\n startQueued,\n removeQueued\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-chat.js","names":[],"sources":["../../src/chat/use-chat.ts"],"sourcesContent":["import { useModel } from '@veltra/compositions'\nimport { reactive, ref, watch, type Ref } from 'vue'\n\nimport { createBuiltinTools } from '../tools'\nimport type { AiChatEmits, AiChatProps } from '../types'\nimport type {\n ChatAttachment,\n ChatMessage,\n ChatQueuedMessage,\n ChatTokenUsage,\n ChatTool,\n ChatToolCall\n} from './types'\n\nexport interface UseChatOptions {\n props: AiChatProps\n emit: AiChatEmits\n}\n\nconst uid = () => {\n return typeof crypto !== 'undefined' && crypto.randomUUID\n ? crypto.randomUUID()\n : `${Date.now()}-${Math.random().toString(36).slice(2)}`\n}\n\nfunction serializeResult(result: unknown): string {\n if (typeof result === 'string') return result\n try {\n return JSON.stringify(result) ?? 'null'\n } catch {\n return String(result)\n }\n}\n\n/** 内置工具 + 用户工具,同名时内置优先 */\nfunction resolveTools(userTools?: ChatTool[]): ChatTool[] {\n const builtins = createBuiltinTools()\n const names = new Set(builtins.map((t) => t.name))\n return [...builtins, ...(userTools ?? []).filter((t) => !names.has(t.name))]\n}\n\nfunction mergeOptionalCount(a?: number, b?: number): number | undefined {\n if (a == null && b == null) return undefined\n return (a ?? 0) + (b ?? 0)\n}\n\n/** 累加两次 usage;缓存字段只在至少一侧有值时保留 */\nfunction addTokenUsage(a: ChatTokenUsage, b: ChatTokenUsage): ChatTokenUsage {\n const usage: ChatTokenUsage = {\n promptTokens: a.promptTokens + b.promptTokens,\n completionTokens: a.completionTokens + b.completionTokens,\n totalTokens: a.totalTokens + b.totalTokens\n }\n const cacheHitTokens = mergeOptionalCount(a.cacheHitTokens, b.cacheHitTokens)\n if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens\n const cacheMissTokens = mergeOptionalCount(a.cacheMissTokens, b.cacheMissTokens)\n if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens\n const cacheCreationTokens = mergeOptionalCount(a.cacheCreationTokens, b.cacheCreationTokens)\n if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens\n return usage\n}\n\n/**\n * AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。\n * 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。\n * 始终注入内置工具(如 askQuestion),无需经 props 传入。\n */\nexport function useChat(options: UseChatOptions) {\n const { props, emit } = options\n\n const messages = useModel({\n props,\n propName: 'messages',\n emit,\n local: true,\n defaultValue: []\n }) as Ref<ChatMessage[]>\n\n const model = useModel({ props, propName: 'model', emit, local: true }) as Ref<string | undefined>\n\n const reasoningLevel = useModel({ props, propName: 'reasoningLevel', emit, local: true }) as Ref<\n string | undefined\n >\n\n /** 是否正在生成中(含工具执行与多轮循环) */\n const running = ref(false)\n\n /** 待发送队列:会话进行中提交的新消息按序排队,收尾后先进先出自动接续 */\n const queue = ref<ChatQueuedMessage[]>([])\n\n /** 当前会话累计 token;从未收到 usage 时为 null */\n const tokenUsage = ref<ChatTokenUsage | null>(null)\n /** 最近一轮用户对话(含工具多轮请求)的 token;该轮无 usage 时为 null */\n const lastTurnUsage = ref<ChatTokenUsage | null>(null)\n\n let abortController: AbortController | null = null\n /** needsConfirm 工具的挂起确认器,key 为 toolCallId */\n const confirmResolvers = new Map<string, (approved: boolean) => void>()\n /** 当前会话是否自然完成(finish);决定收尾后是否自动消耗队列 */\n let finishedNaturally = false\n /** startQueued 触发的中断需要在收尾后立即接续队首 */\n let resumeAfterAbort = false\n\n /** 按当前模型校正推理等级(无 levels 则清空;值不合法则落到默认/首项) */\n const syncReasoningForModel = (modelId: string | undefined) => {\n const option = props.models?.find((m) => m.id === modelId)\n if (!option?.reasoningLevels?.length) {\n if (reasoningLevel.value !== undefined) {\n reasoningLevel.value = undefined\n }\n return\n }\n const levels = option.reasoningLevels\n if (reasoningLevel.value && levels.some((l) => l.value === reasoningLevel.value)) return\n reasoningLevel.value = option.defaultReasoningLevel ?? levels[0]?.value\n }\n\n // models / model 变化时:确保有合法选中模型,并校正推理等级\n watch(\n () => [props.models, model.value] as const,\n ([models, current]) => {\n if (!models?.length) return\n if (!current || !models.some((m) => m.id === current)) {\n model.value = models[0]!.id\n return\n }\n syncReasoningForModel(current)\n },\n { immediate: true }\n )\n\n /**\n * 向父组件同步消息快照(流式 delta 过于频繁,不同步,只在关键节点同步)。\n * 必须经 setter 赋值触发 emit,而非直接 emit 切片:setter 让本地数组与下发的\n * 快照保持同一引用,受控父级(v-model:messages)把快照回写到 props 时,\n * useModel 同步回来的仍是这个数组(流式期间新增的 assistant 占位已 push 在其上);\n * 若直接 emit 切片,本地数组与快照是两个对象,回显的旧快照会把本地新增的消息冲掉。\n */\n const snapshot = () => {\n messages.value = messages.value.slice()\n }\n\n /**\n * 发送一条用户消息。会话进行中时进入待发送队列(不丢失),\n * 会话自然结束后按 FIFO 自动接续;空闲时立即开始新会话。\n */\n const send = (content: string, attachments?: ChatAttachment[]) => {\n if (!content.trim() && !attachments?.length) return\n enqueue(content, attachments)\n }\n\n /**\n * 向队列插入一条消息;beforeId 指定插到某条之前(缺省追加到尾部)。\n * 空闲时立即消耗队首(保持先来先发的合理顺序)。\n */\n const enqueue = (\n content: string,\n attachments?: ChatAttachment[],\n beforeId?: string\n ): ChatQueuedMessage => {\n const item: ChatQueuedMessage = { id: uid(), content, attachments }\n const anchorIndex = beforeId ? queue.value.findIndex((q) => q.id === beforeId) : -1\n if (anchorIndex === -1) {\n queue.value.push(item)\n } else {\n queue.value.splice(anchorIndex, 0, item)\n }\n if (!running.value) drainQueue()\n return item\n }\n\n /** 消耗队首并开始新一轮会话 */\n const drainQueue = () => {\n const next = queue.value.shift()\n if (next) startUserTurn(next.content, next.attachments)\n }\n\n /** 立即执行队列中的某条:中断当前会话,该条插队为下一条(其余保持原顺序) */\n const startQueued = (id: string) => {\n const item = removeQueued(id)\n if (!item) return\n\n if (!running.value) {\n startUserTurn(item.content, item.attachments)\n return\n }\n\n // 插回队首并中断当前会话,收尾逻辑会自动接上它\n queue.value.unshift(item)\n resumeAfterAbort = true\n abort()\n }\n\n /** 从队列移除某条(返回被移除项;编辑场景由 UI 取回内容) */\n const removeQueued = (id: string): ChatQueuedMessage | undefined => {\n const index = queue.value.findIndex((item) => item.id === id)\n if (index === -1) return undefined\n const [item] = queue.value.splice(index, 1)\n return item\n }\n\n /** 推送用户消息并启动对话循环 */\n const startUserTurn = (content: string, attachments?: ChatAttachment[]) => {\n const message = reactive<ChatMessage>({ id: uid(), role: 'user', content, attachments })\n messages.value.push(message)\n snapshot()\n emit('send', message)\n\n void runConversation()\n }\n\n /** 执行单个工具调用(含确认门控),结果以 tool 消息追加 */\n const executeToolCall = async (\n toolCall: ChatToolCall,\n signal: AbortSignal,\n tools: ChatTool[]\n ) => {\n const appendToolMessage = (content: string) => {\n messages.value.push(\n reactive<ChatMessage>({ id: uid(), role: 'tool', toolCallId: toolCall.id, content })\n )\n }\n\n const tool = tools.find((t) => t.name === toolCall.name)\n\n if (!tool) {\n toolCall.status = 'error'\n toolCall.error = `未找到名为 \"${toolCall.name}\" 的工具`\n appendToolMessage(`Error: tool \"${toolCall.name}\" is not available.`)\n return\n }\n\n if (tool.needsConfirm) {\n toolCall.status = 'awaiting-confirm'\n const approved = await new Promise<boolean>((resolve) => {\n confirmResolvers.set(toolCall.id, resolve)\n signal.addEventListener('abort', () => resolve(false), { once: true })\n })\n confirmResolvers.delete(toolCall.id)\n\n if (!approved) {\n toolCall.status = 'rejected'\n appendToolMessage(`The user rejected the execution of tool \"${toolCall.name}\".`)\n return\n }\n }\n\n toolCall.status = 'running'\n try {\n const args = toolCall.arguments ? JSON.parse(toolCall.arguments) : {}\n const result = await tool.execute(args, { toolCall, signal })\n toolCall.status = 'success'\n toolCall.result = serializeResult(result)\n appendToolMessage(toolCall.result)\n } catch (error) {\n toolCall.status = 'error'\n toolCall.error = error instanceof Error ? error.message : String(error)\n appendToolMessage(`Error: ${toolCall.error}`)\n }\n }\n\n /** 对话主循环入口:生成 → 工具执行 → 再生成,直至模型不再调用工具 */\n const runConversation = async () => {\n if (!props.transport) return\n\n running.value = true\n finishedNaturally = false\n lastTurnUsage.value = null\n const controller = new AbortController()\n abortController = controller\n\n try {\n await runRound(controller, 0)\n } finally {\n running.value = false\n abortController = null\n snapshot()\n\n // 队列接续:自然完成或 startQueued 中断插队时自动发下一条;\n // 手动停止 / 出错时保留队列,交由用户处置\n const shouldResume = finishedNaturally || resumeAfterAbort\n finishedNaturally = false\n resumeAfterAbort = false\n if (shouldResume) drainQueue()\n }\n }\n\n /** 单轮生成;存在工具调用时执行后递归进入下一轮(递归避免 await-in-loop) */\n const runRound = async (controller: AbortController, depth: number): Promise<void> => {\n if (!props.transport) return\n const { signal } = controller\n const tools = resolveTools(props.tools)\n\n const assistant = reactive<ChatMessage>({\n id: uid(),\n role: 'assistant',\n content: '',\n status: 'streaming',\n toolCalls: []\n })\n messages.value.push(assistant)\n\n let requestError: Error | null = null\n\n try {\n await props.transport(\n {\n // 排除刚追加的 assistant 占位消息\n messages: messages.value.slice(0, -1),\n systemPrompt: props.systemPrompt,\n tools,\n model: model.value,\n reasoningLevel: reasoningLevel.value,\n signal\n },\n {\n onTextDelta: (delta) => {\n assistant.content += delta\n },\n onReasoningDelta: (delta) => {\n assistant.reasoning = (assistant.reasoning ?? '') + delta\n },\n onToolCall: (call) => {\n const toolCall = reactive<ChatToolCall>({ ...call, status: 'pending' })\n assistant.toolCalls!.push(toolCall)\n emit('tool-call', toolCall)\n },\n onUsage: (usage) => {\n lastTurnUsage.value = lastTurnUsage.value\n ? addTokenUsage(lastTurnUsage.value, usage)\n : usage\n tokenUsage.value = tokenUsage.value ? addTokenUsage(tokenUsage.value, usage) : usage\n },\n onError: (error) => {\n requestError = error\n }\n }\n )\n } catch (error) {\n requestError = error instanceof Error ? error : new Error(String(error))\n }\n\n if (signal.aborted) {\n assistant.status = 'aborted'\n return\n }\n\n if (requestError) {\n assistant.status = 'error'\n emit('error', requestError)\n return\n }\n\n assistant.status = 'done'\n\n const toolCalls = assistant.toolCalls ?? []\n if (!toolCalls.length) {\n completeAsFinish(assistant)\n return\n }\n\n // 工具串行执行(保持结果消息与调用顺序一致),用 Promise 链避免 await-in-loop\n await toolCalls.reduce<Promise<void>>(\n (prev, toolCall) =>\n prev.then(() => (signal.aborted ? undefined : executeToolCall(toolCall, signal, tools))),\n Promise.resolve()\n )\n snapshot()\n\n if (signal.aborted) return\n\n // 终结工具执行成功:工具 UI 即最终答复,不再回灌模型生成文字\n const hitTerminal = toolCalls.some(\n (call) => call.status === 'success' && tools.some((t) => t.name === call.name && t.terminal)\n )\n if (hitTerminal) {\n completeAsFinish(assistant)\n return\n }\n\n // 达到最大轮次上限:停止继续请求,防止模型失控循环调用工具\n if (depth + 1 >= (props.maxToolRounds ?? 10)) {\n completeAsFinish(assistant)\n return\n }\n\n return runRound(controller, depth + 1)\n }\n\n /** 标记本轮对话自然完成(收尾后据此自动消耗待发送队列) */\n const completeAsFinish = (assistant: ChatMessage) => {\n finishedNaturally = true\n emit('finish', assistant)\n }\n\n /** 中断当前生成,挂起的工具确认按拒绝处理 */\n const abort = () => {\n abortController?.abort()\n }\n\n /** 响应 needsConfirm 工具的用户确认 */\n const respondToolCall = (toolCallId: string, approved: boolean) => {\n confirmResolvers.get(toolCallId)?.(approved)\n }\n\n /** 重新生成:移除最后一条用户消息之后的所有消息,重新跑对话循环 */\n const regenerate = () => {\n if (running.value) return\n\n const list = messages.value\n let lastUserIndex = -1\n for (let i = list.length - 1; i >= 0; i--) {\n if (list[i]?.role === 'user') {\n lastUserIndex = i\n break\n }\n }\n if (lastUserIndex === -1 || lastUserIndex === list.length - 1) return\n\n messages.value = list.slice(0, lastUserIndex + 1)\n snapshot()\n void runConversation()\n }\n\n /** 清空消息、待发送队列与 token 统计,生成中则先中断 */\n const clear = () => {\n abort()\n queue.value = []\n messages.value = []\n tokenUsage.value = null\n lastTurnUsage.value = null\n snapshot()\n }\n\n return {\n messages,\n model,\n reasoningLevel,\n running,\n queue,\n tokenUsage,\n lastTurnUsage,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall,\n enqueue,\n startQueued,\n removeQueued\n }\n}\n"],"mappings":"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"}
|
|
@@ -30,7 +30,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
30
30
|
placeholder: {},
|
|
31
31
|
accept: {},
|
|
32
32
|
maxAttachmentSize: {},
|
|
33
|
-
rendererProps: {}
|
|
33
|
+
rendererProps: {},
|
|
34
|
+
tokenUsageDetail: { type: Boolean }
|
|
34
35
|
},
|
|
35
36
|
emits: [
|
|
36
37
|
"update:messages",
|
|
@@ -53,12 +54,14 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
53
54
|
const tools = [...builtins, ...(props.tools ?? []).filter((t) => !names.has(t.name))];
|
|
54
55
|
return Object.fromEntries(tools.map((t) => [t.name, t]));
|
|
55
56
|
});
|
|
56
|
-
const { messages, model, reasoningLevel, running, queue, send, abort, regenerate, clear, respondToolCall, enqueue, startQueued, removeQueued } = useChat({
|
|
57
|
+
const { messages, model, reasoningLevel, running, queue, tokenUsage, lastTurnUsage, send, abort, regenerate, clear, respondToolCall, enqueue, startQueued, removeQueued } = useChat({
|
|
57
58
|
props,
|
|
58
59
|
emit
|
|
59
60
|
});
|
|
60
61
|
/** 空会话(无可见消息):输入区垂直居中;欢迎区钉在滚动容器外、贴于输入框上方 */
|
|
61
62
|
const isEmpty = computed(() => !messages.value.some((msg) => msg.role !== "tool"));
|
|
63
|
+
/** 有对话内容、队列或生成中才允许清除,避免误触空会话 */
|
|
64
|
+
const clearable = computed(() => messages.value.length > 0 || queue.value.length > 0 || running.value);
|
|
62
65
|
/** 所有 renderTo: 'panel' 的工具调用(按消息顺序,面板内容的来源) */
|
|
63
66
|
const panelCalls = computed(() => {
|
|
64
67
|
const calls = [];
|
|
@@ -170,6 +173,8 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
170
173
|
abort,
|
|
171
174
|
regenerate,
|
|
172
175
|
clear,
|
|
176
|
+
tokenUsage,
|
|
177
|
+
lastTurnUsage,
|
|
173
178
|
queue,
|
|
174
179
|
startQueued,
|
|
175
180
|
removeQueued,
|
|
@@ -231,8 +236,12 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
231
236
|
placeholder: __props.placeholder,
|
|
232
237
|
accept: __props.accept,
|
|
233
238
|
"max-attachment-size": __props.maxAttachmentSize,
|
|
239
|
+
clearable: clearable.value,
|
|
240
|
+
"token-usage": unref(tokenUsage),
|
|
241
|
+
"token-usage-detail": __props.tokenUsageDetail,
|
|
234
242
|
onSend: handleSend,
|
|
235
|
-
onAbort: unref(abort)
|
|
243
|
+
onAbort: unref(abort),
|
|
244
|
+
onClear: unref(clear)
|
|
236
245
|
}, null, 8, [
|
|
237
246
|
"model",
|
|
238
247
|
"reasoning-level",
|
|
@@ -241,7 +250,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
241
250
|
"placeholder",
|
|
242
251
|
"accept",
|
|
243
252
|
"max-attachment-size",
|
|
244
|
-
"
|
|
253
|
+
"clearable",
|
|
254
|
+
"token-usage",
|
|
255
|
+
"token-usage-detail",
|
|
256
|
+
"onAbort",
|
|
257
|
+
"onClear"
|
|
245
258
|
])
|
|
246
259
|
], 2),
|
|
247
260
|
createCommentVNode(" 侧边面板(renderTo: 'panel' 的工具渲染区),与会话区宽度经 ULayout 拖拽手柄调节 "),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-chat.js","names":["$slots"],"sources":["../../../src/components/ai-chat/ai-chat.vue"],"sourcesContent":["<template>\n <ULayout\n ref=\"layoutRef\"\n :class=\"[cls.b, bem.is('resizing', resizing)]\"\n :cols=\"layoutCols\"\n rows=\"1fr\"\n :resizable=\"panelOpen\"\n :col-min-sizes=\"panelOpen ? [MAIN_MIN_WIDTH, PANEL_MIN_WIDTH] : undefined\"\n @resize-start=\"resizing = true\"\n @resize-end=\"resizing = false\"\n >\n <div :class=\"[cls.e('main'), bem.is('empty', isEmpty)]\">\n <MessageList\n :messages=\"messages\"\n :welcome=\"welcome\"\n :running=\"running\"\n :renderer-props=\"rendererProps\"\n @respond=\"respondToolCall\"\n @regenerate=\"regenerate\"\n @welcome-click=\"(text) => handleSend(text, [])\"\n >\n <template v-if=\"$slots.welcome\" #welcome>\n <slot name=\"welcome\" />\n </template>\n </MessageList>\n\n <QueueList\n :queue=\"queue\"\n @start-now=\"startQueued\"\n @edit=\"handleQueueEdit\"\n @remove=\"removeQueued\"\n />\n\n <ChatInput\n ref=\"chatInputRef\"\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=\"handleSend\"\n @abort=\"abort\"\n />\n </div>\n\n <!-- 侧边面板(renderTo: 'panel' 的工具渲染区),与会话区宽度经 ULayout 拖拽手柄调节 -->\n <SidePanel\n v-if=\"activePanelCall\"\n ref=\"panelRef\"\n :tool-call=\"activePanelCall\"\n @close=\"closePanel\"\n />\n </ULayout>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useResizeObserver } from '@veltra/compositions'\nimport { ULayout } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, provide, ref, useSlots, useTemplateRef, watch } from 'vue'\n\nimport type { ChatAttachment, 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'\nimport QueueList from './queue-list.vue'\nimport SidePanel from './side-panel.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/renderTo/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\nconst {\n messages,\n model,\n reasoningLevel,\n running,\n queue,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall,\n enqueue,\n startQueued,\n removeQueued\n} = useChat({ props, emit })\n\n/** 空会话(无可见消息):输入区垂直居中;欢迎区钉在滚动容器外、贴于输入框上方 */\nconst isEmpty = computed(() => !messages.value.some((msg) => msg.role !== 'tool'))\n\n/** 所有 renderTo: 'panel' 的工具调用(按消息顺序,面板内容的来源) */\nconst panelCalls = computed(() => {\n const calls: ChatToolCall[] = []\n for (const msg of messages.value) {\n if (msg.role !== 'assistant' || !msg.toolCalls) continue\n for (const call of msg.toolCalls) {\n if (toolMap.value[call.name]?.renderTo === 'panel') calls.push(call)\n }\n }\n return calls\n})\n\n/** 当前展示在右侧面板的工具调用 id;null 表示面板关闭 */\nconst activePanelCallId = ref<string | null>(null)\n\nconst activePanelCall = computed(() => {\n if (!activePanelCallId.value) return null\n return panelCalls.value.find((call) => call.id === activePanelCallId.value) ?? null\n})\n\nconst panelOpen = computed(() => activePanelCall.value != null)\n\n// 出现新的面板工具调用时自动打开并聚焦到它\nwatch(\n () => panelCalls.value[panelCalls.value.length - 1]?.id,\n (id) => {\n if (id) activePanelCallId.value = id\n }\n)\n\n// 聚焦的调用消失(清空消息 / 重新生成)时,回落到最近一次面板调用或关闭\nwatch(panelCalls, (calls) => {\n const id = activePanelCallId.value\n if (id && !calls.some((call) => call.id === id)) {\n activePanelCallId.value = calls[calls.length - 1]?.id ?? null\n }\n})\n\n/** 打开面板并聚焦到指定工具调用(工具卡片「查看面板」入口,经 DI 下发) */\nconst openPanel = (toolCallId: string) => {\n if (panelCalls.value.some((call) => call.id === toolCallId)) {\n activePanelCallId.value = toolCallId\n }\n}\n\nconst closePanel = () => {\n activePanelCallId.value = null\n}\n\nprovide(AiChatDIKey, { cls, slots, tools: toolMap, openPanel })\n\n/**\n * 面板宽度:经 ULayout 列轨施加(cols = 1fr + 面板宽度),\n * 拖拽手柄由 ULayout 提供(colMinSizes 约束会话区 / 面板最小宽度)。\n */\nconst PANEL_MIN_WIDTH = 320\nconst MAIN_MIN_WIDTH = 360\n/** 面板打开时默认给会话区留的宽度(即面板默认尽可能大) */\nconst MAIN_DEFAULT_WIDTH = 860\n\nconst layoutRef = useTemplateRef<InstanceType<typeof ULayout>>('layoutRef')\nconst panelRef = useTemplateRef<InstanceType<typeof SidePanel>>('panelRef')\nconst panelWidth = ref(PANEL_MIN_WIDTH)\nconst resizing = ref(false)\n\nconst layoutCols = computed(() => {\n return panelOpen.value ? ['1fr', `${panelWidth.value}px`] : ['1fr']\n})\n\nconst containerWidth = () => (layoutRef.value?.$el as HTMLElement | undefined)?.clientWidth ?? 0\n\n/** 面板默认宽度:容器宽减去会话区默认保留宽度(缺省给会话区留 MAIN_DEFAULT_WIDTH) */\nconst defaultPanelWidth = () => {\n const width = containerWidth()\n return width ? width - MAIN_DEFAULT_WIDTH : 420\n}\n\n/** 钳位面板宽度在 [PANEL_MIN_WIDTH, 容器宽 - MAIN_MIN_WIDTH] 区间 */\nconst clampPanelWidth = (width: number) => {\n const width0 = containerWidth()\n const maxWidth = width0\n ? Math.max(PANEL_MIN_WIDTH, width0 - MAIN_MIN_WIDTH)\n : Number.MAX_SAFE_INTEGER\n return Math.min(Math.max(width, PANEL_MIN_WIDTH), maxWidth)\n}\n\n// 聚焦到指定了 panelWidth 的工具调用时应用其宽度;面板从关闭到打开且工具未指定宽度时\n// 应用默认宽(会话区留 860px);面板已打开时切换聚焦保持当前宽度(含用户拖拽结果)\nwatch(activePanelCall, (call, prevCall) => {\n if (!call) return\n const toolWidth = toolMap.value[call.name]?.panelWidth\n const width = toolWidth ?? (prevCall ? undefined : defaultPanelWidth())\n if (typeof width !== 'number') return\n panelWidth.value = clampPanelWidth(width)\n})\n\n// 跟踪面板实际宽度(含用户拖拽结果):面板关闭重开、切换到未指定宽度的工具时保持\nuseResizeObserver({\n targets: computed(() => (panelRef.value?.$el as HTMLElement | undefined) ?? undefined),\n onResize(entries) {\n // borderBoxSize 随通知携带(含边框、零布局开销),不读 offsetWidth/clientWidth\n const inlineSize = entries[0]?.borderBoxSize?.[0]?.inlineSize\n if (!inlineSize) return\n const width = Math.round(inlineSize)\n // 滞回:忽略亚像素/边框级的微小偏差(< 2px),阻断任何「观测→回写→再观测」的微缩反馈回路\n if (Math.abs(width - panelWidth.value) < 2) return\n panelWidth.value = width\n }\n})\n\nconst chatInputRef = useTemplateRef<InstanceType<typeof ChatInput>>('chatInputRef')\n\n/**\n * 编辑中的队列项原后继 id(编辑态标记):\n * undefined 表示无编辑;null 表示原位置在队尾;否则重新提交时插回该后继之前,\n * 使前后项顺序不受编辑影响(前项已执行则该项自然成为最前待执行项)。\n */\nconst editingSuccessor = ref<string | null | undefined>(undefined)\n\n/** 编辑队列项:取回输入框重新编辑;已有未完成编辑时先按锚点还原,避免内容丢失 */\nconst handleQueueEdit = (id: string) => {\n restoreEditing()\n\n const index = queue.value.findIndex((item) => item.id === id)\n if (index === -1) return\n editingSuccessor.value = queue.value[index + 1]?.id ?? null\n\n const item = removeQueued(id)\n if (item) chatInputRef.value?.setContent(item.content)\n}\n\n/** 将输入框中未提交的编辑内容按原锚点插回队列 */\nconst restoreEditing = () => {\n if (editingSuccessor.value === undefined) return\n const content = chatInputRef.value?.getContent().trim()\n if (content) enqueue(content, undefined, editingSuccessor.value ?? undefined)\n editingSuccessor.value = undefined\n}\n\nconst handleSend = (content: string, attachments: ChatAttachment[]) => {\n // 编辑态提交:插回原锚点位置(会话进行中则留在队列,空闲时由 enqueue 自动消耗队首)\n if (editingSuccessor.value !== undefined) {\n enqueue(content, attachments, editingSuccessor.value ?? undefined)\n editingSuccessor.value = undefined\n return\n }\n send(content, attachments)\n}\n\ndefineExpose<_AiChatExposed>({\n send,\n abort,\n regenerate,\n clear,\n queue,\n startQueued,\n removeQueued,\n enqueue\n})\n</script>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ai-chat.js","names":["$slots"],"sources":["../../../src/components/ai-chat/ai-chat.vue"],"sourcesContent":["<template>\n <ULayout\n ref=\"layoutRef\"\n :class=\"[cls.b, bem.is('resizing', resizing)]\"\n :cols=\"layoutCols\"\n rows=\"1fr\"\n :resizable=\"panelOpen\"\n :col-min-sizes=\"panelOpen ? [MAIN_MIN_WIDTH, PANEL_MIN_WIDTH] : undefined\"\n @resize-start=\"resizing = true\"\n @resize-end=\"resizing = false\"\n >\n <div :class=\"[cls.e('main'), bem.is('empty', isEmpty)]\">\n <MessageList\n :messages=\"messages\"\n :welcome=\"welcome\"\n :running=\"running\"\n :renderer-props=\"rendererProps\"\n @respond=\"respondToolCall\"\n @regenerate=\"regenerate\"\n @welcome-click=\"(text) => handleSend(text, [])\"\n >\n <template v-if=\"$slots.welcome\" #welcome>\n <slot name=\"welcome\" />\n </template>\n </MessageList>\n\n <QueueList\n :queue=\"queue\"\n @start-now=\"startQueued\"\n @edit=\"handleQueueEdit\"\n @remove=\"removeQueued\"\n />\n\n <ChatInput\n ref=\"chatInputRef\"\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 :clearable=\"clearable\"\n :token-usage=\"tokenUsage\"\n :token-usage-detail=\"tokenUsageDetail\"\n @send=\"handleSend\"\n @abort=\"abort\"\n @clear=\"clear\"\n />\n </div>\n\n <!-- 侧边面板(renderTo: 'panel' 的工具渲染区),与会话区宽度经 ULayout 拖拽手柄调节 -->\n <SidePanel\n v-if=\"activePanelCall\"\n ref=\"panelRef\"\n :tool-call=\"activePanelCall\"\n @close=\"closePanel\"\n />\n </ULayout>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useResizeObserver } from '@veltra/compositions'\nimport { ULayout } from '@veltra/desktop'\nimport { bem } from '@veltra/utils'\nimport { computed, provide, ref, useSlots, useTemplateRef, watch } from 'vue'\n\nimport type { ChatAttachment, 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'\nimport QueueList from './queue-list.vue'\nimport SidePanel from './side-panel.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/renderTo/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\nconst {\n messages,\n model,\n reasoningLevel,\n running,\n queue,\n tokenUsage,\n lastTurnUsage,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall,\n enqueue,\n startQueued,\n removeQueued\n} = useChat({ props, emit })\n\n/** 空会话(无可见消息):输入区垂直居中;欢迎区钉在滚动容器外、贴于输入框上方 */\nconst isEmpty = computed(() => !messages.value.some((msg) => msg.role !== 'tool'))\n\n/** 有对话内容、队列或生成中才允许清除,避免误触空会话 */\nconst clearable = computed(\n () => messages.value.length > 0 || queue.value.length > 0 || running.value\n)\n\n/** 所有 renderTo: 'panel' 的工具调用(按消息顺序,面板内容的来源) */\nconst panelCalls = computed(() => {\n const calls: ChatToolCall[] = []\n for (const msg of messages.value) {\n if (msg.role !== 'assistant' || !msg.toolCalls) continue\n for (const call of msg.toolCalls) {\n if (toolMap.value[call.name]?.renderTo === 'panel') calls.push(call)\n }\n }\n return calls\n})\n\n/** 当前展示在右侧面板的工具调用 id;null 表示面板关闭 */\nconst activePanelCallId = ref<string | null>(null)\n\nconst activePanelCall = computed(() => {\n if (!activePanelCallId.value) return null\n return panelCalls.value.find((call) => call.id === activePanelCallId.value) ?? null\n})\n\nconst panelOpen = computed(() => activePanelCall.value != null)\n\n// 出现新的面板工具调用时自动打开并聚焦到它\nwatch(\n () => panelCalls.value[panelCalls.value.length - 1]?.id,\n (id) => {\n if (id) activePanelCallId.value = id\n }\n)\n\n// 聚焦的调用消失(清空消息 / 重新生成)时,回落到最近一次面板调用或关闭\nwatch(panelCalls, (calls) => {\n const id = activePanelCallId.value\n if (id && !calls.some((call) => call.id === id)) {\n activePanelCallId.value = calls[calls.length - 1]?.id ?? null\n }\n})\n\n/** 打开面板并聚焦到指定工具调用(工具卡片「查看面板」入口,经 DI 下发) */\nconst openPanel = (toolCallId: string) => {\n if (panelCalls.value.some((call) => call.id === toolCallId)) {\n activePanelCallId.value = toolCallId\n }\n}\n\nconst closePanel = () => {\n activePanelCallId.value = null\n}\n\nprovide(AiChatDIKey, { cls, slots, tools: toolMap, openPanel })\n\n/**\n * 面板宽度:经 ULayout 列轨施加(cols = 1fr + 面板宽度),\n * 拖拽手柄由 ULayout 提供(colMinSizes 约束会话区 / 面板最小宽度)。\n */\nconst PANEL_MIN_WIDTH = 320\nconst MAIN_MIN_WIDTH = 360\n/** 面板打开时默认给会话区留的宽度(即面板默认尽可能大) */\nconst MAIN_DEFAULT_WIDTH = 860\n\nconst layoutRef = useTemplateRef<InstanceType<typeof ULayout>>('layoutRef')\nconst panelRef = useTemplateRef<InstanceType<typeof SidePanel>>('panelRef')\nconst panelWidth = ref(PANEL_MIN_WIDTH)\nconst resizing = ref(false)\n\nconst layoutCols = computed(() => {\n return panelOpen.value ? ['1fr', `${panelWidth.value}px`] : ['1fr']\n})\n\nconst containerWidth = () => (layoutRef.value?.$el as HTMLElement | undefined)?.clientWidth ?? 0\n\n/** 面板默认宽度:容器宽减去会话区默认保留宽度(缺省给会话区留 MAIN_DEFAULT_WIDTH) */\nconst defaultPanelWidth = () => {\n const width = containerWidth()\n return width ? width - MAIN_DEFAULT_WIDTH : 420\n}\n\n/** 钳位面板宽度在 [PANEL_MIN_WIDTH, 容器宽 - MAIN_MIN_WIDTH] 区间 */\nconst clampPanelWidth = (width: number) => {\n const width0 = containerWidth()\n const maxWidth = width0\n ? Math.max(PANEL_MIN_WIDTH, width0 - MAIN_MIN_WIDTH)\n : Number.MAX_SAFE_INTEGER\n return Math.min(Math.max(width, PANEL_MIN_WIDTH), maxWidth)\n}\n\n// 聚焦到指定了 panelWidth 的工具调用时应用其宽度;面板从关闭到打开且工具未指定宽度时\n// 应用默认宽(会话区留 860px);面板已打开时切换聚焦保持当前宽度(含用户拖拽结果)\nwatch(activePanelCall, (call, prevCall) => {\n if (!call) return\n const toolWidth = toolMap.value[call.name]?.panelWidth\n const width = toolWidth ?? (prevCall ? undefined : defaultPanelWidth())\n if (typeof width !== 'number') return\n panelWidth.value = clampPanelWidth(width)\n})\n\n// 跟踪面板实际宽度(含用户拖拽结果):面板关闭重开、切换到未指定宽度的工具时保持\nuseResizeObserver({\n targets: computed(() => (panelRef.value?.$el as HTMLElement | undefined) ?? undefined),\n onResize(entries) {\n // borderBoxSize 随通知携带(含边框、零布局开销),不读 offsetWidth/clientWidth\n const inlineSize = entries[0]?.borderBoxSize?.[0]?.inlineSize\n if (!inlineSize) return\n const width = Math.round(inlineSize)\n // 滞回:忽略亚像素/边框级的微小偏差(< 2px),阻断任何「观测→回写→再观测」的微缩反馈回路\n if (Math.abs(width - panelWidth.value) < 2) return\n panelWidth.value = width\n }\n})\n\nconst chatInputRef = useTemplateRef<InstanceType<typeof ChatInput>>('chatInputRef')\n\n/**\n * 编辑中的队列项原后继 id(编辑态标记):\n * undefined 表示无编辑;null 表示原位置在队尾;否则重新提交时插回该后继之前,\n * 使前后项顺序不受编辑影响(前项已执行则该项自然成为最前待执行项)。\n */\nconst editingSuccessor = ref<string | null | undefined>(undefined)\n\n/** 编辑队列项:取回输入框重新编辑;已有未完成编辑时先按锚点还原,避免内容丢失 */\nconst handleQueueEdit = (id: string) => {\n restoreEditing()\n\n const index = queue.value.findIndex((item) => item.id === id)\n if (index === -1) return\n editingSuccessor.value = queue.value[index + 1]?.id ?? null\n\n const item = removeQueued(id)\n if (item) chatInputRef.value?.setContent(item.content)\n}\n\n/** 将输入框中未提交的编辑内容按原锚点插回队列 */\nconst restoreEditing = () => {\n if (editingSuccessor.value === undefined) return\n const content = chatInputRef.value?.getContent().trim()\n if (content) enqueue(content, undefined, editingSuccessor.value ?? undefined)\n editingSuccessor.value = undefined\n}\n\nconst handleSend = (content: string, attachments: ChatAttachment[]) => {\n // 编辑态提交:插回原锚点位置(会话进行中则留在队列,空闲时由 enqueue 自动消耗队首)\n if (editingSuccessor.value !== undefined) {\n enqueue(content, attachments, editingSuccessor.value ?? undefined)\n editingSuccessor.value = undefined\n return\n }\n send(content, attachments)\n}\n\ndefineExpose<_AiChatExposed>({\n send,\n abort,\n regenerate,\n clear,\n tokenUsage,\n lastTurnUsage,\n queue,\n startQueued,\n removeQueued,\n enqueue\n})\n</script>\n"],"mappings":"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"}
|