实现响应结果的重排和对象输入框的实现

This commit is contained in:
锦恢 2025-04-25 03:46:29 +08:00
parent f14687b1d8
commit ad857e6544
26 changed files with 403 additions and 233 deletions

View File

@ -105,17 +105,3 @@ B <--mcp--> m(MCP Server)
``` ```
and just press f5, いただきます 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
```

View File

@ -15,7 +15,7 @@ import MainPanel from '@/components/main-panel/index.vue';
import { setDefaultCss } from './hook/css'; import { setDefaultCss } from './hook/css';
import { greenLog, pinkLog } from './views/setting/util'; import { greenLog, pinkLog } from './views/setting/util';
import { acquireVsCodeApi, useMessageBridge } from './api/message-bridge'; 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 { loadSetting } from './hook/setting';
import { loadPanels } from './hook/panel'; import { loadPanels } from './hook/panel';
@ -29,7 +29,7 @@ bridge.addCommandListener('hello', data => {
function initDebug() { 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'; connectionMethods.current = 'STDIO';
setTimeout(async () => { setTimeout(async () => {

View 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>

View File

@ -1,14 +1,27 @@
import { ToolItem } from "@/hook/type"; import { ToolItem } from "@/hook/type";
import { ref } from "vue"; import { Ref, ref } from "vue";
import type { OpenAI } from 'openai'; import type { OpenAI } from 'openai';
type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; 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 { export interface IExtraInfo {
created: number, created: number,
state: MessageState,
serverName: string, serverName: string,
usage?: ChatCompletionChunk['usage']; usage?: ChatCompletionChunk['usage'];
[key: string]: any [key: string]: any;
} }
export interface ChatMessage { export interface ChatMessage {
@ -53,6 +66,15 @@ export interface ToolCall {
export const allTools = ref<ToolItem[]>([]); 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[]) { export function getToolSchema(enableTools: EnableToolItem[]) {
const toolsSchema = []; const toolsSchema = [];
for (let i = 0; i < enableTools.length; i++) { for (let i = 0; i < enableTools.length; i++) {

View File

@ -78,11 +78,11 @@
</template> </template>
<script setup lang="ts"> <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 { useI18n } from 'vue-i18n';
import { ElMessage, ScrollbarInstance } from 'element-plus'; import { ElMessage, ScrollbarInstance } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ChatMessage, ChatStorage, IExtraInfo, ToolCall } from './chat'; import { ChatMessage, ChatStorage, IRenderMessage, MessageState, ToolCall } from './chat';
import Setting from './setting.vue'; import Setting from './setting.vue';
@ -125,16 +125,6 @@ if (!tabStorage.messages) {
tabStorage.messages = [] as ChatMessage[]; 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 renderMessages = computed(() => {
const messages: IRenderMessage[] = []; const messages: IRenderMessage[] = [];
for (const message of tabStorage.messages) { for (const message of tabStorage.messages) {
@ -142,8 +132,7 @@ const renderMessages = computed(() => {
messages.push({ messages.push({
role: 'user', role: 'user',
content: message.content, content: message.content,
extraInfo: message.extraInfo, extraInfo: message.extraInfo
isLast: false
}); });
} else if (message.role === 'assistant') { } else if (message.role === 'assistant') {
if (message.tool_calls) { if (message.tool_calls) {
@ -152,15 +141,16 @@ const renderMessages = computed(() => {
content: message.content, content: message.content,
tool_calls: message.tool_calls, tool_calls: message.tool_calls,
showJson: ref(false), showJson: ref(false),
extraInfo: message.extraInfo, extraInfo: {
isLast: false ...message.extraInfo,
state: MessageState.Unknown
}
}); });
} else { } else {
messages.push({ messages.push({
role: 'assistant/content', role: 'assistant/content',
content: message.content, content: message.content,
extraInfo: message.extraInfo, extraInfo: message.extraInfo
isLast: false
}); });
} }
@ -169,16 +159,12 @@ const renderMessages = computed(() => {
const lastAssistantMessage = messages[messages.length - 1]; const lastAssistantMessage = messages[messages.length - 1];
if (lastAssistantMessage.role === 'assistant/tool_calls') { if (lastAssistantMessage.role === 'assistant/tool_calls') {
lastAssistantMessage.toolResult = message.content; lastAssistantMessage.toolResult = message.content;
lastAssistantMessage.extraInfo.state = message.extraInfo.state;
lastAssistantMessage.extraInfo.usage = lastAssistantMessage.extraInfo.usage || message.extraInfo.usage; lastAssistantMessage.extraInfo.usage = lastAssistantMessage.extraInfo.usage || message.extraInfo.usage;
} }
} }
} }
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
lastMessage.isLast = true;
}
return messages; return messages;
}); });
@ -264,26 +250,30 @@ const handleSend = () => {
loop = new TaskLoop(streamingContent, streamingToolCalls); loop = new TaskLoop(streamingContent, streamingToolCalls);
loop.registerOnError((msg) => { loop.registerOnError((error) => {
ElMessage({ ElMessage({
message: msg, message: error.msg,
type: 'error', type: 'error',
duration: 3000 duration: 3000
}); });
tabStorage.messages.push({ if (error.state === MessageState.ReceiveChunkError) {
role: 'assistant', tabStorage.messages.push({
content: `错误: ${msg}`, role: 'assistant',
extraInfo: { content: error.msg,
created: Date.now(), extraInfo: {
serverName: llms[llmManager.currentModelIndex].id || 'unknown' created: Date.now(),
} state: error.state,
}); serverName: llms[llmManager.currentModelIndex].id || 'unknown'
}
});
}
isLoading.value = false; isLoading.value = false;
}); });
loop.registerOnChunk((chunk) => { loop.registerOnChunk(() => {
scrollToBottom(); scrollToBottom();
}); });

View File

@ -49,9 +49,6 @@ const usageStatistic = computed(() => {
return makeUsageStatistic(props.message.extraInfo); return makeUsageStatistic(props.message.extraInfo);
}); });
console.log(props.message);
console.log(usageStatistic);
const showTime = ref(false); const showTime = ref(false);
</script> </script>

View File

@ -1,16 +1,16 @@
<template> <template>
<div class="message-role"> <div class="message-role">
Agent 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 class="tool-loading iconfont icon-double-loading">
</span> </span>
</span> </span>
</div> </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> <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"> <el-collapse-item name="tool">
<template #title> <template #title>
@ -35,28 +35,46 @@
<!-- 工具调用结果 --> <!-- 工具调用结果 -->
<div v-if="props.message.toolResult"> <div v-if="props.message.toolResult">
<div class="tool-call-header"> <div class="tool-call-header">
<span class="tool-name">{{ "响应" }}</span> <span class="tool-name" :class="{ 'error': !isValidJson }">
<span style="width: 200px;" class="tools-dialog-container"> {{ 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" <el-switch v-model="props.message.showJson!.value" inline-prompt active-text="JSON"
inactive-text="Text" style="margin-left: 10px; width: 200px;" inactive-text="Text" style="margin-left: 10px; width: 200px;"
:inactive-action-style="'backgroundColor: var(--sidebar)'" /> :inactive-action-style="'backgroundColor: var(--sidebar)'" />
</span> </span>
</div> </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 v-if="props.message.showJson!.value" class="tool-result-content">
<div class="inner"> <div class="inner">
<div v-html="jsonResultToHtml(props.message.toolResult)"></div> <div v-html="jsonResultToHtml(props.message.toolResult)"></div>
</div> </div>
</div> </div>
<span v-else> <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%"> <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> <div v-else class="tool-other">{{ JSON.stringify(item) }}</div>
</el-scrollbar> </el-scrollbar>
</div> </div>
</span> </span>
</div> </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>
</div> </div>
</el-collapse-item> </el-collapse-item>
@ -66,19 +84,21 @@
</template> </template>
<script setup lang="ts"> <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 MessageMeta from './message-meta.vue';
import { markdownToHtml } from '../markdown'; import { markdownToHtml } from '../markdown';
import { createTest } from '@/views/setting/llm'; import { createTest } from '@/views/setting/llm';
import { IRenderMessage, MessageState } from '../chat';
const props = defineProps({ const props = defineProps({
message: { message: {
type: Object, type: Object as PropType<IRenderMessage>,
required: true required: true
} }
}); });
const activeNames = ref<string[]>(props.message.toolResult ? [''] : ['tool']); const activeNames = ref<string[]>(props.message.toolResult ? [''] : ['tool']);
watch( watch(
@ -98,14 +118,15 @@ const jsonResultToHtml = (jsonString: string) => {
return html; return html;
}; };
const isValidJSON = (str: string) => {
const isValidJson = computed(() => {
try { try {
JSON.parse(str); JSON.parse(props.message.toolResult || '');
return true; return true;
} catch { } catch {
return false; return false;
} }
}; });
</script> </script>
@ -130,7 +151,7 @@ const isValidJSON = (str: string) => {
.tool-call-header { .tool-call-header {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 5px; margin-top: 10px;
} }
.tool-name { .tool-name {
@ -143,6 +164,10 @@ const isValidJSON = (str: string) => {
height: 26px; height: 26px;
} }
.tool-name.error {
color: var(--el-color-error);
}
.tool-type { .tool-type {
font-size: 0.8em; font-size: 0.8em;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
@ -155,6 +180,10 @@ const isValidJSON = (str: string) => {
height: 22px; height: 22px;
} }
.response-item {
margin-bottom: 10px;
}
.tool-arguments { .tool-arguments {
margin: 0; margin: 0;
padding: 8px; padding: 8px;
@ -170,6 +199,10 @@ const isValidJSON = (str: string) => {
border-radius: 4px; border-radius: 4px;
} }
.tool-result.error {
background-color: rgba(245, 108, 108, 0.5);
}
.tool-text { .tool-text {
white-space: pre-wrap; white-space: pre-wrap;
line-height: 1.6; line-height: 1.6;

View File

@ -1,6 +1,6 @@
/* eslint-disable */ /* eslint-disable */
import { Ref } from "vue"; import { Ref } from "vue";
import { ToolCall, ChatStorage, getToolSchema } from "./chat"; import { ToolCall, ChatStorage, getToolSchema, MessageState } from "./chat";
import { useMessageBridge } from "@/api/message-bridge"; import { useMessageBridge } from "@/api/message-bridge";
import type { OpenAI } from 'openai'; import type { OpenAI } from 'openai';
import { callTool } from "../tool/tools"; import { callTool } from "../tool/tools";
@ -12,6 +12,11 @@ interface TaskLoopOptions {
maxEpochs: number; maxEpochs: number;
} }
interface IErrorMssage {
state: MessageState,
msg: string
}
/** /**
* @description * @description
*/ */
@ -23,7 +28,7 @@ export class TaskLoop {
constructor( constructor(
private readonly streamingContent: Ref<string>, private readonly streamingContent: Ref<string>,
private readonly streamingToolCalls: Ref<ToolCall[]>, private readonly streamingToolCalls: Ref<ToolCall[]>,
private onError: (msg: string) => void = (msg) => {}, private onError: (error: IErrorMssage) => void = (msg) => {},
private onChunk: (chunk: ChatCompletionChunk) => void = (chunk) => {}, private onChunk: (chunk: ChatCompletionChunk) => void = (chunk) => {},
private onDone: () => void = () => {}, private onDone: () => void = () => {},
private onEpoch: () => void = () => {}, private onEpoch: () => void = () => {},
@ -42,15 +47,33 @@ export class TaskLoop {
if (!toolResponse.isError) { if (!toolResponse.isError) {
const content = JSON.stringify(toolResponse.content); const content = JSON.stringify(toolResponse.content);
return content; return {
content,
state: MessageState.Success,
};
} else { } else {
this.onError(`工具调用失败: ${toolResponse.content}`); this.onError({
state: MessageState.ToolCall,
msg: `工具调用失败: ${toolResponse.content}`
});
console.error(toolResponse.content); console.error(toolResponse.content);
return {
content: toolResponse.content.toString(),
state: MessageState.ToolCall
}
} }
} catch (error) { } catch (error) {
this.onError(`工具调用失败: ${(error as Error).message}`); this.onError({
state: MessageState.ToolCall,
msg: `工具调用失败: ${(error as Error).message}`
});
console.error(error); 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) => { return new Promise<void>((resolve, reject) => {
const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => { const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => {
if (data.code !== 200) { if (data.code !== 200) {
this.onError(data.msg || '请求模型服务时发生错误'); this.onError({
state: MessageState.ReceiveChunkError,
msg: data.msg || '请求模型服务时发生错误'
});
resolve(); resolve();
return; return;
} }
@ -182,7 +208,7 @@ export class TaskLoop {
this.streamingToolCalls.value = []; this.streamingToolCalls.value = [];
} }
public registerOnError(handler: (msg: string) => void) { public registerOnError(handler: (msg: IErrorMssage) => void) {
this.onError = handler; this.onError = handler;
} }
@ -208,6 +234,7 @@ export class TaskLoop {
content: userMessage, content: userMessage,
extraInfo: { extraInfo: {
created: Date.now(), created: Date.now(),
state: MessageState.Success,
serverName: llms[llmManager.currentModelIndex].id || 'unknown' serverName: llms[llmManager.currentModelIndex].id || 'unknown'
} }
}); });
@ -238,6 +265,7 @@ export class TaskLoop {
tool_calls: this.streamingToolCalls.value, tool_calls: this.streamingToolCalls.value,
extraInfo: { extraInfo: {
created: Date.now(), created: Date.now(),
state: MessageState.Success,
serverName: llms[llmManager.currentModelIndex].id || 'unknown' serverName: llms[llmManager.currentModelIndex].id || 'unknown'
} }
}); });
@ -249,9 +277,10 @@ export class TaskLoop {
tabStorage.messages.push({ tabStorage.messages.push({
role: 'tool', role: 'tool',
tool_call_id: toolCall.id || toolCall.function.name, tool_call_id: toolCall.id || toolCall.function.name,
content: toolCallResult, content: toolCallResult.content,
extraInfo: { extraInfo: {
created: Date.now(), created: Date.now(),
state: toolCallResult.state,
serverName: llms[llmManager.currentModelIndex].id || 'unknown', serverName: llms[llmManager.currentModelIndex].id || 'unknown',
usage: this.completionUsage usage: this.completionUsage
} }
@ -264,6 +293,7 @@ export class TaskLoop {
content: this.streamingContent.value, content: this.streamingContent.value,
extraInfo: { extraInfo: {
created: Date.now(), created: Date.now(),
state: MessageState.Success,
serverName: llms[llmManager.currentModelIndex].id || 'unknown', serverName: llms[llmManager.currentModelIndex].id || 'unknown',
usage: this.completionUsage usage: this.completionUsage
} }

View File

@ -20,7 +20,7 @@ export function makeUsageStatistic(extraInfo: IExtraInfo): UsageStatistic | unde
input: usage.prompt_tokens, input: usage.prompt_tokens,
output: usage.completion_tokens, output: usage.completion_tokens,
total: usage.prompt_tokens + 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': case 'openai':

View File

@ -16,7 +16,7 @@
v-if="property.type === 'string'" v-if="property.type === 'string'"
v-model="tabStorage.formData[name]" v-model="tabStorage.formData[name]"
type="text" type="text"
:placeholder="t('enter') + ' ' + (property.title || name)" :placeholder="property.description || t('enter') + ' ' + (property.title || name)"
@keydown.enter.prevent="handleExecute" @keydown.enter.prevent="handleExecute"
/> />
@ -24,14 +24,23 @@
v-else-if="property.type === 'number' || property.type === 'integer'" v-else-if="property.type === 'number' || property.type === 'integer'"
v-model="tabStorage.formData[name]" v-model="tabStorage.formData[name]"
controls-position="right" controls-position="right"
:placeholder="t('enter') + ' ' + (property.title || name)" :placeholder="property.description || t('enter') + ' ' + (property.title || name)"
@keydown.enter.prevent="handleExecute" @keydown.enter.prevent="handleExecute"
/> />
<el-switch <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]" 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> </el-form-item>
</template> </template>
@ -53,9 +62,10 @@ import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { callTool, toolsManager, ToolStorage } from './tools'; import { callTool, toolsManager, ToolStorage } from './tools';
import { pinkLog } from '@/views/setting/util';
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp'; import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
import KInputObject from '@/components/k-input-object/index.vue';
defineComponent({ name: 'tool-executor' }); defineComponent({ name: 'tool-executor' });
const { t } = useI18n(); const { t } = useI18n();
@ -74,6 +84,9 @@ if (!tabStorage.formData) {
tabStorage.formData = {}; tabStorage.formData = {};
} }
console.log(tabStorage.formData);
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const loading = ref(false); const loading = ref(false);
@ -81,6 +94,7 @@ const currentTool = computed(() => {
return toolsManager.tools.find(tool => tool.name === tabStorage.currentToolName); return toolsManager.tools.find(tool => tool.name === tabStorage.currentToolName);
}); });
const formRules = computed<FormRules>(() => { const formRules = computed<FormRules>(() => {
const rules: FormRules = {}; const rules: FormRules = {};
if (!currentTool.value?.inputSchema?.properties) return rules; if (!currentTool.value?.inputSchema?.properties) return rules;
@ -108,7 +122,9 @@ const initFormData = () => {
if (!currentTool.value?.inputSchema?.properties) return; 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]) => { Object.entries(currentTool.value.inputSchema.properties).forEach(([name, property]) => {
newSchemaDataForm[name] = getDefaultValue(property); newSchemaDataForm[name] = getDefaultValue(property);
@ -145,4 +161,21 @@ watch(() => tabStorage.currentToolName, () => {
border-radius: .5em; border-radius: .5em;
margin-bottom: 15px; 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> </style>

View File

@ -12,7 +12,7 @@ export const toolsManager = reactive<{
export interface ToolStorage { export interface ToolStorage {
currentToolName: string; currentToolName: string;
lastToolCallResponse?: ToolCallResponse; lastToolCallResponse?: ToolCallResponse;
formData: Record<string, number | string | boolean>; formData: Record<string, any>;
} }
const bridge = useMessageBridge(); const bridge = useMessageBridge();

View File

@ -1,5 +1,3 @@
import { SchemaProperty } from "./type";
interface TypeAble { interface TypeAble {
type: string; type: string;
} }
@ -9,6 +7,8 @@ export function getDefaultValue(property: TypeAble) {
return 0; return 0;
} else if (property.type === 'boolean') { } else if (property.type === 'boolean') {
return false; return false;
} else if (property.type === 'object') {
return {};
} else { } else {
return ''; return '';
} }
@ -19,7 +19,7 @@ export function normaliseJavascriptType(type: string) {
case 'integer': case 'integer':
return 'number'; return 'number';
case 'number': case 'number':
return 'integer'; return 'number';
case 'boolean': case 'boolean':
return 'boolean'; return 'boolean';
case 'string': case 'string':

View File

@ -2,6 +2,7 @@
export interface SchemaProperty { export interface SchemaProperty {
title: string; title: string;
type: string; type: string;
description?: string;
} }
export interface InputSchema { export interface InputSchema {

View File

@ -144,5 +144,6 @@
"single-dialog": "محادثة من جولة واحدة", "single-dialog": "محادثة من جولة واحدة",
"multi-dialog": "محادثة متعددة الجولات", "multi-dialog": "محادثة متعددة الجولات",
"press-and-run": "اكتب سؤالاً لبدء الاختبار", "press-and-run": "اكتب سؤالاً لبدء الاختبار",
"connect-sigature": "توقيع الاتصال" "connect-sigature": "توقيع الاتصال",
"finish-refresh": "تم التحديث"
} }

View File

@ -144,5 +144,6 @@
"single-dialog": "Einzelrunden-Dialog", "single-dialog": "Einzelrunden-Dialog",
"multi-dialog": "Mehrrundengespräch", "multi-dialog": "Mehrrundengespräch",
"press-and-run": "Geben Sie eine Frage ein, um den Test zu starten", "press-and-run": "Geben Sie eine Frage ein, um den Test zu starten",
"connect-sigature": "Verbindungssignatur" "connect-sigature": "Verbindungssignatur",
"finish-refresh": "Aktualisierung abgeschlossen"
} }

View File

@ -144,5 +144,6 @@
"single-dialog": "Single-round dialogue", "single-dialog": "Single-round dialogue",
"multi-dialog": "Multi-turn conversation", "multi-dialog": "Multi-turn conversation",
"press-and-run": "Type a question to start the test", "press-and-run": "Type a question to start the test",
"connect-sigature": "Connection signature" "connect-sigature": "Connection signature",
"finish-refresh": "Refresh completed"
} }

View File

@ -144,5 +144,6 @@
"single-dialog": "Dialogue en un tour", "single-dialog": "Dialogue en un tour",
"multi-dialog": "Conversation multi-tours", "multi-dialog": "Conversation multi-tours",
"press-and-run": "Tapez une question pour commencer le test", "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"
} }

View File

@ -144,5 +144,6 @@
"single-dialog": "単一ラウンドの対話", "single-dialog": "単一ラウンドの対話",
"multi-dialog": "マルチターン会話", "multi-dialog": "マルチターン会話",
"press-and-run": "テストを開始するには質問を入力してください", "press-and-run": "テストを開始するには質問を入力してください",
"connect-sigature": "接続署名" "connect-sigature": "接続署名",
"finish-refresh": "更新が完了しました"
} }

View File

@ -144,5 +144,6 @@
"single-dialog": "단일 라운드 대화", "single-dialog": "단일 라운드 대화",
"multi-dialog": "다중 턴 대화", "multi-dialog": "다중 턴 대화",
"press-and-run": "테스트를 시작하려면 질문을 입력하세요", "press-and-run": "테스트를 시작하려면 질문을 입력하세요",
"connect-sigature": "연결 서명" "connect-sigature": "연결 서명",
"finish-refresh": "새로 고침 완료"
} }

View File

@ -144,5 +144,6 @@
"single-dialog": "Однораундовый диалог", "single-dialog": "Однораундовый диалог",
"multi-dialog": "Многораундовый разговор", "multi-dialog": "Многораундовый разговор",
"press-and-run": "Введите вопрос, чтобы начать тест", "press-and-run": "Введите вопрос, чтобы начать тест",
"connect-sigature": "Подпись соединения" "connect-sigature": "Подпись соединения",
"finish-refresh": "Обновление завершено"
} }

View File

@ -144,5 +144,6 @@
"single-dialog": "单轮对话", "single-dialog": "单轮对话",
"multi-dialog": "多轮对话", "multi-dialog": "多轮对话",
"press-and-run": "键入问题以开始测试", "press-and-run": "键入问题以开始测试",
"connect-sigature": "连接签名" "connect-sigature": "连接签名",
"finish-refresh": "完成刷新"
} }

View File

@ -144,5 +144,6 @@
"single-dialog": "單輪對話", "single-dialog": "單輪對話",
"multi-dialog": "多輪對話", "multi-dialog": "多輪對話",
"press-and-run": "輸入問題以開始測試", "press-and-run": "輸入問題以開始測試",
"connect-sigature": "連接簽名" "connect-sigature": "連接簽名",
"finish-refresh": "刷新完成"
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="connection-option"> <div class="connection-option">
<span>{{ t('log') }}</span> <span>{{ t('log') }}</span>
<el-scrollbar height="100%"> <el-scrollbar height="90%">
<div class="output-content"> <div class="output-content">
<div v-for="(log, index) in connectionResult.logString" :key="index" :class="log.type"> <div v-for="(log, index) in connectionResult.logString" :key="index" :class="log.type">
<span class="log-message">{{ log.message }}</span> <span class="log-message">{{ log.message }}</span>
@ -24,7 +24,7 @@ const { t } = useI18n();
<style> <style>
.connection-option { .connection-option {
height: 100%; height: 90%;
} }
.connection-option .el-scrollbar__view { .connection-option .el-scrollbar__view {

View File

@ -1,5 +1,6 @@
<template> <template>
<div class="connection-container"> <el-scrollbar>
<div class="connection-container">
<div class="connect-panel-container"> <div class="connect-panel-container">
<ConnectionMethod></ConnectionMethod> <ConnectionMethod></ConnectionMethod>
<ConnectionArgs></ConnectionArgs> <ConnectionArgs></ConnectionArgs>
@ -18,6 +19,7 @@
<ConnectionLog></ConnectionLog> <ConnectionLog></ConnectionLog>
</div> </div>
</div> </div>
</el-scrollbar>
</template> </template>

2
service/.gitignore vendored
View File

@ -23,3 +23,5 @@ pnpm-debug.log*
*.sw? *.sw?
config.json config.json
setting.json setting.json
tabs.example-servers/puppeteer.json

View File

@ -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": ""
}
}
}
]
}