@veltra/ai 2.1.6 → 2.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +42 -0
  2. package/dist/chat/fold.d.ts +41 -0
  3. package/dist/chat/fold.js +208 -0
  4. package/dist/chat/fold.js.map +1 -0
  5. package/dist/chat/index.d.ts +4 -2
  6. package/dist/chat/session.d.ts +123 -0
  7. package/dist/chat/session.js +79 -0
  8. package/dist/chat/session.js.map +1 -0
  9. package/dist/chat/types.d.ts +38 -1
  10. package/dist/chat/use-chat-helpers.js +39 -0
  11. package/dist/chat/use-chat-helpers.js.map +1 -0
  12. package/dist/chat/use-chat-session.js +68 -0
  13. package/dist/chat/use-chat-session.js.map +1 -0
  14. package/dist/chat/use-chat.d.ts +50 -2
  15. package/dist/chat/use-chat.js +73 -39
  16. package/dist/chat/use-chat.js.map +1 -1
  17. package/dist/components/ai-chat/ai-chat.js +49 -14
  18. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  19. package/dist/components/ai-chat/approval-banner.js +54 -0
  20. package/dist/components/ai-chat/approval-banner.js.map +1 -0
  21. package/dist/components/ai-chat/ask-question.js +32 -51
  22. package/dist/components/ai-chat/ask-question.js.map +1 -1
  23. package/dist/components/ai-chat/di.js.map +1 -1
  24. package/dist/components/ai-chat/job-bar.js +48 -0
  25. package/dist/components/ai-chat/job-bar.js.map +1 -0
  26. package/dist/components/ai-chat/message-list.js +5 -2
  27. package/dist/components/ai-chat/message-list.js.map +1 -1
  28. package/dist/components/ai-chat/queue-list.js +9 -3
  29. package/dist/components/ai-chat/queue-list.js.map +1 -1
  30. package/dist/components/ai-chat/side-panel.js +12 -30
  31. package/dist/components/ai-chat/side-panel.js.map +1 -1
  32. package/dist/components/ai-chat/style2.css +146 -8
  33. package/dist/components/ai-chat/tool-call.js +51 -40
  34. package/dist/components/ai-chat/tool-call.js.map +1 -1
  35. package/dist/components/ai-chat/tool-icons.js +31 -0
  36. package/dist/components/ai-chat/tool-icons.js.map +1 -0
  37. package/dist/components/ai-chat/tool-json.js +40 -0
  38. package/dist/components/ai-chat/tool-json.js.map +1 -0
  39. package/dist/components/ai-orb/style2.css +1 -1
  40. package/dist/index.d.ts +6 -4
  41. package/dist/index.js +3 -1
  42. package/dist/tools/ask-question/ask-question.js +48 -2
  43. package/dist/tools/ask-question/ask-question.js.map +1 -1
  44. package/dist/types/ai-chat.d.ts +17 -6
  45. package/package.json +7 -7
  46. package/src/chat/__test__/fold.test.ts +136 -0
  47. package/src/chat/__test__/session.test.ts +191 -0
  48. package/src/chat/__test__/use-chat.test.ts +268 -1
  49. package/src/chat/fold.ts +260 -0
  50. package/src/chat/index.ts +2 -0
  51. package/src/chat/session.ts +168 -0
  52. package/src/chat/types.ts +40 -0
  53. package/src/chat/use-chat-helpers.ts +39 -0
  54. package/src/chat/use-chat-session.ts +87 -0
  55. package/src/chat/use-chat.ts +90 -44
  56. package/src/components/ai-chat/__test__/ai-chat.test.ts +405 -7
  57. package/src/components/ai-chat/__test__/tool-icons.test.ts +33 -0
  58. package/src/components/ai-chat/ai-chat.vue +39 -4
  59. package/src/components/ai-chat/approval-banner.vue +37 -0
  60. package/src/components/ai-chat/ask-question.vue +38 -54
  61. package/src/components/ai-chat/di.ts +6 -4
  62. package/src/components/ai-chat/job-bar.vue +41 -0
  63. package/src/components/ai-chat/message-list.vue +4 -1
  64. package/src/components/ai-chat/queue-list.vue +3 -1
  65. package/src/components/ai-chat/side-panel.vue +15 -13
  66. package/src/components/ai-chat/style.scss +158 -6
  67. package/src/components/ai-chat/tool-call.vue +46 -34
  68. package/src/components/ai-chat/tool-icons.ts +33 -0
  69. package/src/components/ai-chat/tool-json.vue +52 -0
  70. package/src/tools/ask-question/ask-question.ts +53 -4
  71. package/src/types/ai-chat.ts +17 -5
