@veltra/ai 2.1.6 → 2.1.8
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/README.md +42 -0
- package/dist/chat/fold.d.ts +41 -0
- package/dist/chat/fold.js +208 -0
- package/dist/chat/fold.js.map +1 -0
- package/dist/chat/index.d.ts +4 -2
- package/dist/chat/session.d.ts +123 -0
- package/dist/chat/session.js +79 -0
- package/dist/chat/session.js.map +1 -0
- package/dist/chat/types.d.ts +38 -1
- package/dist/chat/use-chat-helpers.js +39 -0
- package/dist/chat/use-chat-helpers.js.map +1 -0
- package/dist/chat/use-chat-session.js +68 -0
- package/dist/chat/use-chat-session.js.map +1 -0
- package/dist/chat/use-chat.d.ts +50 -2
- package/dist/chat/use-chat.js +73 -39
- package/dist/chat/use-chat.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js +49 -14
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/approval-banner.js +54 -0
- package/dist/components/ai-chat/approval-banner.js.map +1 -0
- package/dist/components/ai-chat/ask-question.js +32 -51
- package/dist/components/ai-chat/ask-question.js.map +1 -1
- package/dist/components/ai-chat/di.js.map +1 -1
- package/dist/components/ai-chat/job-bar.js +48 -0
- package/dist/components/ai-chat/job-bar.js.map +1 -0
- package/dist/components/ai-chat/message-list.js +5 -2
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/queue-list.js +9 -3
- package/dist/components/ai-chat/queue-list.js.map +1 -1
- package/dist/components/ai-chat/side-panel.js +12 -30
- package/dist/components/ai-chat/side-panel.js.map +1 -1
- package/dist/components/ai-chat/style2.css +146 -8
- package/dist/components/ai-chat/tool-call.js +51 -40
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-chat/tool-icons.js +31 -0
- package/dist/components/ai-chat/tool-icons.js.map +1 -0
- package/dist/components/ai-chat/tool-json.js +40 -0
- package/dist/components/ai-chat/tool-json.js.map +1 -0
- package/dist/components/ai-orb/style2.css +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -1
- package/dist/tools/ask-question/ask-question.js +48 -2
- package/dist/tools/ask-question/ask-question.js.map +1 -1
- package/dist/types/ai-chat.d.ts +17 -6
- package/package.json +7 -7
- package/src/chat/__test__/fold.test.ts +136 -0
- package/src/chat/__test__/session.test.ts +191 -0
- package/src/chat/__test__/use-chat.test.ts +268 -1
- package/src/chat/fold.ts +260 -0
- package/src/chat/index.ts +2 -0
- package/src/chat/session.ts +168 -0
- package/src/chat/types.ts +40 -0
- package/src/chat/use-chat-helpers.ts +39 -0
- package/src/chat/use-chat-session.ts +87 -0
- package/src/chat/use-chat.ts +90 -44
- package/src/components/ai-chat/__test__/ai-chat.test.ts +405 -7
- package/src/components/ai-chat/__test__/tool-icons.test.ts +33 -0
- package/src/components/ai-chat/ai-chat.vue +39 -4
- package/src/components/ai-chat/approval-banner.vue +37 -0
- package/src/components/ai-chat/ask-question.vue +38 -54
- package/src/components/ai-chat/di.ts +6 -4
- package/src/components/ai-chat/job-bar.vue +41 -0
- package/src/components/ai-chat/message-list.vue +4 -1
- package/src/components/ai-chat/queue-list.vue +3 -1
- package/src/components/ai-chat/side-panel.vue +15 -13
- package/src/components/ai-chat/style.scss +158 -6
- package/src/components/ai-chat/tool-call.vue +46 -34
- package/src/components/ai-chat/tool-icons.ts +33 -0
- package/src/components/ai-chat/tool-json.vue +52 -0
- package/src/tools/ask-question/ask-question.ts +53 -4
- package/src/types/ai-chat.ts +17 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import _sfc_main$1 from "./components/ai-orb/ai-orb.js";
|
|
2
2
|
import _sfc_main from "./components/ai-chat/ai-chat.js";
|
|
3
|
-
import { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./chat/types.js";
|
|
3
|
+
import { ChatAttachment, ChatJob, ChatJobStatus, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolMeta, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, ToolCallStatus } from "./chat/types.js";
|
|
4
|
+
import { AskQuestionAnswer, AskQuestionArgs, AskQuestionItem, AskQuestionResult } from "./tools/ask-question/ask-question.js";
|
|
5
|
+
import "./tools/index.js";
|
|
6
|
+
import { ChatSessionAdapter, ChatSessionEvent, ChatSessionTransport, createServerTransport, isServerTransport } from "./chat/session.js";
|
|
4
7
|
import { ChatModel, ChatModelOption, ChatProvider, ChatReasoningLevel } from "./providers/types.js";
|
|
5
8
|
import { AiChatEmits, AiChatExposed, AiChatProps, _AiChatExposed } from "./types/ai-chat.js";
|
|
6
9
|
import { AiOrbEmits, AiOrbExposed, AiOrbProps, AiOrbReaction, AiOrbStatus } from "./types/ai-orb.js";
|
|
7
10
|
import "./types/index.js";
|
|
11
|
+
import { ChatFoldState, ChatPendingApproval, createFoldState, foldSessionEvent } from "./chat/fold.js";
|
|
8
12
|
import { UseChatOptions, useChat } from "./chat/use-chat.js";
|
|
9
13
|
import { OpenAITransport, OpenAITransportOptions, createOpenAITransport } from "./chat/transports/openai.js";
|
|
10
14
|
import "./chat/index.js";
|
|
11
|
-
|
|
12
|
-
import "./tools/index.js";
|
|
13
|
-
export { type AiChatEmits, type AiChatExposed, type AiChatProps, type AiOrbEmits, type AiOrbExposed, type AiOrbProps, type AiOrbReaction, type AiOrbStatus, type AskQuestionAnswer, type AskQuestionArgs, type AskQuestionItem, type AskQuestionResult, ChatAttachment, ChatMessage, type ChatModel, type ChatModelOption, type ChatProvider, ChatQueuedMessage, type ChatReasoningLevel, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, _sfc_main as UAiChat, _sfc_main$1 as UAiOrb, UseChatOptions, type _AiChatExposed, createOpenAITransport, useChat };
|
|
15
|
+
export { type AiChatEmits, type AiChatExposed, type AiChatProps, type AiOrbEmits, type AiOrbExposed, type AiOrbProps, type AiOrbReaction, type AiOrbStatus, type AskQuestionAnswer, type AskQuestionArgs, type AskQuestionItem, type AskQuestionResult, ChatAttachment, ChatFoldState, ChatJob, ChatJobStatus, ChatMessage, type ChatModel, type ChatModelOption, ChatPendingApproval, type ChatProvider, ChatQueuedMessage, type ChatReasoningLevel, ChatSessionAdapter, ChatSessionEvent, ChatSessionTransport, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolContext, ChatToolMeta, ChatToolRenderProps, ChatTransport, ChatTransportHandlers, ChatTransportRequest, OpenAITransport, OpenAITransportOptions, ToolCallStatus, _sfc_main as UAiChat, _sfc_main$1 as UAiOrb, UseChatOptions, type _AiChatExposed, createFoldState, createOpenAITransport, createServerTransport, foldSessionEvent, isServerTransport, useChat };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createServerTransport, isServerTransport } from "./chat/session.js";
|
|
2
|
+
import { createFoldState, foldSessionEvent } from "./chat/fold.js";
|
|
1
3
|
import { useChat } from "./chat/use-chat.js";
|
|
2
4
|
import { createOpenAITransport } from "./chat/transports/openai.js";
|
|
3
5
|
import _sfc_main$1 from "./components/ai-orb/ai-orb.js";
|
|
4
6
|
import _sfc_main from "./components/ai-chat/ai-chat.js";
|
|
5
|
-
export { _sfc_main as UAiChat, _sfc_main$1 as UAiOrb, createOpenAITransport, useChat };
|
|
7
|
+
export { _sfc_main as UAiChat, _sfc_main$1 as UAiOrb, createFoldState, createOpenAITransport, createServerTransport, foldSessionEvent, isServerTransport, useChat };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { deferAskQuestion, takeAskQuestionDeferred } from "./deferred.js";
|
|
2
1
|
import _sfc_main from "../../components/ai-chat/ask-question.js";
|
|
2
|
+
import { deferAskQuestion, resolveAskQuestion, takeAskQuestionDeferred } from "./deferred.js";
|
|
3
|
+
import { computed, defineComponent, h } from "vue";
|
|
3
4
|
import { QuestionFilled } from "@veltra/icons/normal";
|
|
4
5
|
//#region src/tools/ask-question/ask-question.ts
|
|
5
6
|
const DEFAULT_DESCRIPTION = "当需求不明确或存在歧义时,向用户提问以澄清。一次可提 1-4 个关键问题;每个问题可提供若干预设选项,用户可选择选项或自定义输入。用户作答后再继续生成。";
|
|
@@ -33,6 +34,51 @@ const QUESTIONS_SCHEMA = {
|
|
|
33
34
|
} },
|
|
34
35
|
required: ["questions"]
|
|
35
36
|
};
|
|
37
|
+
function parseQuestions(toolCall) {
|
|
38
|
+
try {
|
|
39
|
+
const args = JSON.parse(toolCall.arguments || "{}");
|
|
40
|
+
return Array.isArray(args.questions) ? args.questions : [];
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function parseAnswers(toolCall) {
|
|
46
|
+
if (toolCall.status !== "success" || !toolCall.result) return void 0;
|
|
47
|
+
try {
|
|
48
|
+
const result = JSON.parse(toolCall.result);
|
|
49
|
+
return Array.isArray(result.answers) ? result.answers : void 0;
|
|
50
|
+
} catch {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function statusError(toolCall) {
|
|
55
|
+
if (toolCall.status !== "error") return void 0;
|
|
56
|
+
const err = toolCall.error ?? "";
|
|
57
|
+
if (err.includes("Abort") || err.includes("取消")) return "已取消";
|
|
58
|
+
return err || "提问失败";
|
|
59
|
+
}
|
|
60
|
+
/** 把 toolCall 转成提问表单的 questions / onSubmit;提交仍走 resolveAskQuestion */
|
|
61
|
+
const AskQuestionRender = defineComponent({
|
|
62
|
+
name: "AskQuestionRender",
|
|
63
|
+
props: { toolCall: {
|
|
64
|
+
type: Object,
|
|
65
|
+
required: true
|
|
66
|
+
} },
|
|
67
|
+
setup(props) {
|
|
68
|
+
const questions = computed(() => parseQuestions(props.toolCall));
|
|
69
|
+
const answers = computed(() => parseAnswers(props.toolCall));
|
|
70
|
+
const error = computed(() => statusError(props.toolCall));
|
|
71
|
+
const onSubmit = (value) => {
|
|
72
|
+
resolveAskQuestion(props.toolCall.id, { answers: Array.isArray(value) ? value : value.answers });
|
|
73
|
+
};
|
|
74
|
+
return () => h(_sfc_main, {
|
|
75
|
+
questions: questions.value,
|
|
76
|
+
answers: answers.value,
|
|
77
|
+
error: error.value,
|
|
78
|
+
onSubmit
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
36
82
|
/**
|
|
37
83
|
* 创建内置提问工具:execute 挂起等待用户在 UI 中作答,提交后结果回灌模型。
|
|
38
84
|
* 工具卡片由内联提问表单渲染(多题分页导航,提交后展示问答摘要,不自动折叠)。
|
|
@@ -45,7 +91,7 @@ function createAskQuestionTool(options = {}) {
|
|
|
45
91
|
parameters: QUESTIONS_SCHEMA,
|
|
46
92
|
icon,
|
|
47
93
|
label,
|
|
48
|
-
render:
|
|
94
|
+
render: AskQuestionRender,
|
|
49
95
|
execute: (_args, { toolCall, signal }) => new Promise((resolve, reject) => {
|
|
50
96
|
deferAskQuestion(toolCall.id, {
|
|
51
97
|
resolve,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ask-question.js","names":["AskQuestion"],"sources":["../../../src/tools/ask-question/ask-question.ts"],"sourcesContent":["import { QuestionFilled } from '@veltra/icons/normal'\nimport type
|
|
1
|
+
{"version":3,"file":"ask-question.js","names":["AskQuestion"],"sources":["../../../src/tools/ask-question/ask-question.ts"],"sourcesContent":["import { QuestionFilled } from '@veltra/icons/normal'\nimport { computed, defineComponent, h, type Component, type PropType } from 'vue'\n\nimport type { ChatTool, ChatToolCall } from '../../chat/types'\nimport AskQuestion from '../../components/ai-chat/ask-question.vue'\nimport { deferAskQuestion, resolveAskQuestion, takeAskQuestionDeferred } from './deferred'\n\n/** 单个提问项 */\nexport interface AskQuestionItem {\n /** 问题文案 */\n question: string\n /** 预设选项(单选);缺省为纯文本题 */\n options?: string[]\n /** 自定义输入占位文案 */\n placeholder?: string\n}\n\n/** 一条问答结果 */\nexport interface AskQuestionAnswer {\n /** 问题文案 */\n question: string\n /** 用户回答(选中的选项或自定义输入) */\n answer: string\n}\n\n/** 提问工具参数(模型输出) */\nexport interface AskQuestionArgs {\n questions: AskQuestionItem[]\n}\n\n/** 提问工具结果(序列化后回灌模型,渲染层据此展示问答摘要) */\nexport interface AskQuestionResult {\n answers: AskQuestionAnswer[]\n}\n\nexport interface CreateAskQuestionToolOptions {\n /** 工具名(传给模型),默认 askQuestion */\n name?: string\n /** 工具描述(传给模型) */\n description?: string\n /** 工具显示名,默认「提问」 */\n label?: string\n /** 工具图标,默认 QuestionFilled */\n icon?: Component\n}\n\nconst DEFAULT_DESCRIPTION =\n '当需求不明确或存在歧义时,向用户提问以澄清。一次可提 1-4 个关键问题;' +\n '每个问题可提供若干预设选项,用户可选择选项或自定义输入。用户作答后再继续生成。'\n\n/** 提问参数 JSON Schema(原样传给模型) */\nconst QUESTIONS_SCHEMA: Record<string, unknown> = {\n type: 'object',\n properties: {\n questions: {\n type: 'array',\n description: '问题列表',\n minItems: 1,\n maxItems: 4,\n items: {\n type: 'object',\n properties: {\n question: { type: 'string', description: '问题文案' },\n options: {\n type: 'array',\n description: '预设选项(单选);缺省为纯文本题',\n items: { type: 'string' }\n },\n placeholder: { type: 'string', description: '自定义输入占位文案' }\n },\n required: ['question']\n }\n }\n },\n required: ['questions']\n}\n\nfunction parseQuestions(toolCall: ChatToolCall): AskQuestionItem[] {\n try {\n const args = JSON.parse(toolCall.arguments || '{}') as Partial<AskQuestionArgs>\n return Array.isArray(args.questions) ? args.questions : []\n } catch {\n return []\n }\n}\n\nfunction parseAnswers(toolCall: ChatToolCall): AskQuestionAnswer[] | undefined {\n if (toolCall.status !== 'success' || !toolCall.result) return undefined\n try {\n const result = JSON.parse(toolCall.result) as Partial<AskQuestionResult>\n return Array.isArray(result.answers) ? result.answers : undefined\n } catch {\n return undefined\n }\n}\n\nfunction statusError(toolCall: ChatToolCall): string | undefined {\n if (toolCall.status !== 'error') return undefined\n const err = toolCall.error ?? ''\n if (err.includes('Abort') || err.includes('取消')) return '已取消'\n return err || '提问失败'\n}\n\n/** 把 toolCall 转成提问表单的 questions / onSubmit;提交仍走 resolveAskQuestion */\nconst AskQuestionRender = defineComponent({\n name: 'AskQuestionRender',\n props: { toolCall: { type: Object as PropType<ChatToolCall>, required: true } },\n setup(props) {\n const questions = computed(() => parseQuestions(props.toolCall))\n const answers = computed(() => parseAnswers(props.toolCall))\n const error = computed(() => statusError(props.toolCall))\n const onSubmit = (value: AskQuestionAnswer[] | AskQuestionResult) => {\n resolveAskQuestion(props.toolCall.id, {\n answers: Array.isArray(value) ? value : value.answers\n })\n }\n return () =>\n h(AskQuestion, {\n questions: questions.value,\n answers: answers.value,\n error: error.value,\n onSubmit\n })\n }\n})\n\n/**\n * 创建内置提问工具:execute 挂起等待用户在 UI 中作答,提交后结果回灌模型。\n * 工具卡片由内联提问表单渲染(多题分页导航,提交后展示问答摘要,不自动折叠)。\n */\nexport function createAskQuestionTool(\n options: CreateAskQuestionToolOptions = {}\n): ChatTool<AskQuestionArgs> {\n const {\n name = 'askQuestion',\n description = DEFAULT_DESCRIPTION,\n label = '提问',\n icon = QuestionFilled\n } = options\n\n return {\n name,\n description,\n parameters: QUESTIONS_SCHEMA,\n icon,\n label,\n render: AskQuestionRender,\n execute: (_args, { toolCall, signal }) =>\n new Promise<AskQuestionResult>((resolve, reject) => {\n deferAskQuestion(toolCall.id, { resolve, reject })\n signal.addEventListener(\n 'abort',\n () => {\n // 不依赖 signal.reason(各环境 DOMException 行为不一),统一拒绝为 AbortError\n const error = new Error('Aborted')\n error.name = 'AbortError'\n takeAskQuestionDeferred(toolCall.id)?.reject(error)\n },\n { once: true }\n )\n })\n }\n}\n"],"mappings":";;;;;AA8CA,MAAM,sBACJ;;AAIF,MAAM,mBAA4C;CAChD,MAAM;CACN,YAAY,EACV,WAAW;EACT,MAAM;EACN,aAAa;EACb,UAAU;EACV,UAAU;EACV,OAAO;GACL,MAAM;GACN,YAAY;IACV,UAAU;KAAE,MAAM;KAAU,aAAa;IAAO;IAChD,SAAS;KACP,MAAM;KACN,aAAa;KACb,OAAO,EAAE,MAAM,SAAS;IAC1B;IACA,aAAa;KAAE,MAAM;KAAU,aAAa;IAAY;GAC1D;GACA,UAAU,CAAC,UAAU;EACvB;CACF,EACF;CACA,UAAU,CAAC,WAAW;AACxB;AAEA,SAAS,eAAe,UAA2C;CACjE,IAAI;EACF,MAAM,OAAO,KAAK,MAAM,SAAS,aAAa,IAAI;EAClD,OAAO,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK,YAAY,CAAC;CAC3D,QAAQ;EACN,OAAO,CAAC;CACV;AACF;AAEA,SAAS,aAAa,UAAyD;CAC7E,IAAI,SAAS,WAAW,aAAa,CAAC,SAAS,QAAQ,OAAO,KAAA;CAC9D,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,SAAS,MAAM;EACzC,OAAO,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO,UAAU,KAAA;CAC1D,QAAQ;EACN;CACF;AACF;AAEA,SAAS,YAAY,UAA4C;CAC/D,IAAI,SAAS,WAAW,SAAS,OAAO,KAAA;CACxC,MAAM,MAAM,SAAS,SAAS;CAC9B,IAAI,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,IAAI,GAAG,OAAO;CACxD,OAAO,OAAO;AAChB;;AAGA,MAAM,oBAAoB,gBAAgB;CACxC,MAAM;CACN,OAAO,EAAE,UAAU;EAAE,MAAM;EAAkC,UAAU;CAAK,EAAE;CAC9E,MAAM,OAAO;EACX,MAAM,YAAY,eAAe,eAAe,MAAM,QAAQ,CAAC;EAC/D,MAAM,UAAU,eAAe,aAAa,MAAM,QAAQ,CAAC;EAC3D,MAAM,QAAQ,eAAe,YAAY,MAAM,QAAQ,CAAC;EACxD,MAAM,YAAY,UAAmD;GACnE,mBAAmB,MAAM,SAAS,IAAI,EACpC,SAAS,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAChD,CAAC;EACH;EACA,aACE,EAAEA,WAAa;GACb,WAAW,UAAU;GACrB,SAAS,QAAQ;GACjB,OAAO,MAAM;GACb;EACF,CAAC;CACL;AACF,CAAC;;;;;AAMD,SAAgB,sBACd,UAAwC,CAAC,GACd;CAC3B,MAAM,EACJ,OAAO,eACP,cAAc,qBACd,QAAQ,MACR,OAAO,mBACL;CAEJ,OAAO;EACL;EACA;EACA,YAAY;EACZ;EACA;EACA,QAAQ;EACR,UAAU,OAAO,EAAE,UAAU,aAC3B,IAAI,SAA4B,SAAS,WAAW;GAClD,iBAAiB,SAAS,IAAI;IAAE;IAAS;GAAO,CAAC;GACjD,OAAO,iBACL,eACM;IAEJ,MAAM,wBAAQ,IAAI,MAAM,SAAS;IACjC,MAAM,OAAO;IACb,wBAAwB,SAAS,EAAE,CAAC,EAAE,OAAO,KAAK;GACpD,GACA,EAAE,MAAM,KAAK,CACf;EACF,CAAC;CACL;AACF"}
|
package/dist/types/ai-chat.d.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatTransport } from "../chat/types.js";
|
|
1
|
+
import { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatTool, ChatToolCall, ChatToolMeta, ChatTransport } from "../chat/types.js";
|
|
2
|
+
import { ChatSessionTransport } from "../chat/session.js";
|
|
2
3
|
import { ChatModelOption } from "../providers/types.js";
|
|
3
|
-
import { Ref } from "vue";
|
|
4
|
+
import { Component, Ref } from "vue";
|
|
4
5
|
import { DeconstructValue } from "@veltra/utils";
|
|
5
6
|
//#region src/types/ai-chat.d.ts
|
|
6
7
|
interface AiChatProps {
|
|
7
|
-
/**
|
|
8
|
-
transport: ChatTransport;
|
|
9
|
-
/**
|
|
10
|
-
|
|
8
|
+
/** 传输层(必填);函数型 ChatTransport 或 session 对象,可使用 createOpenAITransport / createServerTransport */
|
|
9
|
+
transport: ChatTransport | ChatSessionTransport;
|
|
10
|
+
/**
|
|
11
|
+
* 工具列表。函数 transport 传入 ChatTool[](必有 execute);
|
|
12
|
+
* session 下可为 ChatToolMeta[](仅渲染元信息)。ChatTool[] 仍可赋值。
|
|
13
|
+
*/
|
|
14
|
+
tools?: (ChatTool | ChatToolMeta)[];
|
|
11
15
|
/** 系统提示词 */
|
|
12
16
|
systemPrompt?: string;
|
|
13
17
|
/**
|
|
@@ -41,6 +45,13 @@ interface AiChatProps {
|
|
|
41
45
|
* 默认 false:仅在拿到 usage 时显示会话累计「总 token」。接口未返回 usage 时不展示。
|
|
42
46
|
*/
|
|
43
47
|
tokenUsageDetail?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* 覆盖包内 toolName → icon 映射(精确名优先于内置名称规则)。
|
|
50
|
+
* 缺省不传则走内置表;未命中的名称仍用兜底图标,不得 throw。
|
|
51
|
+
*/
|
|
52
|
+
toolIcons?: Record<string, Component>;
|
|
53
|
+
/** 只读:不展示输入区、欢迎语不可发送、队列无插队/编辑/移除。默认 false */
|
|
54
|
+
readonly?: boolean;
|
|
44
55
|
}
|
|
45
56
|
interface AiChatEmits {
|
|
46
57
|
/** 消息列表变化 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veltra/ai",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.8",
|
|
4
4
|
"homepage": "https://github.com/cabinet-fe/ultra-ui#readme",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/cabinet-fe/ultra-ui/issues"
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"markstream-vue": "^2.0.
|
|
39
|
+
"markstream-vue": "^2.0.8"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"@veltra/compositions": "^1.
|
|
43
|
-
"@veltra/desktop": "^1.
|
|
42
|
+
"@veltra/compositions": "^1.7.9",
|
|
43
|
+
"@veltra/desktop": "^1.7.9",
|
|
44
44
|
"@veltra/icons": "^1.5.0",
|
|
45
|
-
"@veltra/styles": "^1.
|
|
46
|
-
"@veltra/utils": "^1.
|
|
47
|
-
"vue": ">=3.5.
|
|
45
|
+
"@veltra/styles": "^1.7.9",
|
|
46
|
+
"@veltra/utils": "^1.7.9",
|
|
47
|
+
"vue": ">=3.5.42"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { createFoldState, foldSessionEvent } from '../fold'
|
|
4
|
+
import type { ChatSessionEvent } from '../session'
|
|
5
|
+
|
|
6
|
+
const foldAll = (events: ChatSessionEvent[]) => events.reduce(foldSessionEvent, createFoldState())
|
|
7
|
+
|
|
8
|
+
describe('foldSessionEvent', () => {
|
|
9
|
+
it('user/message 按服务端回显追加,不制造本地占位', () => {
|
|
10
|
+
const state = foldAll([{ type: 'user/message', messageId: 'u1', seq: 1, content: '你好' }])
|
|
11
|
+
expect(state.messages).toEqual([{ id: 'u1', role: 'user', content: '你好' }])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('assistant/chunk 按 messageId 累积 content 与 reasoning', () => {
|
|
15
|
+
const state = foldAll([
|
|
16
|
+
{ type: 'assistant/chunk', messageId: 'a1', seq: 1, delta: '你', reasoningDelta: '想' },
|
|
17
|
+
{ type: 'assistant/chunk', messageId: 'a1', seq: 2, delta: '好', reasoningDelta: '一下' }
|
|
18
|
+
])
|
|
19
|
+
expect(state.messages).toHaveLength(1)
|
|
20
|
+
expect(state.messages[0]).toMatchObject({
|
|
21
|
+
id: 'a1',
|
|
22
|
+
role: 'assistant',
|
|
23
|
+
content: '你好',
|
|
24
|
+
reasoning: '想一下',
|
|
25
|
+
status: 'streaming'
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('assistant/message 定稿内容与状态', () => {
|
|
30
|
+
const state = foldAll([
|
|
31
|
+
{ type: 'assistant/chunk', messageId: 'a1', seq: 1, delta: '草' },
|
|
32
|
+
{
|
|
33
|
+
type: 'assistant/message',
|
|
34
|
+
messageId: 'a1',
|
|
35
|
+
seq: 2,
|
|
36
|
+
content: '你好世界',
|
|
37
|
+
reasoning: '想完了'
|
|
38
|
+
}
|
|
39
|
+
])
|
|
40
|
+
expect(state.messages[0]).toMatchObject({
|
|
41
|
+
id: 'a1',
|
|
42
|
+
content: '你好世界',
|
|
43
|
+
reasoning: '想完了',
|
|
44
|
+
status: 'done'
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('tool/call 追加 pending 的 ChatToolCall 并保留 view', () => {
|
|
49
|
+
const view = { preview: 'ls' }
|
|
50
|
+
const state = foldAll([
|
|
51
|
+
{ type: 'assistant/chunk', messageId: 'a1', seq: 1, delta: '' },
|
|
52
|
+
{ type: 'tool/call', callId: 'c1', name: 'bash', arguments: '{"cmd":"ls"}', seq: 2, view }
|
|
53
|
+
])
|
|
54
|
+
expect(state.messages[0]?.toolCalls).toEqual([
|
|
55
|
+
{ id: 'c1', name: 'bash', arguments: '{"cmd":"ls"}', status: 'pending', view }
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('tool/result 按 callId 更新 status / result / error / view', () => {
|
|
60
|
+
const state = foldAll([
|
|
61
|
+
{ type: 'tool/call', callId: 'c1', name: 'bash', arguments: '{}', seq: 1, view: { step: 1 } },
|
|
62
|
+
{
|
|
63
|
+
type: 'tool/result',
|
|
64
|
+
callId: 'c1',
|
|
65
|
+
status: 'error',
|
|
66
|
+
result: 'out',
|
|
67
|
+
error: 'boom',
|
|
68
|
+
seq: 2,
|
|
69
|
+
view: { step: 2 }
|
|
70
|
+
}
|
|
71
|
+
])
|
|
72
|
+
expect(state.messages[0]?.toolCalls?.[0]).toMatchObject({
|
|
73
|
+
id: 'c1',
|
|
74
|
+
status: 'error',
|
|
75
|
+
result: 'out',
|
|
76
|
+
error: 'boom',
|
|
77
|
+
view: { step: 2 }
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('queue/snapshot 与 jobs/snapshot 整体替换', () => {
|
|
82
|
+
const state = foldAll([
|
|
83
|
+
{ type: 'queue/snapshot', items: [{ id: 'q1', content: '旧' }] },
|
|
84
|
+
{
|
|
85
|
+
type: 'jobs/snapshot',
|
|
86
|
+
jobs: [{ id: 'j1', kind: 'bash', label: '旧作业', status: 'running' }]
|
|
87
|
+
},
|
|
88
|
+
{ type: 'queue/snapshot', items: [{ id: 'q2', content: '新' }] },
|
|
89
|
+
{ type: 'jobs/snapshot', jobs: [{ id: 'j2', kind: 'web', label: '新作业', status: 'done' }] }
|
|
90
|
+
])
|
|
91
|
+
expect(state.queue).toEqual([{ id: 'q2', content: '新' }])
|
|
92
|
+
expect(state.jobs).toEqual([{ id: 'j2', kind: 'web', label: '新作业', status: 'done' }])
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('projection:tokenUsage 合并、title 取出、其余进 projections', () => {
|
|
96
|
+
const state = foldAll([
|
|
97
|
+
{
|
|
98
|
+
type: 'projection',
|
|
99
|
+
key: 'tokenUsage',
|
|
100
|
+
seq: 1,
|
|
101
|
+
value: { promptTokens: 1, completionTokens: 2, totalTokens: 3 }
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: 'projection',
|
|
105
|
+
key: 'tokenUsage',
|
|
106
|
+
seq: 2,
|
|
107
|
+
value: { promptTokens: 4, completionTokens: 5, totalTokens: 9 }
|
|
108
|
+
},
|
|
109
|
+
{ type: 'projection', key: 'title', seq: 3, value: '会话标题' },
|
|
110
|
+
{ type: 'projection', key: 'extra', seq: 4, value: { n: 1 } }
|
|
111
|
+
])
|
|
112
|
+
expect(state.tokenUsage).toEqual({ promptTokens: 5, completionTokens: 7, totalTokens: 12 })
|
|
113
|
+
expect(state.title).toBe('会话标题')
|
|
114
|
+
expect(state.projections).toEqual({ extra: { n: 1 } })
|
|
115
|
+
expect(state.projections).not.toHaveProperty('title')
|
|
116
|
+
expect(state.projections).not.toHaveProperty('tokenUsage')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('乱序 seq 不覆盖已应用事件', () => {
|
|
120
|
+
const applied = foldSessionEvent(createFoldState(), {
|
|
121
|
+
type: 'user/message',
|
|
122
|
+
messageId: 'u2',
|
|
123
|
+
seq: 2,
|
|
124
|
+
content: '后到的先到'
|
|
125
|
+
})
|
|
126
|
+
const skipped = foldSessionEvent(applied, {
|
|
127
|
+
type: 'user/message',
|
|
128
|
+
messageId: 'u1',
|
|
129
|
+
seq: 1,
|
|
130
|
+
content: '乱序应丢弃'
|
|
131
|
+
})
|
|
132
|
+
expect(skipped).toBe(applied)
|
|
133
|
+
expect(skipped.messages).toEqual([{ id: 'u2', role: 'user', content: '后到的先到' }])
|
|
134
|
+
expect(skipped.lastSeq).toBe(2)
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { createFoldState, foldSessionEvent } from '../fold'
|
|
4
|
+
import * as chat from '../index'
|
|
5
|
+
import {
|
|
6
|
+
createServerTransport,
|
|
7
|
+
isServerTransport,
|
|
8
|
+
type ChatSessionAdapter,
|
|
9
|
+
type ChatSessionEvent
|
|
10
|
+
} from '../session'
|
|
11
|
+
|
|
12
|
+
function createFakeAdapter(options?: {
|
|
13
|
+
catchUp?: ChatSessionEvent[]
|
|
14
|
+
}): ChatSessionAdapter & {
|
|
15
|
+
emit: (event: ChatSessionEvent) => void
|
|
16
|
+
disconnect: () => void
|
|
17
|
+
unsubscribed: () => boolean
|
|
18
|
+
} {
|
|
19
|
+
let listener: { onEvent(event: ChatSessionEvent): void; onDisconnect?(): void } | undefined
|
|
20
|
+
let unsubscribed = false
|
|
21
|
+
|
|
22
|
+
const adapter: ChatSessionAdapter = {
|
|
23
|
+
subscribe(handlers) {
|
|
24
|
+
listener = handlers
|
|
25
|
+
return () => {
|
|
26
|
+
unsubscribed = true
|
|
27
|
+
listener = undefined
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
send: vi.fn(async () => {}),
|
|
31
|
+
cancel: vi.fn(async () => {}),
|
|
32
|
+
respond: vi.fn(async () => {}),
|
|
33
|
+
fetchHistory: vi.fn(async () => ({ events: options?.catchUp ?? [], hasMore: false })),
|
|
34
|
+
selectModel: vi.fn(async () => {})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Object.assign(adapter, {
|
|
38
|
+
emit(event: ChatSessionEvent) {
|
|
39
|
+
listener?.onEvent(event)
|
|
40
|
+
},
|
|
41
|
+
disconnect() {
|
|
42
|
+
listener?.onDisconnect?.()
|
|
43
|
+
},
|
|
44
|
+
unsubscribed: () => unsubscribed
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('isServerTransport', () => {
|
|
49
|
+
it('仅对 kind: session 的对象为 true', () => {
|
|
50
|
+
const transport = createServerTransport(createFakeAdapter())
|
|
51
|
+
expect(transport.kind).toBe('session')
|
|
52
|
+
expect(isServerTransport(transport)).toBe(true)
|
|
53
|
+
expect(isServerTransport({ kind: 'session' })).toBe(true)
|
|
54
|
+
expect(isServerTransport(() => {})).toBe(false)
|
|
55
|
+
expect(isServerTransport(null)).toBe(false)
|
|
56
|
+
expect(isServerTransport(undefined)).toBe(false)
|
|
57
|
+
expect(isServerTransport({})).toBe(false)
|
|
58
|
+
expect(isServerTransport({ kind: 'client' })).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('createServerTransport', () => {
|
|
63
|
+
it('主入口导出 A1 符号且不导出 createBedrockTransport', () => {
|
|
64
|
+
expect(chat.createServerTransport).toBeTypeOf('function')
|
|
65
|
+
expect(chat.isServerTransport).toBeTypeOf('function')
|
|
66
|
+
expect(chat.foldSessionEvent).toBeTypeOf('function')
|
|
67
|
+
expect(chat.createFoldState).toBeTypeOf('function')
|
|
68
|
+
expect('createBedrockTransport' in chat).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('后到的更小 seq 丢弃且 console.warn,不打乱已折叠状态', () => {
|
|
72
|
+
const adapter = createFakeAdapter()
|
|
73
|
+
const transport = createServerTransport(adapter)
|
|
74
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
75
|
+
let state = createFoldState()
|
|
76
|
+
transport.open({
|
|
77
|
+
onEvent(event) {
|
|
78
|
+
state = foldSessionEvent(state, event)
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
adapter.emit({ type: 'user/message', messageId: 'u2', seq: 2, content: '先到' })
|
|
83
|
+
adapter.emit({ type: 'user/message', messageId: 'u1', seq: 1, content: '乱序' })
|
|
84
|
+
|
|
85
|
+
expect(warn).toHaveBeenCalledTimes(1)
|
|
86
|
+
expect(warn.mock.calls[0]?.[0]).toMatch(/丢弃乱序事件/)
|
|
87
|
+
expect(state.messages).toHaveLength(1)
|
|
88
|
+
expect(state.messages[0]).toMatchObject({ id: 'u2', content: '先到' })
|
|
89
|
+
warn.mockRestore()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('open 的 disposer 释放订阅', () => {
|
|
93
|
+
const adapter = createFakeAdapter()
|
|
94
|
+
const transport = createServerTransport(adapter)
|
|
95
|
+
const onEvent = vi.fn()
|
|
96
|
+
const dispose = transport.open({ onEvent })
|
|
97
|
+
|
|
98
|
+
adapter.emit({ type: 'finish' })
|
|
99
|
+
expect(onEvent).toHaveBeenCalledTimes(1)
|
|
100
|
+
|
|
101
|
+
dispose()
|
|
102
|
+
expect(adapter.unsubscribed()).toBe(true)
|
|
103
|
+
adapter.emit({ type: 'finish' })
|
|
104
|
+
adapter.disconnect()
|
|
105
|
+
expect(onEvent).toHaveBeenCalledTimes(1)
|
|
106
|
+
expect(adapter.fetchHistory).not.toHaveBeenCalled()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('断开后 fetchHistory(lastSeq) 补拉再 fold', async () => {
|
|
110
|
+
const catchUp: ChatSessionEvent[] = [
|
|
111
|
+
{ type: 'user/message', messageId: 'u2', seq: 2, content: '补拉' }
|
|
112
|
+
]
|
|
113
|
+
const adapter = createFakeAdapter({ catchUp })
|
|
114
|
+
const transport = createServerTransport(adapter)
|
|
115
|
+
let state = createFoldState()
|
|
116
|
+
const onDisconnect = vi.fn()
|
|
117
|
+
transport.open({
|
|
118
|
+
onEvent(event) {
|
|
119
|
+
state = foldSessionEvent(state, event)
|
|
120
|
+
},
|
|
121
|
+
onDisconnect
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
adapter.emit({ type: 'user/message', messageId: 'u1', seq: 1, content: '已有' })
|
|
125
|
+
adapter.disconnect()
|
|
126
|
+
|
|
127
|
+
await vi.waitFor(() => {
|
|
128
|
+
expect(adapter.fetchHistory).toHaveBeenCalledWith(1)
|
|
129
|
+
expect(state.messages.map((m) => m.id)).toEqual(['u1', 'u2'])
|
|
130
|
+
expect(onDisconnect).toHaveBeenCalledTimes(1)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('send / cancel / respond / selectModel 同一动作未完成时忽略重复调用', async () => {
|
|
135
|
+
const adapter = createFakeAdapter()
|
|
136
|
+
let releaseSend!: () => void
|
|
137
|
+
let releaseCancel!: () => void
|
|
138
|
+
let releaseRespond!: () => void
|
|
139
|
+
let releaseSelect!: () => void
|
|
140
|
+
adapter.send = vi.fn(
|
|
141
|
+
() =>
|
|
142
|
+
new Promise<void>((resolve) => {
|
|
143
|
+
releaseSend = resolve
|
|
144
|
+
})
|
|
145
|
+
)
|
|
146
|
+
adapter.cancel = vi.fn(
|
|
147
|
+
() =>
|
|
148
|
+
new Promise<void>((resolve) => {
|
|
149
|
+
releaseCancel = resolve
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
adapter.respond = vi.fn(
|
|
153
|
+
() =>
|
|
154
|
+
new Promise<void>((resolve) => {
|
|
155
|
+
releaseRespond = resolve
|
|
156
|
+
})
|
|
157
|
+
)
|
|
158
|
+
adapter.selectModel = vi.fn(
|
|
159
|
+
() =>
|
|
160
|
+
new Promise<void>((resolve) => {
|
|
161
|
+
releaseSelect = resolve
|
|
162
|
+
})
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
const transport = createServerTransport(adapter)
|
|
166
|
+
|
|
167
|
+
const send1 = transport.send('a')
|
|
168
|
+
const send2 = transport.send('b')
|
|
169
|
+
expect(adapter.send).toHaveBeenCalledTimes(1)
|
|
170
|
+
releaseSend()
|
|
171
|
+
await Promise.all([send1, send2])
|
|
172
|
+
|
|
173
|
+
const cancel1 = transport.cancel()
|
|
174
|
+
const cancel2 = transport.cancel()
|
|
175
|
+
expect(adapter.cancel).toHaveBeenCalledTimes(1)
|
|
176
|
+
releaseCancel()
|
|
177
|
+
await Promise.all([cancel1, cancel2])
|
|
178
|
+
|
|
179
|
+
const respond1 = transport.respond('rpc-1', true)
|
|
180
|
+
const respond2 = transport.respond('rpc-2', false)
|
|
181
|
+
expect(adapter.respond).toHaveBeenCalledTimes(1)
|
|
182
|
+
releaseRespond()
|
|
183
|
+
await Promise.all([respond1, respond2])
|
|
184
|
+
|
|
185
|
+
const select1 = transport.selectModel('p', 'm1')
|
|
186
|
+
const select2 = transport.selectModel('p', 'm2')
|
|
187
|
+
expect(adapter.selectModel).toHaveBeenCalledTimes(1)
|
|
188
|
+
releaseSelect()
|
|
189
|
+
await Promise.all([select1, select2])
|
|
190
|
+
})
|
|
191
|
+
})
|