实现响应结果的重排和对象输入框的实现
This commit is contained in:
parent
f14687b1d8
commit
ad857e6544
14
README.md
14
README.md
@ -105,17 +105,3 @@ B <--mcp--> m(MCP Server)
|
||||
```
|
||||
|
||||
and just press f5, いただきます
|
||||
|
||||
## Flowchart
|
||||
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A[用户输入问题] --> B[选择工具]
|
||||
B --> C[大模型处理]
|
||||
C --> D{是否有tool use?}
|
||||
D -- 否 --> E[返回 content]
|
||||
D -- 是 --> F[执行工具]
|
||||
F --> G[返回工具执行结果]
|
||||
G --> C
|
||||
```
|
@ -15,7 +15,7 @@ import MainPanel from '@/components/main-panel/index.vue';
|
||||
import { setDefaultCss } from './hook/css';
|
||||
import { greenLog, pinkLog } from './views/setting/util';
|
||||
import { acquireVsCodeApi, useMessageBridge } from './api/message-bridge';
|
||||
import { connectionArgs, connectionMethods, connectionResult, doConnect, getServerVersion, launchConnect } from './views/connect/connection';
|
||||
import { connectionArgs, connectionMethods, doConnect, launchConnect } from './views/connect/connection';
|
||||
import { loadSetting } from './hook/setting';
|
||||
import { loadPanels } from './hook/panel';
|
||||
|
||||
@ -29,7 +29,7 @@ bridge.addCommandListener('hello', data => {
|
||||
|
||||
|
||||
function initDebug() {
|
||||
connectionArgs.commandString = 'uv run mcp run ../servers/main.py';
|
||||
connectionArgs.commandString = 'node C:/Users/K/code/servers/src/puppeteer/dist/index.js';
|
||||
connectionMethods.current = 'STDIO';
|
||||
|
||||
setTimeout(async () => {
|
||||
|
194
renderer/src/components/k-input-object/index.vue
Normal file
194
renderer/src/components/k-input-object/index.vue
Normal file
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<div class="k-input-object">
|
||||
<textarea ref="textareaRef" v-model="inputValue" class="k-input-object__textarea"
|
||||
:class="{ 'is-invalid': isInvalid }" @input="handleInput" @blur="handleBlur"
|
||||
@keydown="handleKeydown"
|
||||
:placeholder="props.placeholder"
|
||||
></textarea>
|
||||
</div>
|
||||
<div v-if="errorMessage" class="k-input-object__error">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch, nextTick } from 'vue';
|
||||
import { debounce } from 'lodash-es';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'KInputObject',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请输入 JSON 对象'
|
||||
},
|
||||
debounceTime: {
|
||||
type: Number,
|
||||
default: 500
|
||||
}
|
||||
},
|
||||
emits: ['update:modelValue', 'parse-error'],
|
||||
setup(props, { emit }) {
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
const inputValue = ref<string>(JSON.stringify(props.modelValue, null, 2))
|
||||
const isInvalid = ref<boolean>(false)
|
||||
const errorMessage = ref<string>('')
|
||||
|
||||
// 防抖处理输入
|
||||
const debouncedParse = debounce((value: string) => {
|
||||
if (value.trim() === '') {
|
||||
errorMessage.value = '';
|
||||
isInvalid.value = false;
|
||||
emit('update:modelValue', undefined);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
isInvalid.value = false;
|
||||
errorMessage.value = '';
|
||||
emit('update:modelValue', parsed);
|
||||
} catch (error) {
|
||||
isInvalid.value = true;
|
||||
errorMessage.value = 'JSON 解析错误: ' + (error as Error).message;
|
||||
emit('parse-error', error);
|
||||
}
|
||||
}, props.debounceTime)
|
||||
|
||||
const handleInput = () => {
|
||||
debouncedParse(inputValue.value)
|
||||
}
|
||||
|
||||
const handleBlur = () => {
|
||||
// 立即执行最后一次防抖
|
||||
debouncedParse.flush()
|
||||
}
|
||||
|
||||
// 当外部 modelValue 变化时更新输入框内容
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
const currentParsed = tryParse(inputValue.value)
|
||||
if (!isDeepEqual(currentParsed, newVal)) {
|
||||
inputValue.value = JSON.stringify(newVal, null, 2)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 辅助函数:尝试解析 JSON
|
||||
const tryParse = (value: string): any => {
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 辅助函数:深度比较对象
|
||||
const isDeepEqual = (obj1: any, obj2: any): boolean => {
|
||||
return JSON.stringify(obj1) === JSON.stringify(obj2)
|
||||
}
|
||||
|
||||
// 自动调整文本区域高度
|
||||
const adjustTextareaHeight = () => {
|
||||
nextTick(() => {
|
||||
if (textareaRef.value) {
|
||||
textareaRef.value.style.height = 'auto'
|
||||
textareaRef.value.style.height = `${textareaRef.value.scrollHeight}px`
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(inputValue, adjustTextareaHeight, { immediate: true })
|
||||
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === '{') {
|
||||
event.preventDefault();
|
||||
const start = textareaRef.value!.selectionStart;
|
||||
const end = textareaRef.value!.selectionEnd;
|
||||
const value = inputValue.value;
|
||||
const newValue = value.substring(0, start) + '{\n \n}' + value.substring(end);
|
||||
inputValue.value = newValue;
|
||||
nextTick(() => {
|
||||
textareaRef.value!.setSelectionRange(start + 2, start + 2);
|
||||
});
|
||||
} else if (event.key === '"') {
|
||||
event.preventDefault();
|
||||
const start = textareaRef.value!.selectionStart;
|
||||
const end = textareaRef.value!.selectionEnd;
|
||||
const value = inputValue.value;
|
||||
const newValue = value.substring(0, start) + '""' + value.substring(end);
|
||||
inputValue.value = newValue;
|
||||
nextTick(() => {
|
||||
textareaRef.value!.setSelectionRange(start + 1, start + 1);
|
||||
});
|
||||
} else if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
const start = textareaRef.value!.selectionStart;
|
||||
const end = textareaRef.value!.selectionEnd;
|
||||
const value = inputValue.value;
|
||||
const newValue = value.substring(0, start) + ' ' + value.substring(end);
|
||||
inputValue.value = newValue;
|
||||
nextTick(() => {
|
||||
textareaRef.value!.setSelectionRange(start + 1, start + 1);
|
||||
});
|
||||
} else if (event.key === 'Enter' && inputValue.value.trim() === '') {
|
||||
event.preventDefault();
|
||||
inputValue.value = '{}';
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
textareaRef,
|
||||
inputValue,
|
||||
isInvalid,
|
||||
errorMessage,
|
||||
handleInput,
|
||||
handleBlur,
|
||||
handleKeydown,
|
||||
props
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.k-input-object {
|
||||
width: 100%;
|
||||
background-color: var(--background);
|
||||
border-radius: .5em;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.k-input-object__textarea {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
transition: border-color 0.2s;
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.k-input-object__textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--main-color);
|
||||
}
|
||||
|
||||
.k-input-object__textarea.is-invalid {
|
||||
border-color: var(--el-color-error);
|
||||
}
|
||||
|
||||
.k-input-object__error {
|
||||
color: var(--el-color-error);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
@ -1,14 +1,27 @@
|
||||
import { ToolItem } from "@/hook/type";
|
||||
import { ref } from "vue";
|
||||
import { Ref, ref } from "vue";
|
||||
|
||||
import type { OpenAI } from 'openai';
|
||||
type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
||||
|
||||
export enum MessageState {
|
||||
ServerError = 'server internal error',
|
||||
ReceiveChunkError = 'receive chunk error',
|
||||
Timeout = 'timeout',
|
||||
MaxEpochs = 'max epochs',
|
||||
Unknown = 'unknown error',
|
||||
Abort = 'abort',
|
||||
ToolCall = 'tool call failed',
|
||||
None = 'none',
|
||||
Success = 'success'
|
||||
}
|
||||
|
||||
export interface IExtraInfo {
|
||||
created: number,
|
||||
state: MessageState,
|
||||
serverName: string,
|
||||
usage?: ChatCompletionChunk['usage'];
|
||||
[key: string]: any
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
@ -53,6 +66,15 @@ export interface ToolCall {
|
||||
|
||||
export const allTools = ref<ToolItem[]>([]);
|
||||
|
||||
export interface IRenderMessage {
|
||||
role: 'user' | 'assistant/content' | 'assistant/tool_calls' | 'tool';
|
||||
content: string;
|
||||
toolResult?: string;
|
||||
tool_calls?: ToolCall[];
|
||||
showJson?: Ref<boolean>;
|
||||
extraInfo: IExtraInfo;
|
||||
}
|
||||
|
||||
export function getToolSchema(enableTools: EnableToolItem[]) {
|
||||
const toolsSchema = [];
|
||||
for (let i = 0; i < enableTools.length; i++) {
|
||||
|
@ -78,11 +78,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, nextTick, watch, Ref } from 'vue';
|
||||
import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, nextTick, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ElMessage, ScrollbarInstance } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
import { ChatMessage, ChatStorage, IExtraInfo, ToolCall } from './chat';
|
||||
import { ChatMessage, ChatStorage, IRenderMessage, MessageState, ToolCall } from './chat';
|
||||
|
||||
import Setting from './setting.vue';
|
||||
|
||||
@ -125,16 +125,6 @@ if (!tabStorage.messages) {
|
||||
tabStorage.messages = [] as ChatMessage[];
|
||||
}
|
||||
|
||||
interface IRenderMessage {
|
||||
role: 'user' | 'assistant/content' | 'assistant/tool_calls' | 'tool';
|
||||
content: string;
|
||||
toolResult?: string;
|
||||
tool_calls?: ToolCall[];
|
||||
showJson?: Ref<boolean>;
|
||||
extraInfo: IExtraInfo;
|
||||
isLast: boolean;
|
||||
}
|
||||
|
||||
const renderMessages = computed(() => {
|
||||
const messages: IRenderMessage[] = [];
|
||||
for (const message of tabStorage.messages) {
|
||||
@ -142,8 +132,7 @@ const renderMessages = computed(() => {
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: message.content,
|
||||
extraInfo: message.extraInfo,
|
||||
isLast: false
|
||||
extraInfo: message.extraInfo
|
||||
});
|
||||
} else if (message.role === 'assistant') {
|
||||
if (message.tool_calls) {
|
||||
@ -152,15 +141,16 @@ const renderMessages = computed(() => {
|
||||
content: message.content,
|
||||
tool_calls: message.tool_calls,
|
||||
showJson: ref(false),
|
||||
extraInfo: message.extraInfo,
|
||||
isLast: false
|
||||
extraInfo: {
|
||||
...message.extraInfo,
|
||||
state: MessageState.Unknown
|
||||
}
|
||||
});
|
||||
} else {
|
||||
messages.push({
|
||||
role: 'assistant/content',
|
||||
content: message.content,
|
||||
extraInfo: message.extraInfo,
|
||||
isLast: false
|
||||
extraInfo: message.extraInfo
|
||||
});
|
||||
}
|
||||
|
||||
@ -169,16 +159,12 @@ const renderMessages = computed(() => {
|
||||
const lastAssistantMessage = messages[messages.length - 1];
|
||||
if (lastAssistantMessage.role === 'assistant/tool_calls') {
|
||||
lastAssistantMessage.toolResult = message.content;
|
||||
lastAssistantMessage.extraInfo.state = message.extraInfo.state;
|
||||
lastAssistantMessage.extraInfo.usage = lastAssistantMessage.extraInfo.usage || message.extraInfo.usage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
lastMessage.isLast = true;
|
||||
}
|
||||
|
||||
return messages;
|
||||
});
|
||||
|
||||
@ -264,26 +250,30 @@ const handleSend = () => {
|
||||
|
||||
loop = new TaskLoop(streamingContent, streamingToolCalls);
|
||||
|
||||
loop.registerOnError((msg) => {
|
||||
loop.registerOnError((error) => {
|
||||
|
||||
ElMessage({
|
||||
message: msg,
|
||||
message: error.msg,
|
||||
type: 'error',
|
||||
duration: 3000
|
||||
});
|
||||
|
||||
tabStorage.messages.push({
|
||||
role: 'assistant',
|
||||
content: `错误: ${msg}`,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
|
||||
}
|
||||
});
|
||||
|
||||
if (error.state === MessageState.ReceiveChunkError) {
|
||||
tabStorage.messages.push({
|
||||
role: 'assistant',
|
||||
content: error.msg,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
state: error.state,
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isLoading.value = false;
|
||||
});
|
||||
|
||||
loop.registerOnChunk((chunk) => {
|
||||
loop.registerOnChunk(() => {
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
|
@ -49,9 +49,6 @@ const usageStatistic = computed(() => {
|
||||
return makeUsageStatistic(props.message.extraInfo);
|
||||
});
|
||||
|
||||
console.log(props.message);
|
||||
console.log(usageStatistic);
|
||||
|
||||
const showTime = ref(false);
|
||||
</script>
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="message-role">
|
||||
Agent
|
||||
<span class="message-reminder" v-if="props.message.isLast && !props.message.toolResult">
|
||||
<span class="message-reminder" v-if="!props.message.toolResult">
|
||||
正在使用工具
|
||||
<span class="tool-loading iconfont icon-double-loading">
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="message-text tool_calls" :class="{ 'fail': !props.message.isLast && !props.message.toolResult }">
|
||||
<div class="message-text tool_calls" :class="{ 'fail': props.message.toolResult && props.message.extraInfo.state != MessageState.Success }">
|
||||
<div v-if="props.message.content" v-html="markdownToHtml(props.message.content)"></div>
|
||||
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse v-model="activeNames" v-if="props.message.tool_calls">
|
||||
<el-collapse-item name="tool">
|
||||
|
||||
<template #title>
|
||||
@ -35,28 +35,46 @@
|
||||
<!-- 工具调用结果 -->
|
||||
<div v-if="props.message.toolResult">
|
||||
<div class="tool-call-header">
|
||||
<span class="tool-name">{{ "响应" }}</span>
|
||||
<span style="width: 200px;" class="tools-dialog-container">
|
||||
<span class="tool-name" :class="{ 'error': !isValidJson }">
|
||||
{{ isValidJson ? '响应': '错误' }}
|
||||
</span>
|
||||
<span style="width: 200px;" class="tools-dialog-container" v-if="isValidJson">
|
||||
<el-switch v-model="props.message.showJson!.value" inline-prompt active-text="JSON"
|
||||
inactive-text="Text" style="margin-left: 10px; width: 200px;"
|
||||
:inactive-action-style="'backgroundColor: var(--sidebar)'" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="tool-result" v-if="isValidJSON(props.message.toolResult)">
|
||||
<div class="tool-result" v-if="isValidJson">
|
||||
<div v-if="props.message.showJson!.value" class="tool-result-content">
|
||||
<div class="inner">
|
||||
<div v-html="jsonResultToHtml(props.message.toolResult)"></div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>
|
||||
<div v-for="(item, index) in JSON.parse(props.message.toolResult)" :key="index">
|
||||
<div v-for="(item, index) in JSON.parse(props.message.toolResult)" :key="index"
|
||||
class="response-item"
|
||||
>
|
||||
<el-scrollbar width="100%">
|
||||
<div v-if="item.type === 'text'" class="tool-text">{{ item.text }}</div>
|
||||
<div v-if="item.type === 'text'" class="tool-text">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="item.type === 'image'" class="tool-image">
|
||||
<img :src="`data:${item.mimeType};base64,${item.data}`" style="max-width: 70%;" />
|
||||
</div>
|
||||
|
||||
<div v-else class="tool-other">{{ JSON.stringify(item) }}</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="tool-result" :class="{ 'error': !isValidJson }">
|
||||
<div class="tool-result-content">
|
||||
<div class="inner">
|
||||
{{ props.message.toolResult }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
@ -66,19 +84,21 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref, watch } from 'vue';
|
||||
import { defineProps, ref, watch, PropType, computed } from 'vue';
|
||||
|
||||
import MessageMeta from './message-meta.vue';
|
||||
import { markdownToHtml } from '../markdown';
|
||||
import { createTest } from '@/views/setting/llm';
|
||||
import { IRenderMessage, MessageState } from '../chat';
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
type: Object,
|
||||
type: Object as PropType<IRenderMessage>,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const activeNames = ref<string[]>(props.message.toolResult ? [''] : ['tool']);
|
||||
|
||||
watch(
|
||||
@ -98,14 +118,15 @@ const jsonResultToHtml = (jsonString: string) => {
|
||||
return html;
|
||||
};
|
||||
|
||||
const isValidJSON = (str: string) => {
|
||||
|
||||
const isValidJson = computed(() => {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
JSON.parse(props.message.toolResult || '');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@ -130,7 +151,7 @@ const isValidJSON = (str: string) => {
|
||||
.tool-call-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.tool-name {
|
||||
@ -143,6 +164,10 @@ const isValidJSON = (str: string) => {
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
.tool-name.error {
|
||||
color: var(--el-color-error);
|
||||
}
|
||||
|
||||
.tool-type {
|
||||
font-size: 0.8em;
|
||||
color: var(--el-text-color-secondary);
|
||||
@ -155,6 +180,10 @@ const isValidJSON = (str: string) => {
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.response-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tool-arguments {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
@ -170,6 +199,10 @@ const isValidJSON = (str: string) => {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tool-result.error {
|
||||
background-color: rgba(245, 108, 108, 0.5);
|
||||
}
|
||||
|
||||
.tool-text {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* eslint-disable */
|
||||
import { Ref } from "vue";
|
||||
import { ToolCall, ChatStorage, getToolSchema } from "./chat";
|
||||
import { ToolCall, ChatStorage, getToolSchema, MessageState } from "./chat";
|
||||
import { useMessageBridge } from "@/api/message-bridge";
|
||||
import type { OpenAI } from 'openai';
|
||||
import { callTool } from "../tool/tools";
|
||||
@ -12,6 +12,11 @@ interface TaskLoopOptions {
|
||||
maxEpochs: number;
|
||||
}
|
||||
|
||||
interface IErrorMssage {
|
||||
state: MessageState,
|
||||
msg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 对任务循环进行的抽象封装
|
||||
*/
|
||||
@ -23,7 +28,7 @@ export class TaskLoop {
|
||||
constructor(
|
||||
private readonly streamingContent: Ref<string>,
|
||||
private readonly streamingToolCalls: Ref<ToolCall[]>,
|
||||
private onError: (msg: string) => void = (msg) => {},
|
||||
private onError: (error: IErrorMssage) => void = (msg) => {},
|
||||
private onChunk: (chunk: ChatCompletionChunk) => void = (chunk) => {},
|
||||
private onDone: () => void = () => {},
|
||||
private onEpoch: () => void = () => {},
|
||||
@ -42,15 +47,33 @@ export class TaskLoop {
|
||||
|
||||
if (!toolResponse.isError) {
|
||||
const content = JSON.stringify(toolResponse.content);
|
||||
return content;
|
||||
return {
|
||||
content,
|
||||
state: MessageState.Success,
|
||||
|
||||
};
|
||||
} else {
|
||||
this.onError(`工具调用失败: ${toolResponse.content}`);
|
||||
console.error(toolResponse.content);
|
||||
this.onError({
|
||||
state: MessageState.ToolCall,
|
||||
msg: `工具调用失败: ${toolResponse.content}`
|
||||
});
|
||||
console.error(toolResponse.content);
|
||||
return {
|
||||
content: toolResponse.content.toString(),
|
||||
state: MessageState.ToolCall
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.onError(`工具调用失败: ${(error as Error).message}`);
|
||||
this.onError({
|
||||
state: MessageState.ToolCall,
|
||||
msg: `工具调用失败: ${(error as Error).message}`
|
||||
});
|
||||
console.error(error);
|
||||
return {
|
||||
content: (error as Error).message,
|
||||
state: MessageState.ToolCall
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +130,10 @@ export class TaskLoop {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => {
|
||||
if (data.code !== 200) {
|
||||
this.onError(data.msg || '请求模型服务时发生错误');
|
||||
this.onError({
|
||||
state: MessageState.ReceiveChunkError,
|
||||
msg: data.msg || '请求模型服务时发生错误'
|
||||
});
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
@ -182,7 +208,7 @@ export class TaskLoop {
|
||||
this.streamingToolCalls.value = [];
|
||||
}
|
||||
|
||||
public registerOnError(handler: (msg: string) => void) {
|
||||
public registerOnError(handler: (msg: IErrorMssage) => void) {
|
||||
this.onError = handler;
|
||||
}
|
||||
|
||||
@ -208,6 +234,7 @@ export class TaskLoop {
|
||||
content: userMessage,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
state: MessageState.Success,
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
|
||||
}
|
||||
});
|
||||
@ -238,6 +265,7 @@ export class TaskLoop {
|
||||
tool_calls: this.streamingToolCalls.value,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
state: MessageState.Success,
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
|
||||
}
|
||||
});
|
||||
@ -249,9 +277,10 @@ export class TaskLoop {
|
||||
tabStorage.messages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: toolCall.id || toolCall.function.name,
|
||||
content: toolCallResult,
|
||||
content: toolCallResult.content,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
state: toolCallResult.state,
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown',
|
||||
usage: this.completionUsage
|
||||
}
|
||||
@ -264,6 +293,7 @@ export class TaskLoop {
|
||||
content: this.streamingContent.value,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
state: MessageState.Success,
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown',
|
||||
usage: this.completionUsage
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ export function makeUsageStatistic(extraInfo: IExtraInfo): UsageStatistic | unde
|
||||
input: usage.prompt_tokens,
|
||||
output: usage.completion_tokens,
|
||||
total: usage.prompt_tokens + usage.completion_tokens,
|
||||
cacheHitRatio: Math.ceil(usage.prompt_tokens_details?.cached_tokens || 0 / usage.prompt_tokens * 1000) / 10,
|
||||
cacheHitRatio: Math.ceil((usage.prompt_tokens_details?.cached_tokens || 0) / usage.prompt_tokens * 1000) / 10,
|
||||
}
|
||||
|
||||
case 'openai':
|
||||
|
@ -16,7 +16,7 @@
|
||||
v-if="property.type === 'string'"
|
||||
v-model="tabStorage.formData[name]"
|
||||
type="text"
|
||||
:placeholder="t('enter') + ' ' + (property.title || name)"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
|
||||
@keydown.enter.prevent="handleExecute"
|
||||
/>
|
||||
|
||||
@ -24,14 +24,23 @@
|
||||
v-else-if="property.type === 'number' || property.type === 'integer'"
|
||||
v-model="tabStorage.formData[name]"
|
||||
controls-position="right"
|
||||
:placeholder="t('enter') + ' ' + (property.title || name)"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
|
||||
@keydown.enter.prevent="handleExecute"
|
||||
/>
|
||||
|
||||
<el-switch
|
||||
v-else-if="property.type === 'boolean'"
|
||||
v-else-if="property.type === 'boolean'"
|
||||
active-text="true"
|
||||
inactive-text="false"
|
||||
v-model="tabStorage.formData[name]"
|
||||
/>
|
||||
|
||||
|
||||
<k-input-object
|
||||
v-else-if="property.type === 'object'"
|
||||
v-model="tabStorage.formData[name]"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
@ -53,9 +62,10 @@ import { useI18n } from 'vue-i18n';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
import { callTool, toolsManager, ToolStorage } from './tools';
|
||||
import { pinkLog } from '@/views/setting/util';
|
||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||
|
||||
import KInputObject from '@/components/k-input-object/index.vue';
|
||||
|
||||
defineComponent({ name: 'tool-executor' });
|
||||
|
||||
const { t } = useI18n();
|
||||
@ -74,6 +84,9 @@ if (!tabStorage.formData) {
|
||||
tabStorage.formData = {};
|
||||
}
|
||||
|
||||
console.log(tabStorage.formData);
|
||||
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
|
||||
@ -81,6 +94,7 @@ const currentTool = computed(() => {
|
||||
return toolsManager.tools.find(tool => tool.name === tabStorage.currentToolName);
|
||||
});
|
||||
|
||||
|
||||
const formRules = computed<FormRules>(() => {
|
||||
const rules: FormRules = {};
|
||||
if (!currentTool.value?.inputSchema?.properties) return rules;
|
||||
@ -108,11 +122,13 @@ const initFormData = () => {
|
||||
|
||||
if (!currentTool.value?.inputSchema?.properties) return;
|
||||
|
||||
const newSchemaDataForm: Record<string, number | boolean | string> = {};
|
||||
const newSchemaDataForm: Record<string, number | boolean | string | object> = {};
|
||||
|
||||
console.log(currentTool.value.inputSchema.properties);
|
||||
|
||||
Object.entries(currentTool.value.inputSchema.properties).forEach(([name, property]) => {
|
||||
newSchemaDataForm[name] = getDefaultValue(property);
|
||||
const originType = normaliseJavascriptType(typeof tabStorage.formData[name]);
|
||||
const originType = normaliseJavascriptType(typeof tabStorage.formData[name]);
|
||||
|
||||
if (tabStorage.formData[name] !== undefined && originType === property.type) {
|
||||
newSchemaDataForm[name] = tabStorage.formData[name];
|
||||
@ -145,4 +161,21 @@ watch(() => tabStorage.currentToolName, () => {
|
||||
border-radius: .5em;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tool-executor-container {
|
||||
|
||||
}
|
||||
|
||||
.tool-executor-container .el-switch .el-switch__action {
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
|
||||
.tool-executor-container .el-switch.is-checked .el-switch__action {
|
||||
background-color: var(--sidebar);
|
||||
}
|
||||
|
||||
.tool-executor-container .el-switch__core {
|
||||
border: 1px solid var(--main-color) !important;
|
||||
}
|
||||
|
||||
</style>
|
@ -12,7 +12,7 @@ export const toolsManager = reactive<{
|
||||
export interface ToolStorage {
|
||||
currentToolName: string;
|
||||
lastToolCallResponse?: ToolCallResponse;
|
||||
formData: Record<string, number | string | boolean>;
|
||||
formData: Record<string, any>;
|
||||
}
|
||||
|
||||
const bridge = useMessageBridge();
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { SchemaProperty } from "./type";
|
||||
|
||||
interface TypeAble {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export function getDefaultValue(property: TypeAble) {
|
||||
export function getDefaultValue(property: TypeAble) {
|
||||
if (property.type === 'number' || property.type === 'integer') {
|
||||
return 0;
|
||||
} else if (property.type === 'boolean') {
|
||||
return false;
|
||||
} else if (property.type === 'object') {
|
||||
return {};
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@ -19,7 +19,7 @@ export function normaliseJavascriptType(type: string) {
|
||||
case 'integer':
|
||||
return 'number';
|
||||
case 'number':
|
||||
return 'integer';
|
||||
return 'number';
|
||||
case 'boolean':
|
||||
return 'boolean';
|
||||
case 'string':
|
||||
|
@ -2,6 +2,7 @@
|
||||
export interface SchemaProperty {
|
||||
title: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface InputSchema {
|
||||
|
@ -144,5 +144,6 @@
|
||||
"single-dialog": "محادثة من جولة واحدة",
|
||||
"multi-dialog": "محادثة متعددة الجولات",
|
||||
"press-and-run": "اكتب سؤالاً لبدء الاختبار",
|
||||
"connect-sigature": "توقيع الاتصال"
|
||||
"connect-sigature": "توقيع الاتصال",
|
||||
"finish-refresh": "تم التحديث"
|
||||
}
|
@ -144,5 +144,6 @@
|
||||
"single-dialog": "Einzelrunden-Dialog",
|
||||
"multi-dialog": "Mehrrundengespräch",
|
||||
"press-and-run": "Geben Sie eine Frage ein, um den Test zu starten",
|
||||
"connect-sigature": "Verbindungssignatur"
|
||||
"connect-sigature": "Verbindungssignatur",
|
||||
"finish-refresh": "Aktualisierung abgeschlossen"
|
||||
}
|
@ -144,5 +144,6 @@
|
||||
"single-dialog": "Single-round dialogue",
|
||||
"multi-dialog": "Multi-turn conversation",
|
||||
"press-and-run": "Type a question to start the test",
|
||||
"connect-sigature": "Connection signature"
|
||||
"connect-sigature": "Connection signature",
|
||||
"finish-refresh": "Refresh completed"
|
||||
}
|
@ -144,5 +144,6 @@
|
||||
"single-dialog": "Dialogue en un tour",
|
||||
"multi-dialog": "Conversation multi-tours",
|
||||
"press-and-run": "Tapez une question pour commencer le test",
|
||||
"connect-sigature": "Signature de connexion"
|
||||
"connect-sigature": "Signature de connexion",
|
||||
"finish-refresh": "Actualisation terminée"
|
||||
}
|
@ -144,5 +144,6 @@
|
||||
"single-dialog": "単一ラウンドの対話",
|
||||
"multi-dialog": "マルチターン会話",
|
||||
"press-and-run": "テストを開始するには質問を入力してください",
|
||||
"connect-sigature": "接続署名"
|
||||
"connect-sigature": "接続署名",
|
||||
"finish-refresh": "更新が完了しました"
|
||||
}
|
@ -144,5 +144,6 @@
|
||||
"single-dialog": "단일 라운드 대화",
|
||||
"multi-dialog": "다중 턴 대화",
|
||||
"press-and-run": "테스트를 시작하려면 질문을 입력하세요",
|
||||
"connect-sigature": "연결 서명"
|
||||
"connect-sigature": "연결 서명",
|
||||
"finish-refresh": "새로 고침 완료"
|
||||
}
|
@ -144,5 +144,6 @@
|
||||
"single-dialog": "Однораундовый диалог",
|
||||
"multi-dialog": "Многораундовый разговор",
|
||||
"press-and-run": "Введите вопрос, чтобы начать тест",
|
||||
"connect-sigature": "Подпись соединения"
|
||||
"connect-sigature": "Подпись соединения",
|
||||
"finish-refresh": "Обновление завершено"
|
||||
}
|
@ -144,5 +144,6 @@
|
||||
"single-dialog": "单轮对话",
|
||||
"multi-dialog": "多轮对话",
|
||||
"press-and-run": "键入问题以开始测试",
|
||||
"connect-sigature": "连接签名"
|
||||
"connect-sigature": "连接签名",
|
||||
"finish-refresh": "完成刷新"
|
||||
}
|
@ -144,5 +144,6 @@
|
||||
"single-dialog": "單輪對話",
|
||||
"multi-dialog": "多輪對話",
|
||||
"press-and-run": "輸入問題以開始測試",
|
||||
"connect-sigature": "連接簽名"
|
||||
"connect-sigature": "連接簽名",
|
||||
"finish-refresh": "刷新完成"
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="connection-option">
|
||||
<span>{{ t('log') }}</span>
|
||||
<el-scrollbar height="100%">
|
||||
<el-scrollbar height="90%">
|
||||
<div class="output-content">
|
||||
<div v-for="(log, index) in connectionResult.logString" :key="index" :class="log.type">
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
@ -24,7 +24,7 @@ const { t } = useI18n();
|
||||
|
||||
<style>
|
||||
.connection-option {
|
||||
height: 100%;
|
||||
height: 90%;
|
||||
}
|
||||
|
||||
.connection-option .el-scrollbar__view {
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="connection-container">
|
||||
<el-scrollbar>
|
||||
<div class="connection-container">
|
||||
<div class="connect-panel-container">
|
||||
<ConnectionMethod></ConnectionMethod>
|
||||
<ConnectionArgs></ConnectionArgs>
|
||||
@ -18,6 +19,7 @@
|
||||
<ConnectionLog></ConnectionLog>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
</template>
|
||||
|
||||
|
4
service/.gitignore
vendored
4
service/.gitignore
vendored
@ -22,4 +22,6 @@ pnpm-debug.log*
|
||||
*.sln
|
||||
*.sw?
|
||||
config.json
|
||||
setting.json
|
||||
setting.json
|
||||
|
||||
tabs.example-servers/puppeteer.json
|
@ -1,129 +0,0 @@
|
||||
{
|
||||
"currentIndex": 1,
|
||||
"tabs": [
|
||||
{
|
||||
"name": "空白测试 2",
|
||||
"icon": "icon-blank",
|
||||
"type": "blank",
|
||||
"componentIndex": -1,
|
||||
"storage": {}
|
||||
},
|
||||
{
|
||||
"name": "交互测试",
|
||||
"icon": "icon-robot",
|
||||
"type": "blank",
|
||||
"componentIndex": 3,
|
||||
"storage": {
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "今天天气",
|
||||
"extraInfo": {
|
||||
"created": 1745498953049,
|
||||
"serverName": "Huoshan DeepSeek"
|
||||
}
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_zw5axi9gftk294vifdlfv83y",
|
||||
"index": 0,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "puppeteer_navigate",
|
||||
"arguments": "{\"url\":\"https://www.weather.com\"}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"extraInfo": {
|
||||
"created": 1745498953891,
|
||||
"serverName": "Huoshan DeepSeek"
|
||||
}
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_zw5axi9gftk294vifdlfv83y",
|
||||
"content": "[{\"type\":\"text\",\"text\":\"Navigated to https://www.weather.com\"}]",
|
||||
"extraInfo": {
|
||||
"created": 1745498965901,
|
||||
"serverName": "Huoshan DeepSeek"
|
||||
}
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_enwp8eh9q0rklmz1cr0lfinx",
|
||||
"index": 0,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "puppeteer_evaluate",
|
||||
"arguments": "{\"script\":\"document.querySelector('.CurrentConditions--tempValue--3KcTQ').innerText\"}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"extraInfo": {
|
||||
"created": 1745498967051,
|
||||
"serverName": "Huoshan DeepSeek"
|
||||
}
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "错误: 工具调用失败: [object Object]",
|
||||
"extraInfo": {
|
||||
"created": 1745498967057,
|
||||
"serverName": "Huoshan DeepSeek"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"modelIndex": 8,
|
||||
"enableTools": [
|
||||
{
|
||||
"name": "puppeteer_navigate",
|
||||
"description": "Navigate to a URL",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "puppeteer_screenshot",
|
||||
"description": "Take a screenshot of the current page or a specific element",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "puppeteer_click",
|
||||
"description": "Click an element on the page",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "puppeteer_fill",
|
||||
"description": "Fill out an input field",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "puppeteer_select",
|
||||
"description": "Select an element on the page with Select tag",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "puppeteer_hover",
|
||||
"description": "Hover an element on the page",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "puppeteer_evaluate",
|
||||
"description": "Execute JavaScript in the browser console",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"enableWebSearch": false,
|
||||
"temperature": 0.7,
|
||||
"contextLength": 10,
|
||||
"systemPrompt": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user