@@ -0,0 +1,68 @@
1
+ import { createFoldState, foldSessionEvent } from "./fold.js";
2
+ //#region src/chat/use-chat-session.ts
3
+ /** 把 fold 状态同步到 useChat 的渲染面 */
4
+ function syncFold(fold, surface) {
5
+ surface.messages.value = fold.messages;
6
+ surface.queue.value = fold.queue;
7
+ surface.jobs.value = fold.jobs;
8
+ surface.tokenUsage.value = fold.tokenUsage;
9
+ surface.projections.value = fold.projections;
10
+ surface.title.value = fold.title;
11
+ surface.running.value = fold.running;
12
+ surface.pendingApprovals.value = fold.pendingApprovals;
13
+ surface.pendingQuestion.value = fold.pendingQuestion;
14
+ }
15
+ /**
16
+ * session transport 的 open / fetchHistory / fold。
17
+ * 历史回放与实时 onEvent 走同一套 fold。
18
+ */
19
+ function createSessionRuntime(surface) {
20
+ let fold = createFoldState();
21
+ let opened = false;
22
+ const apply = (event) => {
23
+ fold = foldSessionEvent(fold, event);
24
+ syncFold(fold, surface);
25
+ };
26
+ return {
27
+ get opened() {
28
+ return opened;
29
+ },
30
+ attach(transport, onCleanup) {
31
+ if (typeof transport.open !== "function") return;
32
+ fold = createFoldState();
33
+ syncFold(fold, surface);
34
+ let disposed = false;
35
+ const dispose = transport.open({
36
+ onEvent(event) {
37
+ if (!disposed) apply(event);
38
+ },
39
+ onDisconnect() {
40
+ if (disposed) return;
41
+ fold = {
42
+ ...fold,
43
+ running: false
44
+ };
45
+ surface.running.value = false;
46
+ }
47
+ });
48
+ opened = true;
49
+ onCleanup(() => {
50
+ disposed = true;
51
+ opened = false;
52
+ dispose();
53
+ });
54
+ transport.fetchHistory().then(({ events }) => {
55
+ if (disposed) return;
56
+ for (const event of events) apply(event);
57
+ });
58
+ },
59
+ resetLocal() {
60
+ fold = createFoldState();
61
+ syncFold(fold, surface);
62
+ }
63
+ };
64
+ }
65
+ //#endregion
66
+ export { createSessionRuntime };
67
+
68
+ //# sourceMappingURL=use-chat-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-chat-session.js","names":[],"sources":["../../src/chat/use-chat-session.ts"],"sourcesContent":["import type { Ref } from 'vue'\n\nimport type { AskQuestionItem } from '../tools'\nimport {\n createFoldState,\n foldSessionEvent,\n type ChatFoldState,\n type ChatPendingApproval\n} from './fold'\nimport type { ChatSessionEvent, ChatSessionTransport } from './session'\nimport type { ChatJob, ChatMessage, ChatQueuedMessage, ChatTokenUsage } from './types'\n\nexport interface SessionChatSurface {\n messages: Ref<ChatMessage[]>\n queue: Ref<ChatQueuedMessage[]>\n jobs: Ref<ChatJob[]>\n tokenUsage: Ref<ChatTokenUsage | null>\n projections: Ref<Record<string, unknown>>\n title: Ref<string | null>\n running: Ref<boolean>\n pendingApprovals: Ref<ChatPendingApproval[]>\n pendingQuestion: Ref<{ questions: AskQuestionItem[]; rpcId: string } | null>\n}\n\n/** 把 fold 状态同步到 useChat 的渲染面 */\nfunction syncFold(fold: ChatFoldState, surface: SessionChatSurface) {\n surface.messages.value = fold.messages\n surface.queue.value = fold.queue\n surface.jobs.value = fold.jobs\n surface.tokenUsage.value = fold.tokenUsage\n surface.projections.value = fold.projections\n surface.title.value = fold.title\n surface.running.value = fold.running\n surface.pendingApprovals.value = fold.pendingApprovals\n surface.pendingQuestion.value = fold.pendingQuestion\n}\n\n/**\n * session transport 的 open / fetchHistory / fold。\n * 历史回放与实时 onEvent 走同一套 fold。\n */\nexport function createSessionRuntime(surface: SessionChatSurface) {\n let fold = createFoldState()\n let opened = false\n\n const apply = (event: ChatSessionEvent) => {\n fold = foldSessionEvent(fold, event)\n syncFold(fold, surface)\n }\n\n return {\n get opened() {\n return opened\n },\n attach(transport: ChatSessionTransport, onCleanup: (fn: () => void) => void) {\n // isServerTransport 只认 kind,不保证 open 存在\n if (typeof transport.open !== 'function') return\n fold = createFoldState()\n syncFold(fold, surface)\n let disposed = false\n const dispose = transport.open({\n onEvent(event) {\n if (!disposed) apply(event)\n },\n onDisconnect() {\n if (disposed) return\n fold = { ...fold, running: false }\n surface.running.value = false\n }\n })\n opened = true\n onCleanup(() => {\n disposed = true\n opened = false\n dispose()\n })\n void transport.fetchHistory().then(({ events }) => {\n if (disposed) return\n for (const event of events) apply(event)\n })\n },\n resetLocal() {\n fold = createFoldState()\n syncFold(fold, surface)\n }\n }\n}\n"],"mappings":";;;AAyBA,SAAS,SAAS,MAAqB,SAA6B;CAClE,QAAQ,SAAS,QAAQ,KAAK;CAC9B,QAAQ,MAAM,QAAQ,KAAK;CAC3B,QAAQ,KAAK,QAAQ,KAAK;CAC1B,QAAQ,WAAW,QAAQ,KAAK;CAChC,QAAQ,YAAY,QAAQ,KAAK;CACjC,QAAQ,MAAM,QAAQ,KAAK;CAC3B,QAAQ,QAAQ,QAAQ,KAAK;CAC7B,QAAQ,iBAAiB,QAAQ,KAAK;CACtC,QAAQ,gBAAgB,QAAQ,KAAK;AACvC;;;;;AAMA,SAAgB,qBAAqB,SAA6B;CAChE,IAAI,OAAO,gBAAgB;CAC3B,IAAI,SAAS;CAEb,MAAM,SAAS,UAA4B;EACzC,OAAO,iBAAiB,MAAM,KAAK;EACnC,SAAS,MAAM,OAAO;CACxB;CAEA,OAAO;EACL,IAAI,SAAS;GACX,OAAO;EACT;EACA,OAAO,WAAiC,WAAqC;GAE3E,IAAI,OAAO,UAAU,SAAS,YAAY;GAC1C,OAAO,gBAAgB;GACvB,SAAS,MAAM,OAAO;GACtB,IAAI,WAAW;GACf,MAAM,UAAU,UAAU,KAAK;IAC7B,QAAQ,OAAO;KACb,IAAI,CAAC,UAAU,MAAM,KAAK;IAC5B;IACA,eAAe;KACb,IAAI,UAAU;KACd,OAAO;MAAE,GAAG;MAAM,SAAS;KAAM;KACjC,QAAQ,QAAQ,QAAQ;IAC1B;GACF,CAAC;GACD,SAAS;GACT,gBAAgB;IACd,WAAW;IACX,SAAS;IACT,QAAQ;GACV,CAAC;GACD,UAAe,aAAa,CAAC,CAAC,MAAM,EAAE,aAAa;IACjD,IAAI,UAAU;IACd,KAAK,MAAM,SAAS,QAAQ,MAAM,KAAK;GACzC,CAAC;EACH;EACA,aAAa;GACX,OAAO,gBAAgB;GACvB,SAAS,MAAM,OAAO;EACxB;CACF;AACF"}
@@ -1,6 +1,9 @@
1
- import { ChatAttachment, ChatMessage, ChatQueuedMessage, ChatTokenUsage } from "./types.js";
1
+ import { ChatAttachment, ChatJob, ChatJobStatus, ChatMessage, ChatQueuedMessage, ChatTokenUsage } from "./types.js";
2
+ import { AskQuestionItem } from "../tools/ask-question/ask-question.js";
3
+ import "../tools/index.js";
2
4
  import { AiChatEmits, AiChatProps } from "../types/ai-chat.js";
3
5
  import "../types/index.js";
6
+ import { ChatPendingApproval } from "./fold.js";
4
7
  import { Ref } from "vue";
5
8
  //#region src/chat/use-chat.d.ts
