修复工具调用的错误

This commit is contained in:
锦恢 2025-04-25 19:16:48 +08:00
parent ad857e6544
commit f925da7d7d
39 changed files with 414 additions and 230 deletions

View File

@ -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, doConnect, launchConnect } from './views/connect/connection';
import { connectionArgs, connectionMethods, doConnect, launchConnect, loadEnvVar } from './views/connect/connection';
import { loadSetting } from './hook/setting';
import { loadPanels } from './hook/panel';
@ -29,13 +29,16 @@ bridge.addCommandListener('hello', data => {
function initDebug() {
connectionArgs.commandString = 'node C:/Users/K/code/servers/src/puppeteer/dist/index.js';
connectionArgs.commandString = 'node /Users/bytedance/projects/mcp/servers/src/puppeteer/dist/index.js';
connectionMethods.current = 'STDIO';
setTimeout(async () => {
//
loadSetting();
//
loadEnvVar();
//
await doConnect();
@ -56,6 +59,9 @@ async function initProduce() {
//
loadSetting();
//
loadEnvVar();
//
await launchConnect();

View File

@ -93,18 +93,6 @@ export default defineComponent({
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();

View File

@ -1,4 +1,4 @@
import { ToolItem } from "@/hook/type";
import { ToolCallContent, ToolItem } from "@/hook/type";
import { Ref, ref } from "vue";
import type { OpenAI } from 'openai';
@ -24,8 +24,17 @@ export interface IExtraInfo {
[key: string]: any;
}
export interface ChatMessage {
role: 'user' | 'assistant' | 'system' | 'tool';
export interface ToolMessage {
role: 'tool';
content: ToolCallContent[];
tool_call_id?: string
name?: string // 工具名称,当 role 为 tool
tool_calls?: ToolCall[],
extraInfo: IExtraInfo
}
export interface TextMessage {
role: 'user' | 'assistant' | 'system';
content: string;
tool_call_id?: string
name?: string // 工具名称,当 role 为 tool
@ -33,6 +42,8 @@ export interface ChatMessage {
extraInfo: IExtraInfo
}
export type ChatMessage = ToolMessage | TextMessage;
// 新增状态和工具数据
interface EnableToolItem {
name: string;
@ -69,7 +80,7 @@ export const allTools = ref<ToolItem[]>([]);
export interface IRenderMessage {
role: 'user' | 'assistant/content' | 'assistant/tool_calls' | 'tool';
content: string;
toolResult?: string;
toolResult?: ToolCallContent[];
tool_calls?: ToolCall[];
showJson?: Ref<boolean>;
extraInfo: IExtraInfo;

View File

@ -5,9 +5,12 @@
<div v-for="(message, index) in renderMessages" :key="index"
:class="['message-item', message.role.split('/')[0]]"
>
<div class="message-avatar" v-if="message.role.startsWith('assistant')">
<div class="message-avatar" v-if="message.role === 'assistant/content'">
<span class="iconfont icon-robot"></span>
</div>
<div class="message-avatar" v-else-if="message.role === 'assistant/tool_calls'">
<span class="iconfont icon-tool"></span>
</div>
<!-- 用户输入的部分 -->
<div class="message-content" v-if="message.role === 'user'">
@ -27,22 +30,7 @@
<!-- 正在加载的部分实时解析 markdown -->
<div v-if="isLoading" class="message-item assistant">
<div class="message-avatar">
<span class="iconfont icon-chat"></span>
</div>
<div class="message-content">
<div class="message-role">
Agent
<span class="message-reminder">
正在生成答案
<span class="tool-loading iconfont icon-double-loading">
</span>
</span>
</div>
<div class="message-text">
<span v-html="waitingMarkdownToHtml(streamingContent)"></span>
</div>
</div>
<Message.StreamingBox :streaming-content="streamingContent" />
</div>
</div>
</el-scrollbar>
@ -84,26 +72,16 @@ import { ElMessage, ScrollbarInstance } from 'element-plus';
import { tabs } from '../panel';
import { ChatMessage, ChatStorage, IRenderMessage, MessageState, ToolCall } from './chat';
import Setting from './setting.vue';
// markdown.ts
import { markdownToHtml } from './markdown';
import { TaskLoop } from './task-loop';
import { llmManager, llms } from '@/views/setting/llm';
import * as Message from './message';
import Setting from './setting.vue';
defineComponent({ name: 'chat' });
const { t } = useI18n();
function waitingMarkdownToHtml(content: string) {
if (content) {
return markdownToHtml(content);
}
return '<span class="typing-cursor">|</span>';
}
const props = defineProps({
tabId: {
type: Number,
@ -358,6 +336,7 @@ onUnmounted(() => {
.message-avatar {
margin-right: 12px;
margin-top: 7px;
}
.message-content {

View File

@ -1,4 +1,5 @@
import Assistant from "./assistant.vue";
import Toolcall from "./toolcall.vue";
import User from "./user.vue";
export { Assistant, Toolcall, User };
import StreamingBox from "./streaming-box.vue";
export { Assistant, Toolcall, User, StreamingBox };

View File

@ -0,0 +1,41 @@
<template>
<div class="message-avatar">
<span class="iconfont icon-chat"></span>
</div>
<div class="message-content">
<div class="message-role">
Agent
<span class="message-reminder">
正在生成答案
<span class="tool-loading iconfont icon-double-loading">
</span>
</span>
</div>
<div class="message-text">
<span v-html="waitingMarkdownToHtml(streamingContent)"></span>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
import { markdownToHtml } from '../markdown';
const props = defineProps({
streamingContent: {
type: String,
required: true
}
});
function waitingMarkdownToHtml(content: string) {
if (content) {
return markdownToHtml(content);
}
return '<span class="typing-cursor">|</span>';
}
</script>
<style>
</style>

View File

@ -1,8 +1,7 @@
<template>
<div class="message-role">
Agent
<span class="message-reminder" v-if="!props.message.toolResult">
正在使用工具
Agent 正在使用工具
<span class="tool-loading iconfont icon-double-loading">
</span>
</span>
@ -16,7 +15,6 @@
<template #title>
<div class="tool-calls">
<div class="tool-call-header">
<span class="tool-type">{{ 'tool use' }}</span>
<span class="tool-name">{{ props.message.tool_calls[0].function.name }}</span>
<el-button size="small" @click="createTest(props.message.tool_calls[0])">
<span class="iconfont icon-send"></span>
@ -34,24 +32,30 @@
<!-- 工具调用结果 -->
<div v-if="props.message.toolResult">
<div class="tool-call-header">
<span class="tool-name" :class="{ 'error': !isValidJson }">
{{ isValidJson ? '响应': '错误' }}
<div class="tool-call-header result">
<span class="tool-name" :class="{ 'error': !isValid }">
{{ isValid ? '响应': '错误' }}
<el-button v-if="!isValid" size="small"
@click="gotoIssue()"
>
反馈
</el-button>
</span>
<span style="width: 200px;" class="tools-dialog-container" v-if="isValidJson">
<span style="width: 200px;" class="tools-dialog-container" v-if="isValid">
<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">
<div class="tool-result" v-if="isValid">
<div v-if="props.message.showJson!.value" class="tool-result-content">
<div class="inner">
<div v-html="jsonResultToHtml(props.message.toolResult)"></div>
<div v-html="toHtml(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 props.message.toolResult" :key="index"
class="response-item"
>
<el-scrollbar width="100%">
@ -68,19 +72,23 @@
</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 v-else class="tool-result" :class="{ 'error': !isValid }">
<div class="tool-result-content"
v-for="(error, index) of collectErrors"
:key="index"
>
{{ error }}
</div>
</div>
</div>
<MessageMeta :message="message" />
</div>
</el-collapse-item>
</el-collapse>
</div>
<MessageMeta :message="message" />
</template>
<script setup lang="ts">
@ -90,6 +98,7 @@ import MessageMeta from './message-meta.vue';
import { markdownToHtml } from '../markdown';
import { createTest } from '@/views/setting/llm';
import { IRenderMessage, MessageState } from '../chat';
import { ToolCallContent } from '@/hook/type';
const props = defineProps({
message: {
@ -112,22 +121,54 @@ watch(
}
);
const jsonResultToHtml = (jsonString: string) => {
const formattedJson = JSON.stringify(JSON.parse(jsonString), null, 2);
const toHtml = (toolResult: ToolCallContent[]) => {
const formattedJson = JSON.stringify(toolResult, null, 2);
const html = markdownToHtml('```json\n' + formattedJson + '\n```');
return html;
};
const isValidJson = computed(() => {
const jsonResultToHtml = (jsonResult: string) => {
try {
JSON.parse(props.message.toolResult || '');
const formattedJson = JSON.stringify(JSON.parse(jsonResult), null, 2);
const html = markdownToHtml('```json\n' + formattedJson + '\n```');
return html;
} catch (error) {
const html = markdownToHtml('```json\n' + jsonResult + '\n```');
return html;
}
}
function gotoIssue() {
window.open('https://github.com/LSTM-Kirigaya/openmcp-client/issues', '_blank');
}
const isValid = computed(() => {
try {
const item = props.message.toolResult![0];
if (item.type === 'error') {
return false;
}
return true;
} catch {
return false;
}
});
const collectErrors = computed(() => {
const errorMessages = [];
try {
const errorResults = props.message.toolResult!.filter(item => item.type === 'error');
console.log(errorResults);
for (const errorResult of errorResults) {
errorMessages.push(errorResult.text);
}
return errorMessages;
} catch {
return errorMessages;
}
});
</script>
<style>
@ -140,10 +181,17 @@ const isValidJson = computed(() => {
border-left: 3px solid var(--el-color-error);
}
.tool-calls {
margin-top: 10px;
.message-text .el-collapse-item__header {
display: flex;
align-items: center;
height: fit-content;
}
.message-text .el-collapse-item__content {
padding-bottom: 5px;
}
.tool-call-item {
margin-bottom: 10px;
}
@ -151,6 +199,9 @@ const isValidJson = computed(() => {
.tool-call-header {
display: flex;
align-items: center;
}
.tool-call-header.result {
margin-top: 10px;
}

View File

@ -18,7 +18,7 @@
<el-tooltip :content="t('tool-use')" placement="top">
<div class="setting-button" :class="{ 'active': availableToolsNum > 0 }" size="small"
@click="toggleTools">
<span class="iconfont icon-tool badge-outer" v-if="availableToolsNum > 0">
<span class="iconfont icon-tool badge-outer">
<span class="badge-inner">
{{ availableToolsNum }}
</span>

View File

@ -46,20 +46,15 @@ export class TaskLoop {
const toolResponse = await callTool(toolName, toolArgs);
if (!toolResponse.isError) {
const content = JSON.stringify(toolResponse.content);
// const content = JSON.stringify(toolResponse.content);
return {
content,
state: MessageState.Success,
content: toolResponse.content,
state: MessageState.Success
};
} else {
this.onError({
state: MessageState.ToolCall,
msg: `工具调用失败: ${toolResponse.content}`
});
console.error(toolResponse.content);
return {
content: toolResponse.content.toString(),
content: toolResponse.content,
state: MessageState.ToolCall
}
}
@ -70,13 +65,27 @@ export class TaskLoop {
msg: `工具调用失败: ${(error as Error).message}`
});
console.error(error);
return {
content: (error as Error).message,
content: [{
type: 'error',
text: this.parseErrorObject(error)
}],
state: MessageState.ToolCall
}
}
}
private parseErrorObject(error: any): string {
if (typeof error === 'string') {
return error;
} else if (typeof error === 'object') {
return JSON.stringify(error, null, 2);
} else {
return error.toString();
}
}
private handleChunkDeltaContent(chunk: ChatCompletionChunk) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
@ -271,7 +280,11 @@ export class TaskLoop {
});
const toolCallResult = await this.handleToolCalls(this.streamingToolCalls.value);
if (toolCallResult) {
console.log(toolCallResult);
if (toolCallResult.content) {
const toolCall = this.streamingToolCalls.value[0];
tabStorage.messages.push({

View File

@ -12,7 +12,7 @@ export const promptsManager = reactive<{
export interface PromptStorage {
currentPromptName: string;
lastPromptGetResponse?: PromptsGetResponse;
formData: Record<string, number | boolean | string>;
formData: Record<string, any>;
}
export function parsePromptTemplate(template: string): {

View File

@ -13,7 +13,7 @@ export const resourcesManager = reactive<{
export interface ResourceStorage {
currentResourceName: string;
lastResourceReadResponse?: ResourcesReadResponse;
formData: Record<string, number | string | boolean>;
formData: Record<string, any>;
}
/**

View File

@ -35,7 +35,6 @@
v-model="tabStorage.formData[name]"
/>
<k-input-object
v-else-if="property.type === 'object'"
v-model="tabStorage.formData[name]"

View File

@ -3,35 +3,34 @@
<span>
<span>{{ t('response') }}</span>
<span style="width: 200px;">
<el-switch
v-model="showRawJson"
inline-prompt
active-text="JSON"
inactive-text="Text"
<el-switch v-model="showRawJson" inline-prompt active-text="JSON" inactive-text="Text"
style="margin-left: 10px; width: 200px;"
:inactive-action-style="'backgroundColor: var(--sidebar)'"
/>
:inactive-action-style="'backgroundColor: var(--sidebar)'" />
</span>
</span>
<el-scrollbar height="500px">
<div
class="output-content"
contenteditable="false"
>
<template v-if="!showRawJson">
<template v-if="tabStorage.lastToolCallResponse?.isError">
<span style="color: var(--el-color-error)">
{{ tabStorage.lastToolCallResponse.content.map(c => c.text).join('\n') }}
<div class="output-content" contenteditable="false">
<!-- TODO: 更加稳定现在通过下面这个来判断上一次执行结果是否成功 -->
<div v-if="typeof tabStorage.lastToolCallResponse === 'string'" class="error-tool-call">
<span>
{{ tabStorage.lastToolCallResponse }}
</span>
</div>
<div v-else>
<!-- 展示原本的信息 -->
<template v-if="!showRawJson">
{{tabStorage.lastToolCallResponse?.content.map(c => c.text).join('\n')}}
</template>
<template v-else>
{{ tabStorage.lastToolCallResponse?.content.map(c => c.text).join('\n') }}
</template>
</template>
<!-- 展示 json -->
<template v-else>
{{ formattedJson }}
</template>
</div>
</div>
</el-scrollbar>
</div>
</template>
@ -60,17 +59,15 @@ const showRawJson = ref(false);
const formattedJson = computed(() => {
try {
if (typeof tabStorage.lastToolCallResponse === 'string') {
return tabStorage.lastToolCallResponse;
}
return JSON.stringify(tabStorage.lastToolCallResponse, null, 2);
} catch {
return 'Invalid JSON';
}
});
const jsonResultToHtml = (jsonString: string) => {
const formattedJson = JSON.stringify(JSON.parse(jsonString), null, 2);
const html = markdownToHtml('```json\n' + formattedJson + '\n```');
return html;
};
</script>
<style>
@ -93,7 +90,7 @@ const jsonResultToHtml = (jsonString: string) => {
background-color: var(--sidebar);
}
.tool-logger > span:first-child {
.tool-logger>span:first-child {
margin-bottom: 5px;
display: flex;
align-items: center;
@ -113,4 +110,10 @@ const jsonResultToHtml = (jsonString: string) => {
line-height: 1.5;
background-color: var(--sidebar);
}
.error-tool-call {
background-color: rgba(245, 108, 108, 0.5);
padding: 5px 9px;
border-radius: .5em;
}
</style>

View File

@ -11,7 +11,7 @@ export const toolsManager = reactive<{
export interface ToolStorage {
currentToolName: string;
lastToolCallResponse?: ToolCallResponse;
lastToolCallResponse?: ToolCallResponse | string;
formData: Record<string, any>;
}
@ -23,7 +23,7 @@ export function callTool(toolName: string, toolArgs: Record<string, any>) {
console.log(data.msg);
if (data.code !== 200) {
reject(new Error(data.msg + ''));
resolve(data.msg);
} else {
resolve(data.msg);
}

View File

@ -2,7 +2,7 @@ interface TypeAble {
type: string;
}
export function getDefaultValue(property: TypeAble) {
export function getDefaultValue(property: TypeAble): any {
if (property.type === 'number' || property.type === 'integer') {
return 0;
} else if (property.type === 'boolean') {

View File

@ -81,6 +81,7 @@ export interface PromptsGetResponse {
export interface ToolCallContent {
type: string;
text: string;
[key: string]: any;
}
export interface ToolCallResponse {

View File

@ -7,7 +7,7 @@
</span>
<p>
OpenMCP Client 0.0.4 OpenMCP@<a href="https://www.zhihu.com/people/can-meng-zhong-de-che-xian">锦恢</a> 开发
OpenMCP Client 0.0.5 OpenMCP@<a href="https://www.zhihu.com/people/can-meng-zhong-de-che-xian">锦恢</a> 开发
</p>
<p>

View File

@ -389,3 +389,74 @@ export function getServerVersion() {
export const envVarStatus = {
launched: false
};
function lookupEnvVar(varNames: string[]) {
const bridge = useMessageBridge();
return new Promise<string[] | undefined>((resolve, reject) => {
bridge.addCommandListener('lookup-env-var', data => {
const { code, msg } = data;
if (code === 200) {
connectionResult.logString.push({
type: 'info',
message: '预设环境变量同步完成'
});
resolve(msg);
} else {
connectionResult.logString.push({
type: 'error',
message: '预设环境变量同步失败: ' + msg
});
resolve(undefined);
}
}, { once: true });
console.log(varNames);
bridge.postMessage({
command: 'lookup-env-var',
data: {
keys: varNames
}
})
});
}
export async function handleEnvSwitch(enabled: boolean) {
const presetVars = ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'];
if (enabled) {
const values = await lookupEnvVar(presetVars);
if (values) {
// 将 key values 合并进 connectionEnv.data 中
// 若已有相同的 key, 则替换 value
for (let i = 0; i < presetVars.length; i++) {
const key = presetVars[i];
const value = values[i];
const sameNameItems = connectionEnv.data.filter(item => item.key === key);
if (sameNameItems.length > 0) {
const conflictItem = sameNameItems[0];
conflictItem.value = value;
} else {
connectionEnv.data.push({
key: key, value: value
});
}
}
}
} else {
// 清空 connectionEnv.data 中所有 key 为 presetVars 的项
const reserveItems = connectionEnv.data.filter(item => !presetVars.includes(item.key));
connectionEnv.data = reserveItems;
}
}
export async function loadEnvVar() {
return await handleEnvSwitch(true);
}

View File

@ -42,51 +42,13 @@
<script setup lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { defineComponent, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { connectionEnv, connectionResult, EnvItem, envVarStatus } from './connection';
import { useMessageBridge } from '@/api/message-bridge';
import { connectionEnv, EnvItem, handleEnvSwitch } from './connection';
defineComponent({ name: 'env-var' });
const { t } = useI18n();
const bridge = useMessageBridge();
function lookupEnvVar(varNames: string[]) {
console.log('enter');
return new Promise<string[] | undefined>((resolve, reject) => {
bridge.addCommandListener('lookup-env-var', data => {
const { code, msg } = data;
if (code === 200) {
connectionResult.logString.push({
type: 'info',
message: '预设环境变量同步完成'
});
resolve(msg);
} else {
connectionResult.logString.push({
type: 'error',
message: '预设环境变量同步失败: ' + msg
});
resolve(undefined);
}
}, { once: true });
console.log(varNames);
bridge.postMessage({
command: 'lookup-env-var',
data: {
keys: varNames
}
})
});
}
/**
* @description 添加环境变量
@ -126,46 +88,6 @@ function deleteEnvVar(option: EnvItem) {
const envEnabled = ref(true);
async function handleEnvSwitch(enabled: boolean) {
const presetVars = ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'];
if (enabled) {
const values = await lookupEnvVar(presetVars);
if (values) {
// key values connectionEnv.data
// key, value
for (let i = 0; i < presetVars.length; i++) {
const key = presetVars[i];
const value = values[i];
const sameNameItems = connectionEnv.data.filter(item => item.key === key);
if (sameNameItems.length > 0) {
const conflictItem = sameNameItems[0];
conflictItem.value = value;
} else {
connectionEnv.data.push({
key: key, value: value
});
}
}
}
} else {
// connectionEnv.data key presetVars
const reserveItems = connectionEnv.data.filter(item => !presetVars.includes(item.key));
connectionEnv.data = reserveItems;
}
}
onMounted(() => {
setTimeout(() => {
if (envVarStatus.launched) {
return;
}
handleEnvSwitch(envEnabled.value);
envVarStatus.launched = true;
}, 200);
});
</script>
<style>

1
service/.gitignore vendored
View File

@ -25,3 +25,4 @@ config.json
setting.json
tabs.example-servers/puppeteer.json
*.traineddata

View File

@ -20,6 +20,7 @@
"pako": "^2.1.0",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"tesseract.js": "^6.0.1",
"ws": "^8.18.1"
},
"devDependencies": {
@ -768,6 +769,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bmp-js": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
"integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==",
"license": "MIT"
},
"node_modules/body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@ -1598,6 +1605,12 @@
"node": ">=0.10.0"
}
},
"node_modules/idb-keyval": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
"integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==",
"license": "Apache-2.0"
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -1692,6 +1705,12 @@
"resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="
},
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
@ -2028,6 +2047,15 @@
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"license": "MIT",
"bin": {
"opencollective-postinstall": "index.js"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
@ -2262,6 +2290,12 @@
"node": ">= 12.13.0"
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@ -2604,6 +2638,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tesseract.js": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.1.tgz",
"integrity": "sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"bmp-js": "^0.1.0",
"idb-keyval": "^6.2.0",
"is-url": "^1.2.4",
"node-fetch": "^2.6.9",
"opencollective-postinstall": "^2.0.3",
"regenerator-runtime": "^0.13.3",
"tesseract.js-core": "^6.0.0",
"wasm-feature-detect": "^1.2.11",
"zlibjs": "^0.3.1"
}
},
"node_modules/tesseract.js-core": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz",
"integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==",
"license": "Apache-2.0"
},
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
@ -2820,6 +2878,12 @@
"node": ">= 0.8"
}
},
"node_modules/wasm-feature-detect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
"license": "Apache-2.0"
},
"node_modules/web-streams-polyfill": {
"version": "4.0.0-beta.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
@ -2905,6 +2969,15 @@
"node": ">=6"
}
},
"node_modules/zlibjs": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/zod": {
"version": "3.24.2",
"resolved": "https://registry.npmmirror.com/zod/-/zod-3.24.2.tgz",

View File

@ -41,6 +41,7 @@
"pako": "^2.1.0",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"tesseract.js": "^6.0.1",
"ws": "^8.18.1"
}
}

View File

@ -1,5 +1,5 @@
import { PostMessageble } from "../adapter";
import { MCPClient } from "./connect";
import { PostMessageble } from "../hook/adapter";
import { MCPClient } from "../hook/client";
export async function lookupEnvVarHandler(client: MCPClient | undefined, data: any, webview: PostMessageble) {

View File

@ -1,8 +1,8 @@
import { PostMessageble } from '../adapter';
import { connect, MCPClient, type MCPOptions } from './connect';
import { PostMessageble } from '../hook/adapter';
import { connect, MCPClient, type MCPOptions } from '../hook/client';
import { lookupEnvVarHandler } from './env-var';
import { callTool, getPrompt, getServerVersion, listPrompts, listResources, listResourceTemplates, listTools, readResource } from './handler';
import { callTool, getPrompt, getServerVersion, listPrompts, listResources, listResourceTemplates, listTools, readResource } from './mcp-server';
import { chatCompletionHandler } from './llm';
import { panelLoadHandler, panelSaveHandler } from './panel';
import { settingLoadHandler, settingSaveHandler } from './setting';

View File

@ -1,6 +1,6 @@
import { OpenAI } from 'openai';
import { MCPClient } from './connect';
import { PostMessageble } from '../adapter';
import { MCPClient } from '../hook/client';
import { PostMessageble } from '../hook/adapter';
let currentStream: AsyncIterable<any> | null = null;

View File

@ -1,5 +1,5 @@
import { PostMessageble } from "../adapter";
import { MCPClient } from "./connect";
import { PostMessageble } from "../hook/adapter";
import { MCPClient } from "../hook/client";
// ==================== 接口定义 ====================
export interface GetPromptOption {

View File

View File

@ -1,6 +1,6 @@
import { PostMessageble } from '../adapter';
import { loadConfig, loadTabSaveConfig, saveConfig, saveTabSaveConfig } from '../util';
import { MCPClient } from './connect';
import { PostMessageble } from '../hook/adapter';
import { loadConfig, loadTabSaveConfig, saveConfig, saveTabSaveConfig } from '../hook/setting';
import { MCPClient } from '../hook/client';
export async function panelSaveHandler(client: MCPClient | undefined, data: any, webview: PostMessageble) {
try {

View File

@ -1,6 +1,6 @@
import { PostMessageble } from '../adapter';
import { loadConfig, saveConfig } from '../util';
import { MCPClient } from './connect';
import { PostMessageble } from '../hook/adapter';
import { loadConfig, saveConfig } from '../hook/setting';
import { MCPClient } from '../hook/client';
export async function settingSaveHandler(client: MCPClient | undefined, data: any, webview: PostMessageble) {
try {

View File

@ -1,5 +1,5 @@
import { PostMessageble } from "../adapter";
import { MCPClient } from "./connect";
import { PostMessageble } from "../hook/adapter";
import { MCPClient } from "../hook/client";
export function ping(client: MCPClient | undefined, webview: PostMessageble) {
if (!client) {

22
service/src/hook/ocr.ts Normal file
View File

@ -0,0 +1,22 @@
import Tesseract from 'tesseract.js';
async function tesseractOCR(
imagePath: string,
logger: (message: Tesseract.LoggerMessage) => void,
lang: string = 'eng+chi_sim'
) {
try {
const { data: { text } } = await Tesseract.recognize(
imagePath,
lang,
{
logger
}
);
return text;
} catch (error) {
console.error('OCR error:', error);
}
return '无法识别图片';
}

View File

@ -2,7 +2,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { llms } from './llm';
import { IServerVersion } from './controller/connect';
import { IServerVersion } from './client';
export let VSCODE_WORKSPACE = '';

View File

@ -1,3 +1,12 @@
// server/src/types.ts
export interface IMessage {
type: string;
data: Record<string, unknown>;
timestamp?: number;
}
export type MessageHandler = (message: IMessage) => void;
// ==================== 基础类型定义 ====================
export interface SchemaProperty {
title: string;

View File

@ -1,5 +1,5 @@
export { messageController } from './controller';
export { VSCodeWebViewLike } from './adapter';
export { setVscodeWorkspace } from './util';
export { VSCodeWebViewLike } from './hook/adapter';
export { setVscodeWorkspace } from './hook/setting';
// TODO: 更加规范
export { client } from './controller';

View File

@ -1,8 +0,0 @@
// server/src/types.ts
export interface IMessage {
type: string;
data: Record<string, unknown>;
timestamp?: number;
}
export type MessageHandler = (message: IMessage) => void;

View File

@ -1,5 +1,5 @@
{
"currentIndex": 0,
"currentIndex": 1,
"tabs": [
{
"name": "交互测试",
@ -171,7 +171,7 @@
"storage": {
"currentToolName": "get_weather_by_city_code",
"formData": {
"city_code": 101210101
"city_code": 0
},
"lastToolCallResponse": {
"content": [