@veltra/ai 2.1.5 → 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.
- 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/chat-input.js +9 -3
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/di.js.map +1 -1
- package/dist/components/ai-chat/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 +150 -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/chat-input.vue +10 -2
- 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 +162 -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
|
@@ -161,7 +161,7 @@ $root-name: ai-chat;
|
|
|
161
161
|
@include m.e(panel-fallback) {
|
|
162
162
|
margin: 0;
|
|
163
163
|
padding: fn.use-var(gap, default);
|
|
164
|
-
font-family:
|
|
164
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
165
165
|
font-size: fn.use-var(font-size-main, small);
|
|
166
166
|
line-height: 1.5;
|
|
167
167
|
white-space: pre-wrap;
|
|
@@ -265,6 +265,16 @@ $root-name: ai-chat;
|
|
|
265
265
|
color: fn.use-var(color, primary);
|
|
266
266
|
background-color: fn.use-var(color, primary, 'light-9');
|
|
267
267
|
}
|
|
268
|
+
|
|
269
|
+
&:disabled {
|
|
270
|
+
cursor: default;
|
|
271
|
+
color: fn.use-var(text-color, second);
|
|
272
|
+
|
|
273
|
+
&:hover {
|
|
274
|
+
color: fn.use-var(text-color, second);
|
|
275
|
+
background-color: fn.use-var(bg-color, top);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
268
278
|
}
|
|
269
279
|
|
|
270
280
|
// 欢迎语逐条轮换的切换过渡
|
|
@@ -621,19 +631,31 @@ $root-name: ai-chat;
|
|
|
621
631
|
|
|
622
632
|
@include m.e(tool-call-section) {
|
|
623
633
|
color: fn.use-var(text-color, assist);
|
|
624
|
-
|
|
634
|
+
}
|
|
625
635
|
|
|
636
|
+
@include m.e(tool-call-json) {
|
|
626
637
|
&:not(:first-child) {
|
|
627
638
|
margin-top: fn.use-var(gap, small);
|
|
628
639
|
}
|
|
629
640
|
}
|
|
630
641
|
|
|
642
|
+
@include m.e(tool-call-json-head) {
|
|
643
|
+
@include m.flex(flex, flex-start, center);
|
|
644
|
+
gap: fn.use-var(gap, small);
|
|
645
|
+
margin-bottom: 4px;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
@include m.e(tool-call-json-toggle) {
|
|
649
|
+
flex-shrink: 0;
|
|
650
|
+
margin-left: auto;
|
|
651
|
+
}
|
|
652
|
+
|
|
631
653
|
@include m.e(tool-call-code) {
|
|
632
654
|
margin: 0;
|
|
633
655
|
padding: 4px fn.use-var(gap, small);
|
|
634
656
|
border-radius: fn.use-var(radius, small);
|
|
635
657
|
background-color: fn.use-var(bg-color, bottom);
|
|
636
|
-
font-family:
|
|
658
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
637
659
|
font-size: fn.use-var(font-size-main, small);
|
|
638
660
|
line-height: 1.5;
|
|
639
661
|
white-space: pre-wrap;
|
|
@@ -866,6 +888,136 @@ $root-name: ai-chat;
|
|
|
866
888
|
}
|
|
867
889
|
}
|
|
868
890
|
|
|
891
|
+
// 作业条:有 jobs 时出现在输入区上方
|
|
892
|
+
@include m.e(job-bar) {
|
|
893
|
+
flex-shrink: 0;
|
|
894
|
+
width: 100%;
|
|
895
|
+
max-width: 800px;
|
|
896
|
+
box-sizing: border-box;
|
|
897
|
+
margin-bottom: 4px;
|
|
898
|
+
border-radius: fn.use-var(radius, default);
|
|
899
|
+
background-color: fn.use-var(bg-color, middle);
|
|
900
|
+
padding: 4px;
|
|
901
|
+
font-size: fn.use-var(font-size-main, small);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
@include m.e(job-bar-head) {
|
|
905
|
+
@include m.flex(flex, space-between, center);
|
|
906
|
+
width: 100%;
|
|
907
|
+
border: none;
|
|
908
|
+
background: none;
|
|
909
|
+
padding: 2px 6px;
|
|
910
|
+
color: fn.use-var(text-color, assist);
|
|
911
|
+
font-size: inherit;
|
|
912
|
+
font-family: inherit;
|
|
913
|
+
cursor: pointer;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
@include m.e(job-bar-chevron) {
|
|
917
|
+
font-size: 12px;
|
|
918
|
+
transition: transform 0.2s;
|
|
919
|
+
|
|
920
|
+
@include m.is(expanded) {
|
|
921
|
+
transform: rotate(90deg);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
@include m.e(job-bar-list) {
|
|
926
|
+
display: flex;
|
|
927
|
+
flex-direction: column;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
@include m.e(job-bar-item) {
|
|
931
|
+
@include m.flex(flex, flex-start, center);
|
|
932
|
+
gap: 6px;
|
|
933
|
+
padding: 3px 6px;
|
|
934
|
+
border-radius: fn.use-var(radius, small);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
@include m.e(job-bar-icon) {
|
|
938
|
+
flex-shrink: 0;
|
|
939
|
+
font-size: 13px;
|
|
940
|
+
color: fn.use-var(text-color, second);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
@include m.e(job-bar-label) {
|
|
944
|
+
flex: 1;
|
|
945
|
+
min-width: 0;
|
|
946
|
+
@include m.ellipsis;
|
|
947
|
+
color: fn.use-var(text-color, main);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
@include m.e(job-bar-dot) {
|
|
951
|
+
flex-shrink: 0;
|
|
952
|
+
width: 6px;
|
|
953
|
+
height: 6px;
|
|
954
|
+
border-radius: 50%;
|
|
955
|
+
background-color: fn.use-var(text-color, placeholder);
|
|
956
|
+
|
|
957
|
+
@include m.is(running) {
|
|
958
|
+
background-color: fn.use-var(color, primary);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
@include m.is(done) {
|
|
962
|
+
background-color: fn.use-var(color, success);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
@include m.is(error) {
|
|
966
|
+
background-color: fn.use-var(color, danger);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// session 提问:与输入区同宽卡片
|
|
971
|
+
@include m.e(session-question) {
|
|
972
|
+
flex-shrink: 0;
|
|
973
|
+
width: 100%;
|
|
974
|
+
max-width: 800px;
|
|
975
|
+
box-sizing: border-box;
|
|
976
|
+
margin-bottom: 4px;
|
|
977
|
+
border-radius: fn.use-var(radius, default);
|
|
978
|
+
background-color: fn.use-var(bg-color, top);
|
|
979
|
+
box-shadow: fn.use-var(shadow, sm);
|
|
980
|
+
padding: fn.use-var(gap, small);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// 无 callId 的审批横幅(浮动确认卡)
|
|
984
|
+
@include m.e(approval-banner) {
|
|
985
|
+
flex-shrink: 0;
|
|
986
|
+
width: 100%;
|
|
987
|
+
max-width: 800px;
|
|
988
|
+
box-sizing: border-box;
|
|
989
|
+
margin-bottom: 4px;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
@include m.e(approval-banner-card) {
|
|
993
|
+
padding: fn.use-var(gap, small);
|
|
994
|
+
border-radius: fn.use-var(radius, default);
|
|
995
|
+
background-color: fn.use-var(bg-color, top);
|
|
996
|
+
box-shadow: fn.use-var(shadow, sm);
|
|
997
|
+
|
|
998
|
+
& + & {
|
|
999
|
+
margin-top: 4px;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
@include m.e(approval-banner-title) {
|
|
1004
|
+
color: fn.use-var(text-color, title);
|
|
1005
|
+
font-weight: 500;
|
|
1006
|
+
font-size: fn.use-var(font-size-main, small);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
@include m.e(approval-banner-reason) {
|
|
1010
|
+
margin-top: 4px;
|
|
1011
|
+
color: fn.use-var(text-color, assist);
|
|
1012
|
+
font-size: fn.use-var(font-size-main, small);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
@include m.e(approval-banner-actions) {
|
|
1016
|
+
@include m.flex(flex, flex-end, center);
|
|
1017
|
+
gap: fn.use-var(gap, small);
|
|
1018
|
+
margin-top: fn.use-var(gap, small);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
869
1021
|
// 输入区:无边框卡片,靠浅阴影区分层级;聚焦不换蓝描边,避免变成另一种框
|
|
870
1022
|
@include m.e(input-area) {
|
|
871
1023
|
flex-shrink: 0;
|
|
@@ -890,9 +1042,13 @@ $root-name: ai-chat;
|
|
|
890
1042
|
font-family: inherit;
|
|
891
1043
|
line-height: 1.6;
|
|
892
1044
|
box-sizing: border-box;
|
|
1045
|
+
overflow-y: hidden;
|
|
893
1046
|
|
|
894
1047
|
&::placeholder {
|
|
895
1048
|
color: fn.use-var(text-color, placeholder);
|
|
1049
|
+
white-space: nowrap;
|
|
1050
|
+
overflow: hidden;
|
|
1051
|
+
text-overflow: ellipsis;
|
|
896
1052
|
}
|
|
897
1053
|
}
|
|
898
1054
|
|
|
@@ -930,7 +1086,7 @@ $root-name: ai-chat;
|
|
|
930
1086
|
transition: background-color 0.15s ease;
|
|
931
1087
|
|
|
932
1088
|
&:hover {
|
|
933
|
-
background-color: var(
|
|
1089
|
+
background-color: fn.use-var(bg-color, hover);
|
|
934
1090
|
}
|
|
935
1091
|
}
|
|
936
1092
|
|
|
@@ -1047,7 +1203,7 @@ $root-name: ai-chat;
|
|
|
1047
1203
|
transition: background-color 0.15s ease;
|
|
1048
1204
|
|
|
1049
1205
|
&:hover {
|
|
1050
|
-
background-color: var(
|
|
1206
|
+
background-color: fn.use-var(bg-color, hover);
|
|
1051
1207
|
}
|
|
1052
1208
|
}
|
|
1053
1209
|
|
|
@@ -1144,7 +1300,7 @@ $root-name: ai-chat;
|
|
|
1144
1300
|
color 0.15s ease;
|
|
1145
1301
|
|
|
1146
1302
|
&:hover {
|
|
1147
|
-
background-color: var(
|
|
1303
|
+
background-color: fn.use-var(bg-color, hover);
|
|
1148
1304
|
}
|
|
1149
1305
|
|
|
1150
1306
|
@include m.is(active) {
|
|
@@ -33,33 +33,26 @@
|
|
|
33
33
|
查看面板
|
|
34
34
|
</UButton>
|
|
35
35
|
</div>
|
|
36
|
-
<!--
|
|
36
|
+
<!-- 自定义渲染:meta.render / tool-<name> 插槽优先于通用工具视图 -->
|
|
37
37
|
<component :is="tool.render" v-else-if="tool?.render" :tool-call="toolCall" />
|
|
38
38
|
<component :is="slotBody" v-else-if="slotBody && hasResult" />
|
|
39
39
|
<template v-else>
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
</template>
|
|
44
|
-
|
|
45
|
-
<template v-if="hasResult">
|
|
46
|
-
<div :class="cls.e('tool-call-section')">
|
|
47
|
-
{{ toolCall.status === 'error' ? '错误' : '结果' }}
|
|
48
|
-
</div>
|
|
49
|
-
<pre :class="cls.e('tool-call-code')">{{ displayResult }}</pre>
|
|
50
|
-
</template>
|
|
40
|
+
<ToolJson v-if="prettyArguments" title="参数" :text="prettyArguments" />
|
|
41
|
+
<ToolJson v-if="hasResult" :title="resultTitle" :text="displayResult" />
|
|
42
|
+
<ToolJson v-if="prettyView" title="视图" :text="prettyView" />
|
|
51
43
|
</template>
|
|
52
44
|
</UCollapseItem>
|
|
53
45
|
</template>
|
|
54
46
|
|
|
55
47
|
<script lang="ts" setup>
|
|
56
48
|
import { UButton, UCollapseItem, UIcon } from '@veltra/desktop'
|
|
57
|
-
import { CircleCheckFilled, CircleClose, Close, Loading, WarningFilled } from '@veltra/icons/normal'
|
|
58
49
|
import { bem } from '@veltra/utils'
|
|
59
|
-
import { computed, inject, ref, watch
|
|
50
|
+
import { computed, inject, ref, watch } from 'vue'
|
|
60
51
|
|
|
61
52
|
import type { ChatToolCall } from '../../chat/types'
|
|
62
53
|
import { AiChatDIKey } from './di'
|
|
54
|
+
import { resolveToolIcon } from './tool-icons'
|
|
55
|
+
import ToolJson from './tool-json.vue'
|
|
63
56
|
|
|
64
57
|
defineOptions({ name: 'UAiChatToolCall' })
|
|
65
58
|
|
|
@@ -70,7 +63,24 @@ const emit = defineEmits<{ (e: 'respond', approved: boolean): void }>()
|
|
|
70
63
|
const di = inject(AiChatDIKey)
|
|
71
64
|
const cls = di?.cls ?? bem('ai-chat')
|
|
72
65
|
|
|
73
|
-
/**
|
|
66
|
+
/** 把未知值格式成可读 JSON,循环引用等异常时退回 String,不得 throw */
|
|
67
|
+
function formatUnknown(value: unknown): string {
|
|
68
|
+
if (value == null) return ''
|
|
69
|
+
if (typeof value === 'string') {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.stringify(JSON.parse(value), null, 2)
|
|
72
|
+
} catch {
|
|
73
|
+
return value
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
return JSON.stringify(value, null, 2) ?? String(value)
|
|
78
|
+
} catch {
|
|
79
|
+
return String(value)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 当前调用对应的工具定义或渲染元信息(session 下忽略 execute / needsConfirm / terminal) */
|
|
74
84
|
const tool = computed(() => di?.tools.value[props.toolCall.name])
|
|
75
85
|
|
|
76
86
|
const isExpanded = ref(false)
|
|
@@ -94,7 +104,7 @@ watch(
|
|
|
94
104
|
() => props.toolCall.status,
|
|
95
105
|
(status) => {
|
|
96
106
|
if (userToggled) return
|
|
97
|
-
if (status === 'running' || status === 'awaiting-confirm') {
|
|
107
|
+
if (status === 'pending' || status === 'running' || status === 'awaiting-confirm') {
|
|
98
108
|
isExpanded.value = true
|
|
99
109
|
} else {
|
|
100
110
|
isExpanded.value = !autoCollapse.value
|
|
@@ -103,17 +113,16 @@ watch(
|
|
|
103
113
|
{ immediate: true }
|
|
104
114
|
)
|
|
105
115
|
|
|
106
|
-
const
|
|
107
|
-
pending:
|
|
108
|
-
running:
|
|
109
|
-
'awaiting-confirm':
|
|
110
|
-
success:
|
|
111
|
-
error:
|
|
112
|
-
rejected:
|
|
116
|
+
const STATUS_CLASS: Record<string, string> = {
|
|
117
|
+
pending: 'is-active',
|
|
118
|
+
running: 'is-active',
|
|
119
|
+
'awaiting-confirm': 'is-warning',
|
|
120
|
+
success: 'is-success',
|
|
121
|
+
error: 'is-danger',
|
|
122
|
+
rejected: 'is-danger'
|
|
113
123
|
}
|
|
114
124
|
|
|
115
|
-
const
|
|
116
|
-
const statusClass = computed(() => STATUS_META[props.toolCall.status]?.class ?? '')
|
|
125
|
+
const statusClass = computed(() => STATUS_CLASS[props.toolCall.status] ?? '')
|
|
117
126
|
|
|
118
127
|
/** 进行中(pending/running):工具名带扫光效果 */
|
|
119
128
|
const isActive = computed(() => {
|
|
@@ -131,8 +140,10 @@ const isSettled = computed(() => {
|
|
|
131
140
|
*/
|
|
132
141
|
const destroyOnCollapse = computed(() => isSettled.value && !isPanelTool.value)
|
|
133
142
|
|
|
134
|
-
/**
|
|
135
|
-
const headerIcon = computed(
|
|
143
|
+
/** 头部图标:meta.icon > 宿主覆盖 > 内置名称规则 > 兜底;状态点走 status class */
|
|
144
|
+
const headerIcon = computed(
|
|
145
|
+
() => tool.value?.icon ?? resolveToolIcon(props.toolCall.name, di?.toolIcons?.value)
|
|
146
|
+
)
|
|
136
147
|
|
|
137
148
|
const parsedArguments = computed<Record<string, unknown> | null>(() => {
|
|
138
149
|
if (!props.toolCall.arguments) return null
|
|
@@ -165,13 +176,14 @@ const hasResult = computed(() => {
|
|
|
165
176
|
return props.toolCall.result != null || props.toolCall.error != null
|
|
166
177
|
})
|
|
167
178
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
179
|
+
const resultTitle = computed(() => (props.toolCall.status === 'error' ? '错误' : '结果'))
|
|
180
|
+
|
|
181
|
+
const displayResult = computed(() => formatUnknown(props.toolCall.error ?? props.toolCall.result))
|
|
182
|
+
|
|
183
|
+
/** 服务端下发的 view,格式化为 JSON;不得丢弃、不得 throw */
|
|
184
|
+
const prettyView = computed(() => {
|
|
185
|
+
if (props.toolCall.view == null) return ''
|
|
186
|
+
return formatUnknown(props.toolCall.view)
|
|
175
187
|
})
|
|
176
188
|
|
|
177
189
|
/** 使用方通过 tool-<name> 插槽自定义该工具的展示(有结果时替换整个 body) */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { EditPen, Folder, Internet, QuestionFilled, Terminal, Tools } from '@veltra/icons/normal'
|
|
2
|
+
import type { Component } from 'vue'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 内置 toolName → icon:按名称子串匹配,顺序即优先级。
|
|
6
|
+
* 未命中任何规则时走兜底图标,不得 throw。
|
|
7
|
+
*/
|
|
8
|
+
const TOOL_ICON_RULES: [needles: string[], icon: Component][] = [
|
|
9
|
+
[['bash', 'terminal'], Terminal],
|
|
10
|
+
[['fs', 'file'], Folder],
|
|
11
|
+
[['web', 'globe'], Internet],
|
|
12
|
+
[['edit', 'pencil'], EditPen],
|
|
13
|
+
[['ask', 'question'], QuestionFilled]
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
/** 未知名工具的兜底图标 */
|
|
17
|
+
export const FALLBACK_TOOL_ICON: Component = Tools
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 按工具名解析图标。
|
|
21
|
+
* 宿主 `toolIcons` 精确名覆盖优先于内置规则;都未命中则兜底。
|
|
22
|
+
*/
|
|
23
|
+
export function resolveToolIcon(name: string, overrides?: Record<string, Component>): Component {
|
|
24
|
+
const key = String(name ?? '')
|
|
25
|
+
const fromOverride = overrides?.[key]
|
|
26
|
+
if (fromOverride) return fromOverride
|
|
27
|
+
|
|
28
|
+
const lower = key.toLowerCase()
|
|
29
|
+
for (const [needles, icon] of TOOL_ICON_RULES) {
|
|
30
|
+
if (needles.some((needle) => lower.includes(needle))) return icon
|
|
31
|
+
}
|
|
32
|
+
return FALLBACK_TOOL_ICON
|
|
33
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="cls.e('tool-call-json')">
|
|
3
|
+
<div :class="cls.e('tool-call-json-head')">
|
|
4
|
+
<span :class="cls.e('tool-call-section')">{{ title }}</span>
|
|
5
|
+
<UButton
|
|
6
|
+
v-if="overLimit"
|
|
7
|
+
:class="cls.e('tool-call-json-toggle')"
|
|
8
|
+
size="small"
|
|
9
|
+
text
|
|
10
|
+
type="primary"
|
|
11
|
+
@click="expanded = !expanded"
|
|
12
|
+
>
|
|
13
|
+
{{ expanded ? '收起' : '展开' }}
|
|
14
|
+
</UButton>
|
|
15
|
+
</div>
|
|
16
|
+
<pre :class="[cls.e('tool-call-code'), bem.is('truncated', overLimit && !expanded)]">{{
|
|
17
|
+
shown
|
|
18
|
+
}}</pre>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<script lang="ts" setup>
|
|
23
|
+
import { UButton } from '@veltra/desktop'
|
|
24
|
+
import { bem } from '@veltra/utils'
|
|
25
|
+
import { computed, inject, ref } from 'vue'
|
|
26
|
+
|
|
27
|
+
import { AiChatDIKey } from './di'
|
|
28
|
+
|
|
29
|
+
defineOptions({ name: 'UAiChatToolJson' })
|
|
30
|
+
|
|
31
|
+
/** 超过该字符数截断,展开后可见全文 */
|
|
32
|
+
const PREVIEW_LIMIT = 480
|
|
33
|
+
|
|
34
|
+
const { title, text } = defineProps<{
|
|
35
|
+
/** 区块标题(参数 / 结果 / 视图) */
|
|
36
|
+
title: string
|
|
37
|
+
/** 已格式化的 JSON 或原文 */
|
|
38
|
+
text: string
|
|
39
|
+
}>()
|
|
40
|
+
|
|
41
|
+
const di = inject(AiChatDIKey)
|
|
42
|
+
const cls = di?.cls ?? bem('ai-chat')
|
|
43
|
+
|
|
44
|
+
const expanded = ref(false)
|
|
45
|
+
|
|
46
|
+
const overLimit = computed(() => text.length > PREVIEW_LIMIT)
|
|
47
|
+
|
|
48
|
+
const shown = computed(() => {
|
|
49
|
+
if (expanded.value || !overLimit.value) return text
|
|
50
|
+
return `${text.slice(0, PREVIEW_LIMIT)}…`
|
|
51
|
+
})
|
|
52
|
+
</script>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { QuestionFilled } from '@veltra/icons/normal'
|
|
2
|
-
import type
|
|
2
|
+
import { computed, defineComponent, h, type Component, type PropType } from 'vue'
|
|
3
3
|
|
|
4
|
-
import type { ChatTool } from '../../chat/types'
|
|
4
|
+
import type { ChatTool, ChatToolCall } from '../../chat/types'
|
|
5
5
|
import AskQuestion from '../../components/ai-chat/ask-question.vue'
|
|
6
|
-
import { deferAskQuestion, takeAskQuestionDeferred } from './deferred'
|
|
6
|
+
import { deferAskQuestion, resolveAskQuestion, takeAskQuestionDeferred } from './deferred'
|
|
7
7
|
|
|
8
8
|
/** 单个提问项 */
|
|
9
9
|
export interface AskQuestionItem {
|
|
@@ -75,6 +75,55 @@ const QUESTIONS_SCHEMA: Record<string, unknown> = {
|
|
|
75
75
|
required: ['questions']
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function parseQuestions(toolCall: ChatToolCall): AskQuestionItem[] {
|
|
79
|
+
try {
|
|
80
|
+
const args = JSON.parse(toolCall.arguments || '{}') as Partial<AskQuestionArgs>
|
|
81
|
+
return Array.isArray(args.questions) ? args.questions : []
|
|
82
|
+
} catch {
|
|
83
|
+
return []
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function parseAnswers(toolCall: ChatToolCall): AskQuestionAnswer[] | undefined {
|
|
88
|
+
if (toolCall.status !== 'success' || !toolCall.result) return undefined
|
|
89
|
+
try {
|
|
90
|
+
const result = JSON.parse(toolCall.result) as Partial<AskQuestionResult>
|
|
91
|
+
return Array.isArray(result.answers) ? result.answers : undefined
|
|
92
|
+
} catch {
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function statusError(toolCall: ChatToolCall): string | undefined {
|
|
98
|
+
if (toolCall.status !== 'error') return undefined
|
|
99
|
+
const err = toolCall.error ?? ''
|
|
100
|
+
if (err.includes('Abort') || err.includes('取消')) return '已取消'
|
|
101
|
+
return err || '提问失败'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 把 toolCall 转成提问表单的 questions / onSubmit;提交仍走 resolveAskQuestion */
|
|
105
|
+
const AskQuestionRender = defineComponent({
|
|
106
|
+
name: 'AskQuestionRender',
|
|
107
|
+
props: { toolCall: { type: Object as PropType<ChatToolCall>, required: true } },
|
|
108
|
+
setup(props) {
|
|
109
|
+
const questions = computed(() => parseQuestions(props.toolCall))
|
|
110
|
+
const answers = computed(() => parseAnswers(props.toolCall))
|
|
111
|
+
const error = computed(() => statusError(props.toolCall))
|
|
112
|
+
const onSubmit = (value: AskQuestionAnswer[] | AskQuestionResult) => {
|
|
113
|
+
resolveAskQuestion(props.toolCall.id, {
|
|
114
|
+
answers: Array.isArray(value) ? value : value.answers
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
return () =>
|
|
118
|
+
h(AskQuestion, {
|
|
119
|
+
questions: questions.value,
|
|
120
|
+
answers: answers.value,
|
|
121
|
+
error: error.value,
|
|
122
|
+
onSubmit
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
78
127
|
/**
|
|
79
128
|
* 创建内置提问工具:execute 挂起等待用户在 UI 中作答,提交后结果回灌模型。
|
|
80
129
|
* 工具卡片由内联提问表单渲染(多题分页导航,提交后展示问答摘要,不自动折叠)。
|
|
@@ -95,7 +144,7 @@ export function createAskQuestionTool(
|
|
|
95
144
|
parameters: QUESTIONS_SCHEMA,
|
|
96
145
|
icon,
|
|
97
146
|
label,
|
|
98
|
-
render:
|
|
147
|
+
render: AskQuestionRender,
|
|
99
148
|
execute: (_args, { toolCall, signal }) =>
|
|
100
149
|
new Promise<AskQuestionResult>((resolve, reject) => {
|
|
101
150
|
deferAskQuestion(toolCall.id, { resolve, reject })
|
package/src/types/ai-chat.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DeconstructValue } from '@veltra/utils'
|
|
2
|
-
import type { Ref } from 'vue'
|
|
2
|
+
import type { Component, Ref } from 'vue'
|
|
3
3
|
|
|
4
|
+
import type { ChatSessionTransport } from '../chat/session'
|
|
4
5
|
import type {
|
|
5
6
|
ChatAttachment,
|
|
6
7
|
ChatMessage,
|
|
@@ -8,15 +9,19 @@ import type {
|
|
|
8
9
|
ChatTokenUsage,
|
|
9
10
|
ChatTool,
|
|
10
11
|
ChatToolCall,
|
|
12
|
+
ChatToolMeta,
|
|
11
13
|
ChatTransport
|
|
12
14
|
} from '../chat/types'
|
|
13
15
|
import type { ChatModelOption } from '../providers'
|
|
14
16
|
|
|
15
17
|
export interface AiChatProps {
|
|
16
|
-
/**
|
|
17
|
-
transport: ChatTransport
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/** 传输层(必填);函数型 ChatTransport 或 session 对象,可使用 createOpenAITransport / createServerTransport */
|
|
19
|
+
transport: ChatTransport | ChatSessionTransport
|
|
20
|
+
/**
|
|
21
|
+
* 工具列表。函数 transport 传入 ChatTool[](必有 execute);
|
|
22
|
+
* session 下可为 ChatToolMeta[](仅渲染元信息)。ChatTool[] 仍可赋值。
|
|
23
|
+
*/
|
|
24
|
+
tools?: (ChatTool | ChatToolMeta)[]
|
|
20
25
|
/** 系统提示词 */
|
|
21
26
|
systemPrompt?: string
|
|
22
27
|
/**
|
|
@@ -50,6 +55,13 @@ export interface AiChatProps {
|
|
|
50
55
|
* 默认 false:仅在拿到 usage 时显示会话累计「总 token」。接口未返回 usage 时不展示。
|
|
51
56
|
*/
|
|
52
57
|
tokenUsageDetail?: boolean
|
|
58
|
+
/**
|
|
59
|
+
* 覆盖包内 toolName → icon 映射(精确名优先于内置名称规则)。
|
|
60
|
+
* 缺省不传则走内置表;未命中的名称仍用兜底图标,不得 throw。
|
|
61
|
+
*/
|
|
62
|
+
toolIcons?: Record<string, Component>
|
|
63
|
+
/** 只读:不展示输入区、欢迎语不可发送、队列无插队/编辑/移除。默认 false */
|
|
64
|
+
readonly?: boolean
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
export interface AiChatEmits {
|