6
9
  interface UseChatOptions {
@@ -10,7 +13,7 @@ interface UseChatOptions {
10
13
  /**
11
14
  * AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。
12
15
  * 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。
13
- * 始终注入内置工具(如 askQuestion),无需经 props 传入。
16
+ * 函数型 transport 注入内置工具;session transport 不注入、不执行客户端工具。
14
17
  */
15
18
  declare function useChat(options: UseChatOptions): {
16
19
  messages: Ref<ChatMessage[], ChatMessage[]>;
@@ -36,6 +39,17 @@ declare function useChat(options: UseChatOptions): {
36
39
  dataUrl: string;
37
40
  }[] | undefined;
38
41
  }[]>;
42
+ jobs: Ref<{
43
+ id: string;
44
+ kind: string;
45
+ label: string;
46
+ status: ChatJobStatus;
47
+ }[], ChatJob[] | {
48
+ id: string;
49
+ kind: string;
50
+ label: string;
51
+ status: ChatJobStatus;
52
+ }[]>;
39
53
  tokenUsage: Ref<{
40
54
  promptTokens: number;
41
55
  completionTokens: number;
@@ -66,11 +80,45 @@ declare function useChat(options: UseChatOptions): {
66
80
  cacheMissTokens?: number | undefined;
67
81
  cacheCreationTokens?: number | undefined;
68
82
  } | null>;
83
+ projections: Ref<Record<string, unknown>, Record<string, unknown>>;
84
+ title: Ref<string | null, string | null>;
85
+ pendingApprovals: Ref<{
86
+ approvalId: string;
87
+ toolName: string;
88
+ callId?: string | undefined;
89
+ reason?: string | undefined;
90
+ rpcId: string;
91
+ }[], ChatPendingApproval[] | {
92
+ approvalId: string;
93
+ toolName: string;
94
+ callId?: string | undefined;
95
+ reason?: string | undefined;
96
+ rpcId: string;
97
+ }[]>;
98
+ pendingQuestion: Ref<{
99
+ questions: {
100
+ question: string;
101
+ options?: string[] | undefined;
102
+ placeholder?: string | undefined;
103
+ }[];
104
+ rpcId: string;
105
+ } | null, {
106
+ questions: AskQuestionItem[];
107
+ rpcId: string;
108
+ } | {
109
+ questions: {
110
+ question: string;
111
+ options?: string[] | undefined;
112
+ placeholder?: string | undefined;
113
+ }[];
114
+ rpcId: string;
115
+ } | null>;
69
116
  send: (content: string, attachments?: ChatAttachment[]) => void;
70
117
  abort: () => void;
71
118
  regenerate: () => void;
72
119
  clear: () => void;
73
120
  respondToolCall: (toolCallId: string, approved: boolean) => void;
121
+ respondSession: (rpcId: string, ok: boolean, value?: unknown) => void;
74
122
  enqueue: (content: string, attachments?: ChatAttachment[], beforeId?: string) => ChatQueuedMessage;
75
123
  startQueued: (id: string) => void;
76
124
  removeQueued: (id: string) => ChatQueuedMessage | undefined;
@@ -1,47 +1,16 @@
1
- import { createBuiltinTools } from "../tools/index.js";
1
+ import { isServerTransport } from "./session.js";
2
+ import { addTokenUsage, resolveTools, serializeResult } from "./use-chat-helpers.js";
3
+ import { createSessionRuntime } from "./use-chat-session.js";
2
4
  import { useModel } from "@veltra/compositions";
3
5
  import { reactive, ref, watch } from "vue";
4
6
  //#region src/chat/use-chat.ts
5
7
  const uid = () => {
6
8
  return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
7
9
  };
8
- function serializeResult(result) {
9
- if (typeof result === "string") return result;
10
- try {
11
- return JSON.stringify(result) ?? "null";
12
- } catch {
13
- return String(result);
14
- }
15
- }
16
- /** 内置工具 + 用户工具,同名时内置优先 */
17
- function resolveTools(userTools) {
18
- const builtins = createBuiltinTools();
19
- const names = new Set(builtins.map((t) => t.name));
20
- return [...builtins, ...(userTools ?? []).filter((t) => !names.has(t.name))];
21
- }
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
- }
41
10
  /**
42
11
  * AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。
43
12
  * 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。
44
- * 始终注入内置工具(如 askQuestion),无需经 props 传入。
13
+ * 函数型 transport 注入内置工具;session transport 不注入、不执行客户端工具。
45
14
  */
46
15
  function useChat(options) {
47
16
  const { props, emit } = options;
@@ -72,6 +41,24 @@ function useChat(options) {
72
41
  const tokenUsage = ref(null);
73
42
  /** 最近一轮用户对话(含工具多轮请求)的 token;该轮无 usage 时为 null */
74
43
  const lastTurnUsage = ref(null);
44
+ /** session 下由 jobs/snapshot 整体替换;函数 transport 保持空数组 */
45
+ const jobs = ref([]);
46
+ const projections = ref({});
47
+ const title = ref(null);
48
+ const pendingApprovals = ref([]);
49
+ const pendingQuestion = ref(null);
50
+ const sessionRuntime = createSessionRuntime({
51
+ messages,
52
+ queue,
53
+ jobs,
54
+ tokenUsage,
55
+ projections,
56
+ title,
57
+ running,
58
+ pendingApprovals,
59
+ pendingQuestion
60
+ });
61
+ const session = () => isServerTransport(props.transport) ? props.transport : null;
75
62
  let abortController = null;
76
63
  /** needsConfirm 工具的挂起确认器,key 为 toolCallId */
77
64
  const confirmResolvers = /* @__PURE__ */ new Map();
@@ -98,6 +85,17 @@ function useChat(options) {
98
85
  }
99
86
  syncReasoningForModel(current);
100
87
  }, { immediate: true });
88
+ watch(() => props.transport, (transport, _prev, onCleanup) => {
89
+ if (!isServerTransport(transport)) return;
90
+ sessionRuntime.attach(transport, onCleanup);
91
+ }, { immediate: true });
92
+ watch(model, (modelId) => {
93
+ const current = session();
94
+ if (!sessionRuntime.opened || !current || !modelId) return;
95
+ const option = props.models?.find((m) => m.id === modelId);
96
+ if (!option) return;
97
+ current.selectModel(option.providerId, modelId);
98
+ });
101
99
  /**
102
100
  * 向父组件同步消息快照(流式 delta 过于频繁,不同步,只在关键节点同步)。
103
101
  * 必须经 setter 赋值触发 emit,而非直接 emit 切片:setter 让本地数组与下发的
@@ -114,6 +112,11 @@ function useChat(options) {
114
112
  */
115
113
  const send = (content, attachments) => {
116
114
  if (!content.trim() && !attachments?.length) return;
115
+ const current = session();
116
+ if (current) {
117
+ current.send(content, attachments);
118
+ return;
119
+ }
117
120
  enqueue(content, attachments);
118
121
  };
119
122
  /**
@@ -126,6 +129,7 @@ function useChat(options) {
126
129
  content,
127
130
  attachments
128
131
  };
132
+ if (session()) return item;
129
133
  const anchorIndex = beforeId ? queue.value.findIndex((q) => q.id === beforeId) : -1;
130
134
  if (anchorIndex === -1) queue.value.push(item);
131
135
  else queue.value.splice(anchorIndex, 0, item);
@@ -139,6 +143,7 @@ function useChat(options) {
139
143
  };
140
144
  /** 立即执行队列中的某条:中断当前会话,该条插队为下一条(其余保持原顺序) */
141
145
  const startQueued = (id) => {
146
+ if (session()) return;
142
147
  const item = removeQueued(id);
143
148
  if (!item) return;
144
149
  if (!running.value) {
@@ -151,6 +156,7 @@ function useChat(options) {
151
156
  };
152
157
  /** 从队列移除某条(返回被移除项;编辑场景由 UI 取回内容) */
153
158
  const removeQueued = (id) => {
159
+ if (session()) return void 0;
154
160
  const index = queue.value.findIndex((item) => item.id === id);
155
161
  if (index === -1) return void 0;
156
162
  const [item] = queue.value.splice(index, 1);
@@ -237,7 +243,8 @@ function useChat(options) {
237
243
  };
238
244
  /** 单轮生成;存在工具调用时执行后递归进入下一轮(递归避免 await-in-loop) */
239
245
  const runRound = async (controller, depth) => {
240
- if (!props.transport) return;
246
+ const transport = props.transport;
247
+ if (typeof transport !== "function") return;
241
248
  const { signal } = controller;
242
249
  const tools = resolveTools(props.tools);
243
250
  const assistant = reactive({
@@ -250,7 +257,7 @@ function useChat(options) {
250
257
  messages.value.push(assistant);
251
258
  let requestError = null;
252
259
  try {
253
- await props.transport({
260
+ await transport({
254
261
  messages: messages.value.slice(0, -1),
255
262
  systemPrompt: props.systemPrompt,
256
263
  tools,
@@ -318,15 +325,31 @@ function useChat(options) {
318
325
  };
319
326
  /** 中断当前生成,挂起的工具确认按拒绝处理 */
320
327
  const abort = () => {
328
+ const current = session();
329
+ if (current) {
330
+ current.cancel();
331
+ return;
332
+ }
321
333
  abortController?.abort();
322
334
  };
323
- /** 响应 needsConfirm 工具的用户确认 */
335
+ /** 响应 needsConfirm 工具的用户确认;session 下按 callId 找到 rpcId 后 respond */
324
336
  const respondToolCall = (toolCallId, approved) => {
337
+ const current = session();
338
+ if (current) {
339
+ const pending = pendingApprovals.value.find((a) => a.callId === toolCallId);
340
+ if (pending) current.respond(pending.rpcId, approved);
341
+ return;
342
+ }
325
343
  confirmResolvers.get(toolCallId)?.(approved);
326
344
  };
345
+ /** session 审批横幅 / 提问提交:直接 respond(rpcId) */
346
+ const respondSession = (rpcId, ok, value) => {
347
+ const current = session();
348
+ if (current) current.respond(rpcId, ok, value);
349
+ };
327
350
  /** 重新生成:移除最后一条用户消息之后的所有消息,重新跑对话循环 */
328
351
  const regenerate = () => {
329
- if (running.value) return;
352
+ if (session() || running.value) return;
330
353
  const list = messages.value;
331
354
  let lastUserIndex = -1;
332
355
  for (let i = list.length - 1; i >= 0; i--) if (list[i]?.role === "user") {
@@ -340,6 +363,11 @@ function useChat(options) {
340
363
  };
341
364
  /** 清空消息、待发送队列与 token 统计,生成中则先中断 */
342
365
  const clear = () => {
366
+ if (session()) {
367
+ sessionRuntime.resetLocal();
368
+ lastTurnUsage.value = null;
369
+ return;
370
+ }
343
371
  abort();
344
372
  queue.value = [];
345
373
  messages.value = [];
@@ -353,13 +381,19 @@ function useChat(options) {
353
381
  reasoningLevel,
354
382
  running,
355
383
  queue,
384
+ jobs,
356
385
  tokenUsage,
357
386
  lastTurnUsage,
387
+ projections,
388
+ title,
389
+ pendingApprovals,
390
+ pendingQuestion,
358
391
  send,
359
392
  abort,
360
393
  regenerate,
361
394
  clear,
362
395
  respondToolCall,
396
+ respondSession,
363
397
  enqueue,
364
398
  startQueued,
365
399
  removeQueued
@@ -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 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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"}
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 type { AskQuestionItem } from '../tools'\nimport type { AiChatEmits, AiChatProps } from '../types'\nimport type { ChatPendingApproval } from './fold'\nimport { isServerTransport, type ChatSessionTransport } from './session'\nimport type {\n ChatAttachment,\n ChatJob,\n ChatMessage,\n ChatQueuedMessage,\n ChatTokenUsage,\n ChatTool,\n ChatToolCall\n} from './types'\nimport { addTokenUsage, resolveTools, serializeResult } from './use-chat-helpers'\nimport { createSessionRuntime } from './use-chat-session'\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\n/**\n * AI 对话核心状态机:消息管理、流式追加、工具调用循环编排。\n * 与 UI 解耦,ai-chat.vue 只负责渲染与交互转发。\n * 函数型 transport 注入内置工具;session transport 不注入、不执行客户端工具。\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 /** session 下由 jobs/snapshot 整体替换;函数 transport 保持空数组 */\n const jobs = ref<ChatJob[]>([])\n const projections = ref<Record<string, unknown>>({})\n const title = ref<string | null>(null)\n const pendingApprovals = ref<ChatPendingApproval[]>([])\n const pendingQuestion = ref<{ questions: AskQuestionItem[]; rpcId: string } | null>(null)\n\n const sessionRuntime = createSessionRuntime({\n messages,\n queue,\n jobs,\n tokenUsage,\n projections,\n title,\n running,\n pendingApprovals,\n pendingQuestion\n })\n\n const session = (): ChatSessionTransport | null =>\n isServerTransport(props.transport) ? props.transport : 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 // session:open + fetchHistory 播种;更换或卸载时 disposer\n watch(\n () => props.transport,\n (transport, _prev, onCleanup) => {\n if (!isServerTransport(transport)) return\n sessionRuntime.attach(transport, onCleanup)\n },\n { immediate: true }\n )\n\n // session 且已 open 时,模型切换走 selectModel\n watch(model, (modelId) => {\n const current = session()\n if (!sessionRuntime.opened || !current || !modelId) return\n const option = props.models?.find((m) => m.id === modelId)\n if (!option) return\n void current.selectModel(option.providerId, modelId)\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 const current = session()\n if (current) {\n void current.send(content, attachments)\n return\n }\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 if (session()) return item\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 if (session()) return\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 if (session()) return 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 const transport = props.transport\n if (typeof transport !== 'function') return\n const { signal } = controller\n const tools = resolveTools(props.tools as ChatTool[] | undefined)\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 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 const current = session()\n if (current) {\n void current.cancel()\n return\n }\n abortController?.abort()\n }\n\n /** 响应 needsConfirm 工具的用户确认;session 下按 callId 找到 rpcId 后 respond */\n const respondToolCall = (toolCallId: string, approved: boolean) => {\n const current = session()\n if (current) {\n const pending = pendingApprovals.value.find((a) => a.callId === toolCallId)\n if (pending) void current.respond(pending.rpcId, approved)\n return\n }\n confirmResolvers.get(toolCallId)?.(approved)\n }\n\n /** session 审批横幅 / 提问提交:直接 respond(rpcId) */\n const respondSession = (rpcId: string, ok: boolean, value?: unknown) => {\n const current = session()\n if (current) void current.respond(rpcId, ok, value)\n }\n\n /** 重新生成:移除最后一条用户消息之后的所有消息,重新跑对话循环 */\n const regenerate = () => {\n if (session() || 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 if (session()) {\n sessionRuntime.resetLocal()\n lastTurnUsage.value = null\n return\n }\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 jobs,\n tokenUsage,\n lastTurnUsage,\n projections,\n title,\n pendingApprovals,\n pendingQuestion,\n send,\n abort,\n regenerate,\n clear,\n respondToolCall,\n respondSession,\n enqueue,\n startQueued,\n removeQueued\n }\n}\n"],"mappings":";;;;;;AAwBA,MAAM,YAAY;CAChB,OAAO,OAAO,WAAW,eAAe,OAAO,aAC3C,OAAO,WAAW,IAClB,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC;AACzD;;;;;;AAOA,SAAgB,QAAQ,SAAyB;CAC/C,MAAM,EAAE,OAAO,SAAS;CAExB,MAAM,WAAW,SAAS;EACxB;EACA,UAAU;EACV;EACA,OAAO;EACP,cAAc,CAAC;CACjB,CAAC;CAED,MAAM,QAAQ,SAAS;EAAE;EAAO,UAAU;EAAS;EAAM,OAAO;CAAK,CAAC;CAEtE,MAAM,iBAAiB,SAAS;EAAE;EAAO,UAAU;EAAkB;EAAM,OAAO;CAAK,CAAC;;CAKxF,MAAM,UAAU,IAAI,KAAK;;CAGzB,MAAM,QAAQ,IAAyB,CAAC,CAAC;;CAGzC,MAAM,aAAa,IAA2B,IAAI;;CAElD,MAAM,gBAAgB,IAA2B,IAAI;;CAGrD,MAAM,OAAO,IAAe,CAAC,CAAC;CAC9B,MAAM,cAAc,IAA6B,CAAC,CAAC;CACnD,MAAM,QAAQ,IAAmB,IAAI;CACrC,MAAM,mBAAmB,IAA2B,CAAC,CAAC;CACtD,MAAM,kBAAkB,IAA4D,IAAI;CAExF,MAAM,iBAAiB,qBAAqB;EAC1C;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CAED,MAAM,gBACJ,kBAAkB,MAAM,SAAS,IAAI,MAAM,YAAY;CAEzD,IAAI,kBAA0C;;CAE9C,MAAM,mCAAmB,IAAI,IAAyC;;CAEtE,IAAI,oBAAoB;;CAExB,IAAI,mBAAmB;;CAGvB,MAAM,yBAAyB,YAAgC;EAC7D,MAAM,SAAS,MAAM,QAAQ,MAAM,MAAM,EAAE,OAAO,OAAO;EACzD,IAAI,CAAC,QAAQ,iBAAiB,QAAQ;GACpC,IAAI,eAAe,UAAU,KAAA,GAC3B,eAAe,QAAQ,KAAA;GAEzB;EACF;EACA,MAAM,SAAS,OAAO;EACtB,IAAI,eAAe,SAAS,OAAO,MAAM,MAAM,EAAE,UAAU,eAAe,KAAK,GAAG;EAClF,eAAe,QAAQ,OAAO,yBAAyB,OAAO,EAAE,EAAE;CACpE;CAGA,YACQ,CAAC,MAAM,QAAQ,MAAM,KAAK,IAC/B,CAAC,QAAQ,aAAa;EACrB,IAAI,CAAC,QAAQ,QAAQ;EACrB,IAAI,CAAC,WAAW,CAAC,OAAO,MAAM,MAAM,EAAE,OAAO,OAAO,GAAG;GACrD,MAAM,QAAQ,OAAO,EAAE,CAAE;GACzB;EACF;EACA,sBAAsB,OAAO;CAC/B,GACA,EAAE,WAAW,KAAK,CACpB;CAGA,YACQ,MAAM,YACX,WAAW,OAAO,cAAc;EAC/B,IAAI,CAAC,kBAAkB,SAAS,GAAG;EACnC,eAAe,OAAO,WAAW,SAAS;CAC5C,GACA,EAAE,WAAW,KAAK,CACpB;CAGA,MAAM,QAAQ,YAAY;EACxB,MAAM,UAAU,QAAQ;EACxB,IAAI,CAAC,eAAe,UAAU,CAAC,WAAW,CAAC,SAAS;EACpD,MAAM,SAAS,MAAM,QAAQ,MAAM,MAAM,EAAE,OAAO,OAAO;EACzD,IAAI,CAAC,QAAQ;EACb,QAAa,YAAY,OAAO,YAAY,OAAO;CACrD,CAAC;;;;;;;;CASD,MAAM,iBAAiB;EACrB,SAAS,QAAQ,SAAS,MAAM,MAAM;CACxC;;;;;CAMA,MAAM,QAAQ,SAAiB,gBAAmC;EAChE,IAAI,CAAC,QAAQ,KAAK,KAAK,CAAC,aAAa,QAAQ;EAC7C,MAAM,UAAU,QAAQ;EACxB,IAAI,SAAS;GACX,QAAa,KAAK,SAAS,WAAW;GACtC;EACF;EACA,QAAQ,SAAS,WAAW;CAC9B;;;;;CAMA,MAAM,WACJ,SACA,aACA,aACsB;EACtB,MAAM,OAA0B;GAAE,IAAI,IAAI;GAAG;GAAS;EAAY;EAClE,IAAI,QAAQ,GAAG,OAAO;EACtB,MAAM,cAAc,WAAW,MAAM,MAAM,WAAW,MAAM,EAAE,OAAO,QAAQ,IAAI;EACjF,IAAI,gBAAgB,IAClB,MAAM,MAAM,KAAK,IAAI;OAErB,MAAM,MAAM,OAAO,aAAa,GAAG,IAAI;EAEzC,IAAI,CAAC,QAAQ,OAAO,WAAW;EAC/B,OAAO;CACT;;CAGA,MAAM,mBAAmB;EACvB,MAAM,OAAO,MAAM,MAAM,MAAM;EAC/B,IAAI,MAAM,cAAc,KAAK,SAAS,KAAK,WAAW;CACxD;;CAGA,MAAM,eAAe,OAAe;EAClC,IAAI,QAAQ,GAAG;EACf,MAAM,OAAO,aAAa,EAAE;EAC5B,IAAI,CAAC,MAAM;EAEX,IAAI,CAAC,QAAQ,OAAO;GAClB,cAAc,KAAK,SAAS,KAAK,WAAW;GAC5C;EACF;EAGA,MAAM,MAAM,QAAQ,IAAI;EACxB,mBAAmB;EACnB,MAAM;CACR;;CAGA,MAAM,gBAAgB,OAA8C;EAClE,IAAI,QAAQ,GAAG,OAAO,KAAA;EACtB,MAAM,QAAQ,MAAM,MAAM,WAAW,SAAS,KAAK,OAAO,EAAE;EAC5D,IAAI,UAAU,IAAI,OAAO,KAAA;EACzB,MAAM,CAAC,QAAQ,MAAM,MAAM,OAAO,OAAO,CAAC;EAC1C,OAAO;CACT;;CAGA,MAAM,iBAAiB,SAAiB,gBAAmC;EACzE,MAAM,UAAU,SAAsB;GAAE,IAAI,IAAI;GAAG,MAAM;GAAQ;GAAS;EAAY,CAAC;EACvF,SAAS,MAAM,KAAK,OAAO;EAC3B,SAAS;EACT,KAAK,QAAQ,OAAO;EAEpB,gBAAqB;CACvB;;CAGA,MAAM,kBAAkB,OACtB,UACA,QACA,UACG;EACH,MAAM,qBAAqB,YAAoB;GAC7C,SAAS,MAAM,KACb,SAAsB;IAAE,IAAI,IAAI;IAAG,MAAM;IAAQ,YAAY,SAAS;IAAI;GAAQ,CAAC,CACrF;EACF;EAEA,MAAM,OAAO,MAAM,MAAM,MAAM,EAAE,SAAS,SAAS,IAAI;EAEvD,IAAI,CAAC,MAAM;GACT,SAAS,SAAS;GAClB,SAAS,QAAQ,UAAU,SAAS,KAAK;GACzC,kBAAkB,gBAAgB,SAAS,KAAK,oBAAoB;GACpE;EACF;EAEA,IAAI,KAAK,cAAc;GACrB,SAAS,SAAS;GAClB,MAAM,WAAW,MAAM,IAAI,SAAkB,YAAY;IACvD,iBAAiB,IAAI,SAAS,IAAI,OAAO;IACzC,OAAO,iBAAiB,eAAe,QAAQ,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;GACvE,CAAC;GACD,iBAAiB,OAAO,SAAS,EAAE;GAEnC,IAAI,CAAC,UAAU;IACb,SAAS,SAAS;IAClB,kBAAkB,4CAA4C,SAAS,KAAK,GAAG;IAC/E;GACF;EACF;EAEA,SAAS,SAAS;EAClB,IAAI;GACF,MAAM,OAAO,SAAS,YAAY,KAAK,MAAM,SAAS,SAAS,IAAI,CAAC;GACpE,MAAM,SAAS,MAAM,KAAK,QAAQ,MAAM;IAAE;IAAU;GAAO,CAAC;GAC5D,SAAS,SAAS;GAClB,SAAS,SAAS,gBAAgB,MAAM;GACxC,kBAAkB,SAAS,MAAM;EACnC,SAAS,OAAO;GACd,SAAS,SAAS;GAClB,SAAS,QAAQ,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;GACtE,kBAAkB,UAAU,SAAS,OAAO;EAC9C;CACF;;CAGA,MAAM,kBAAkB,YAAY;EAClC,IAAI,CAAC,MAAM,WAAW;EAEtB,QAAQ,QAAQ;EAChB,oBAAoB;EACpB,cAAc,QAAQ;EACtB,MAAM,aAAa,IAAI,gBAAgB;EACvC,kBAAkB;EAElB,IAAI;GACF,MAAM,SAAS,YAAY,CAAC;EAC9B,UAAU;GACR,QAAQ,QAAQ;GAChB,kBAAkB;GAClB,SAAS;GAIT,MAAM,eAAe,qBAAqB;GAC1C,oBAAoB;GACpB,mBAAmB;GACnB,IAAI,cAAc,WAAW;EAC/B;CACF;;CAGA,MAAM,WAAW,OAAO,YAA6B,UAAiC;EACpF,MAAM,YAAY,MAAM;EACxB,IAAI,OAAO,cAAc,YAAY;EACrC,MAAM,EAAE,WAAW;EACnB,MAAM,QAAQ,aAAa,MAAM,KAA+B;EAEhE,MAAM,YAAY,SAAsB;GACtC,IAAI,IAAI;GACR,MAAM;GACN,SAAS;GACT,QAAQ;GACR,WAAW,CAAC;EACd,CAAC;EACD,SAAS,MAAM,KAAK,SAAS;EAE7B,IAAI,eAA6B;EAEjC,IAAI;GACF,MAAM,UACJ;IAEE,UAAU,SAAS,MAAM,MAAM,GAAG,EAAE;IACpC,cAAc,MAAM;IACpB;IACA,OAAO,MAAM;IACb,gBAAgB,eAAe;IAC/B;GACF,GACA;IACE,cAAc,UAAU;KACtB,UAAU,WAAW;IACvB;IACA,mBAAmB,UAAU;KAC3B,UAAU,aAAa,UAAU,aAAa,MAAM;IACtD;IACA,aAAa,SAAS;KACpB,MAAM,WAAW,SAAuB;MAAE,GAAG;MAAM,QAAQ;KAAU,CAAC;KACtE,UAAU,UAAW,KAAK,QAAQ;KAClC,KAAK,aAAa,QAAQ;IAC5B;IACA,UAAU,UAAU;KAClB,cAAc,QAAQ,cAAc,QAChC,cAAc,cAAc,OAAO,KAAK,IACxC;KACJ,WAAW,QAAQ,WAAW,QAAQ,cAAc,WAAW,OAAO,KAAK,IAAI;IACjF;IACA,UAAU,UAAU;KAClB,eAAe;IACjB;GACF,CACF;EACF,SAAS,OAAO;GACd,eAAe,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;EACzE;EAEA,IAAI,OAAO,SAAS;GAClB,UAAU,SAAS;GACnB;EACF;EAEA,IAAI,cAAc;GAChB,UAAU,SAAS;GACnB,KAAK,SAAS,YAAY;GAC1B;EACF;EAEA,UAAU,SAAS;EAEnB,MAAM,YAAY,UAAU,aAAa,CAAC;EAC1C,IAAI,CAAC,UAAU,QAAQ;GACrB,iBAAiB,SAAS;GAC1B;EACF;EAGA,MAAM,UAAU,QACb,MAAM,aACL,KAAK,WAAY,OAAO,UAAU,KAAA,IAAY,gBAAgB,UAAU,QAAQ,KAAK,CAAE,GACzF,QAAQ,QAAQ,CAClB;EACA,SAAS;EAET,IAAI,OAAO,SAAS;EAMpB,IAHoB,UAAU,MAC3B,SAAS,KAAK,WAAW,aAAa,MAAM,MAAM,MAAM,EAAE,SAAS,KAAK,QAAQ,EAAE,QAAQ,CAE/E,GAAG;GACf,iBAAiB,SAAS;GAC1B;EACF;EAGA,IAAI,QAAQ,MAAM,MAAM,iBAAiB,KAAK;GAC5C,iBAAiB,SAAS;GAC1B;EACF;EAEA,OAAO,SAAS,YAAY,QAAQ,CAAC;CACvC;;CAGA,MAAM,oBAAoB,cAA2B;EACnD,oBAAoB;EACpB,KAAK,UAAU,SAAS;CAC1B;;CAGA,MAAM,cAAc;EAClB,MAAM,UAAU,QAAQ;EACxB,IAAI,SAAS;GACX,QAAa,OAAO;GACpB;EACF;EACA,iBAAiB,MAAM;CACzB;;CAGA,MAAM,mBAAmB,YAAoB,aAAsB;EACjE,MAAM,UAAU,QAAQ;EACxB,IAAI,SAAS;GACX,MAAM,UAAU,iBAAiB,MAAM,MAAM,MAAM,EAAE,WAAW,UAAU;GAC1E,IAAI,SAAS,QAAa,QAAQ,QAAQ,OAAO,QAAQ;GACzD;EACF;EACA,iBAAiB,IAAI,UAAU,CAAC,GAAG,QAAQ;CAC7C;;CAGA,MAAM,kBAAkB,OAAe,IAAa,UAAoB;EACtE,MAAM,UAAU,QAAQ;EACxB,IAAI,SAAS,QAAa,QAAQ,OAAO,IAAI,KAAK;CACpD;;CAGA,MAAM,mBAAmB;EACvB,IAAI,QAAQ,KAAK,QAAQ,OAAO;EAEhC,MAAM,OAAO,SAAS;EACtB,IAAI,gBAAgB;EACpB,KAAK,IAAI,IAAI,KAAK,SAAS,GAAG,KAAK,GAAG,KACpC,IAAI,KAAK,EAAE,EAAE,SAAS,QAAQ;GAC5B,gBAAgB;GAChB;EACF;EAEF,IAAI,kBAAkB,MAAM,kBAAkB,KAAK,SAAS,GAAG;EAE/D,SAAS,QAAQ,KAAK,MAAM,GAAG,gBAAgB,CAAC;EAChD,SAAS;EACT,gBAAqB;CACvB;;CAGA,MAAM,cAAc;EAClB,IAAI,QAAQ,GAAG;GACb,eAAe,WAAW;GAC1B,cAAc,QAAQ;GACtB;EACF;EACA,MAAM;EACN,MAAM,QAAQ,CAAC;EACf,SAAS,QAAQ,CAAC;EAClB,WAAW,QAAQ;EACnB,cAAc,QAAQ;EACtB,SAAS;CACX;CAEA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
@@ -1,12 +1,16 @@
1
+ import { isServerTransport } from "../../chat/session.js";
1
2
  import { AiChatDIKey } from "./di.js";
3
+ import _sfc_main$1 from "./ask-question.js";
2
4
  import { createBuiltinTools } from "../../tools/index.js";
3
5
  import { useChat } from "../../chat/use-chat.js";
4
- import _sfc_main$1 from "./chat-input.js";
5
- import _sfc_main$2 from "./message-list.js";
6
- import _sfc_main$3 from "./queue-list.js";
7
- import _sfc_main$4 from "./side-panel.js";
6
+ import _sfc_main$2 from "./approval-banner.js";
7
+ import _sfc_main$3 from "./chat-input.js";
8
+ import _sfc_main$4 from "./job-bar.js";
9
+ import _sfc_main$5 from "./message-list.js";
10
+ import _sfc_main$6 from "./queue-list.js";
11
+ import _sfc_main$7 from "./side-panel.js";
8
12
  import { useResizeObserver } from "@veltra/compositions";
9
- import { computed, createBlock, createCommentVNode, createElementVNode, createSlots, createVNode, defineComponent, isRef, normalizeClass, openBlock, provide, ref, renderSlot, unref, useSlots, useTemplateRef, watch, withCtx } from "vue";
13
+ import { computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createVNode, defineComponent, isRef, normalizeClass, openBlock, provide, ref, renderSlot, unref, useSlots, useTemplateRef, watch, withCtx } from "vue";
10
14
  import { ULayout } from "@veltra/desktop";
11
15
  import { bem } from "@veltra/utils";
12
16
  //#region src/components/ai-chat/ai-chat.vue
@@ -18,7 +22,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
18
22
  name: "UAiChat",
19
23
  __name: "ai-chat",
20
24
  props: {
21
- transport: { type: Function },
25
+ transport: { type: [Function, Object] },
22
26
  tools: {},
23
27
  systemPrompt: {},
24
28
  maxToolRounds: {},
@@ -31,7 +35,9 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
31
35
  accept: {},
32
36
  maxAttachmentSize: {},
33
37
  rendererProps: {},
34
- tokenUsageDetail: { type: Boolean }
38
+ tokenUsageDetail: { type: Boolean },
39
+ toolIcons: {},
40
+ readonly: { type: Boolean }
35
41
  },
36
42
  emits: [
37
43
  "update:messages",
@@ -47,14 +53,19 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
47
53
  const emit = __emit;
48
54
  const cls = bem("ai-chat");
49
55
  const slots = useSlots();
50
- /** 内置 + 用户工具(同名内置优先),供工具卡片解析 icon/label/render/renderTo/autoCollapse */
56
+ /**
57
+ * 按 name 查表:session 下 tools 仅为渲染元信息(不注入内置工具,忽略 execute 等)。
58
+ * 函数 transport 仍注入 createBuiltinTools(同名内置优先)。
59
+ */
51
60
  const toolMap = computed(() => {
61
+ if (isServerTransport(props.transport)) return Object.fromEntries((props.tools ?? []).map((t) => [t.name, t]));
52
62
  const builtins = createBuiltinTools();
53
63
  const names = new Set(builtins.map((t) => t.name));
54
64
  const tools = [...builtins, ...(props.tools ?? []).filter((t) => !names.has(t.name))];
55
65
  return Object.fromEntries(tools.map((t) => [t.name, t]));
56
66
  });
57
- const { messages, model, reasoningLevel, running, queue, tokenUsage, lastTurnUsage, send, abort, regenerate, clear, respondToolCall, enqueue, startQueued, removeQueued } = useChat({
67
+ const toolIcons = computed(() => props.toolIcons);
68
+ const { messages, model, reasoningLevel, running, queue, jobs, tokenUsage, lastTurnUsage, pendingApprovals, pendingQuestion, send, abort, regenerate, clear, respondToolCall, respondSession, enqueue, startQueued, removeQueued } = useChat({
58
69
  props,
59
70
  emit
60
71
  });
@@ -96,6 +107,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
96
107
  cls,
97
108
  slots,
98
109
  tools: toolMap,
110
+ toolIcons,
99
111
  openPanel
100
112
  });
101
113
  /**
@@ -161,6 +173,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
161
173
  editingSuccessor.value = void 0;
162
174
  };
163
175
  const handleSend = (content, attachments) => {
176
+ if (props.readonly) return;
164
177
  if (editingSuccessor.value !== void 0) {
165
178
  enqueue(content, attachments, editingSuccessor.value ?? void 0);
166
179
  editingSuccessor.value = void 0;
@@ -168,6 +181,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
168
181
  }
169
182
  send(content, attachments);
170
183
  };
184
+ const handleQuestionSubmit = (value) => {
185
+ const pending = pendingQuestion.value;
186
+ if (!pending) return;
187
+ respondSession(pending.rpcId, true, Array.isArray(value) ? value : value.answers);
188
+ };
171
189
  __expose({
172
190
  send,
173
191
  abort,
@@ -194,10 +212,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
194
212
  }, {
195
213
  default: withCtx(() => [
196
214
  createElementVNode("div", { class: normalizeClass([unref(cls).e("main"), unref(bem).is("empty", isEmpty.value)]) }, [
197
- createVNode(_sfc_main$2, {
215
+ createVNode(_sfc_main$5, {
198
216
  messages: unref(messages),
199
217
  welcome: __props.welcome,
200
218
  running: unref(running),
219
+ readonly: __props.readonly,
201
220
  "renderer-props": __props.rendererProps,
202
221
  onRespond: unref(respondToolCall),
203
222
  onRegenerate: unref(regenerate),
@@ -210,21 +229,37 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
210
229
  "messages",
211
230
  "welcome",
212
231
  "running",
232
+ "readonly",
213
233
  "renderer-props",
214
234
  "onRespond",
215
235
  "onRegenerate"
216
236
  ]),
217
- createVNode(_sfc_main$3, {
237
+ unref(pendingQuestion) ? (openBlock(), createElementBlock("div", {
238
+ key: 0,
239
+ class: normalizeClass(unref(cls).e("session-question"))
240
+ }, [createVNode(_sfc_main$1, {
241
+ questions: unref(pendingQuestion).questions,
242
+ onSubmit: handleQuestionSubmit
243
+ }, null, 8, ["questions"])], 2)) : createCommentVNode("v-if", true),
244
+ createVNode(_sfc_main$2, {
245
+ approvals: unref(pendingApprovals),
246
+ onRespond: unref(respondSession)
247
+ }, null, 8, ["approvals", "onRespond"]),
248
+ createVNode(_sfc_main$6, {
218
249
  queue: unref(queue),
250
+ readonly: __props.readonly,
219
251
  onStartNow: unref(startQueued),
220
252
  onEdit: handleQueueEdit,
221
253
  onRemove: unref(removeQueued)
222
254
  }, null, 8, [
223
255
  "queue",
256
+ "readonly",
224
257
  "onStartNow",
225
258
  "onRemove"
226
259
  ]),
227
- createVNode(_sfc_main$1, {
260
+ createVNode(_sfc_main$4, { jobs: unref(jobs) }, null, 8, ["jobs"]),
261
+ !__props.readonly ? (openBlock(), createBlock(_sfc_main$3, {
262
+ key: 1,
228
263
  ref_key: "chatInputRef",
229
264
  ref: chatInputRef,
230
265
  model: unref(model),
@@ -255,10 +290,10 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
255
290
  "token-usage-detail",
256
291
  "onAbort",
257
292
  "onClear"
258
- ])
293
+ ])) : createCommentVNode("v-if", true)
259
294
  ], 2),
260
295
  createCommentVNode(" 侧边面板(renderTo: 'panel' 的工具渲染区),与会话区宽度经 ULayout 拖拽手柄调节 "),
261
- activePanelCall.value ? (openBlock(), createBlock(_sfc_main$4, {
296
+ activePanelCall.value ? (openBlock(), createBlock(_sfc_main$7, {
262
297
  key: 0,
263
298
  ref_key: "panelRef",
264
299
  ref: panelRef,