Compare commits
No commits in common. "d9928ecc5d5ae0a036e519d850a8320a9ada0631" and "af3b2cbee50561729c5c05526d23ec2cf95711b0" have entirely different histories.
d9928ecc5d
...
af3b2cbee5
@ -28,4 +28,3 @@ software/**
|
||||
.github
|
||||
webpack
|
||||
.openmcp
|
||||
.vscode
|
@ -1,13 +1,5 @@
|
||||
# Change Log
|
||||
|
||||
## [main] 0.1.9
|
||||
- 增加 mook 功能:可以利用随机种子或者AI生成来自动化填充测试 tool 的表单数据
|
||||
- 增加工具自检功能:openmcp 的 tool 下可以点击「工具模块」 右侧的 「工具自检」进入自检模式,该模式下,用户可以自己定义工具执行的拓扑顺序,然后一次性进行自动检测。
|
||||
- 修复 issue #44: 完成链接跳转的平台适配
|
||||
- 修复 issue #36: 完成非文件夹打开下的成功启动
|
||||
- 修复 issue #45: 数组类型参数不支持
|
||||
- 修复多行对话粘贴进入对话框样式异常的问题
|
||||
|
||||
## [main] 0.1.8
|
||||
- 增加 STDIO 下的热更新,现在用户修改 mcp 代码,openmcp 会自动完成一切相关功能的热更新,无需用户手动重启。
|
||||
- 完成 mcpconfig.json 的导出功能,导出的 配置文件 可以通过 openmcp-sdk 框架完成低代码 agent 部署;也可以直接载入 Claude Desktop 等等 MCP 客户端中,实现 MCP 的快速部署和使用。
|
||||
|
@ -15,6 +15,5 @@
|
||||
"join-project": "Participate in the project",
|
||||
"comment-plugin": "Comment Plugin",
|
||||
"preset-env-sync": "Preset environment variables synchronized successfully",
|
||||
"preset-env-sync.fail": "Failed to sync preset environment variables",
|
||||
"error.notOpenWorkspace": "No workspace is currently open in VSCode. Please open a workspace (e.g., open a folder) first."
|
||||
"preset-env-sync.fail": "Failed to sync preset environment variables"
|
||||
}
|
@ -15,7 +15,5 @@
|
||||
"join-project": "プロジェクトに参加する",
|
||||
"comment-plugin": "コメントプラグイン",
|
||||
"preset-env-sync": "プリセット環境変数の同期が完了しました",
|
||||
"preset-env-sync.fail": "プリセット環境変数の同期に失敗しました",
|
||||
"error.notOpenWorkspace": "現在、VSCode でワークスペースが開かれていません。まずワークスペース(例:フォルダーを開く)を開いてください。"
|
||||
|
||||
"preset-env-sync.fail": "プリセット環境変数の同期に失敗しました"
|
||||
}
|
@ -15,6 +15,5 @@
|
||||
"join-project": "参与项目",
|
||||
"comment-plugin": "评论插件",
|
||||
"preset-env-sync": "预设环境变量同步完成",
|
||||
"preset-env-sync.fail": "预设环境变量同步失败",
|
||||
"error.notOpenWorkspace": "当前VScode没有打开工作区,请先打开工作区(例如打开文件夹)"
|
||||
"preset-env-sync.fail": "预设环境变量同步失败"
|
||||
}
|
863
package-lock.json
generated
863
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,7 @@
|
||||
"name": "openmcp",
|
||||
"displayName": "OpenMCP",
|
||||
"description": "An all in one MCP Client/TestTool",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.8",
|
||||
"publisher": "kirigaya",
|
||||
"private": true,
|
||||
"author": {
|
||||
@ -224,7 +224,7 @@
|
||||
"setup": "yarn install && yarn prepare:ocr",
|
||||
"serve": "turbo serve",
|
||||
"build": "turbo build && tsc -p ./ && node esbuild.config.js",
|
||||
"build:plugin": "yarn build && tsc && vsce package --allow-package-all-secrets",
|
||||
"build:plugin": "yarn build && tsc && vsce package",
|
||||
"vscode:prepublish": "node esbuild.config.js",
|
||||
"compile": "tsc -p ./",
|
||||
"watch": "tsc -watch -p ./",
|
||||
|
@ -19,14 +19,10 @@
|
||||
"@codemirror/autocomplete": "^6.18.6",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@faker-js/faker": "^9.8.0",
|
||||
"chalk": "^5.4.1",
|
||||
"codemirror": "^6.0.1",
|
||||
"core-js": "^3.8.3",
|
||||
"d3": "^7.9.0",
|
||||
"element-plus": "^2.9.9",
|
||||
"elkjs": "^0.10.0",
|
||||
"json-schema-faker": "^0.5.9",
|
||||
"katex": "^0.16.21",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
@ -49,7 +45,6 @@
|
||||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||
"@rollup/pluginutils": "^5.1.4",
|
||||
"@tsconfig/node22": "^22.0.1",
|
||||
"@types/d3": "^7.4.3",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
|
@ -3,15 +3,6 @@
|
||||
--main-color: #CB81DA;
|
||||
--main-dark-color: #2D323B;
|
||||
--main-light-color: rgba(203, 129, 218, 0.7);
|
||||
--main-light-color-90: rgba(203, 129, 218, 0.9);
|
||||
--main-light-color-80: rgba(203, 129, 218, 0.8);
|
||||
--main-light-color-70: rgba(203, 129, 218, 0.7);
|
||||
--main-light-color-60: rgba(203, 129, 218, 0.6);
|
||||
--main-light-color-50: rgba(203, 129, 218, 0.5);
|
||||
--main-light-color-40: rgba(203, 129, 218, 0.4);
|
||||
--main-light-color-30: rgba(203, 129, 218, 0.3);
|
||||
--main-light-color-20: rgba(203, 129, 218, 0.2);
|
||||
--main-light-color-10: rgba(203, 129, 218, 0.1);
|
||||
--sidebar-width: 330px;
|
||||
--right-nav-width: 50px;
|
||||
--time-scale-height: 30px;
|
||||
|
@ -163,26 +163,24 @@ function getDefaultValue(property: any): string {
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
// 当前编辑器内容
|
||||
const currentContent = editorView.value?.state.doc.toString() ?? '';
|
||||
const newContent = JSON.stringify(newVal ?? {}, null, 2);
|
||||
|
||||
if (currentContent !== newContent && editorView.value) {
|
||||
editorView.value.dispatch({
|
||||
changes: {
|
||||
from: 0,
|
||||
to: editorView.value.state.doc.length,
|
||||
insert: newContent
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 修改 watch 逻辑
|
||||
// watch(
|
||||
// () => props.modelValue,
|
||||
// (newVal) => {
|
||||
// const currentParsed = tryParse(inputValue.value)
|
||||
// if (!isDeepEqual(currentParsed, newVal)) {
|
||||
// const newContent = JSON.stringify(newVal, null, 2)
|
||||
// editorView.value?.dispatch({
|
||||
// changes: {
|
||||
// from: 0,
|
||||
// to: editorView.value.state.doc.length,
|
||||
// insert: newContent
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// },
|
||||
// { deep: true }
|
||||
// )
|
||||
|
||||
// 辅助函数:尝试解析 JSON
|
||||
const tryParse = (value: string): any => {
|
||||
|
@ -58,7 +58,7 @@ export interface EnableToolItem {
|
||||
}
|
||||
|
||||
export interface ChatSetting {
|
||||
modelIndex?: number
|
||||
modelIndex: number
|
||||
systemPrompt: string
|
||||
enableTools: EnableToolItem[]
|
||||
temperature: number
|
||||
|
@ -56,7 +56,6 @@ import { llmManager, llms } from '@/views/setting/llm';
|
||||
import { mcpClientAdapter } from '@/views/connect/core';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
import { gotoWebsite } from '@/hook/util';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
@ -167,11 +166,11 @@ const exportCode = async () => {
|
||||
|
||||
const gotoHowtoUse = () => {
|
||||
if (locale.value === 'zh') {
|
||||
gotoWebsite('https://kirigaya.cn/openmcp/zh/sdk-tutorial/#%E4%BD%BF%E7%94%A8');
|
||||
window.open('https://kirigaya.cn/openmcp/zh/sdk-tutorial/#%E4%BD%BF%E7%94%A8');
|
||||
} else if (locale.value === 'ja') {
|
||||
gotoWebsite('https://kirigaya.cn/openmcp/ja/sdk-tutorial/#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95');
|
||||
window.open('https://kirigaya.cn/openmcp/ja/sdk-tutorial/#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95');
|
||||
} else {
|
||||
gotoWebsite('https://kirigaya.cn/openmcp/sdk-tutorial/#usage');
|
||||
window.open('https://kirigaya.cn/openmcp/sdk-tutorial/#usage');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,15 +64,4 @@ const onRadioGroupChange = () => {
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.setting-button:hover {
|
||||
background: var(--main-light-color, #f0f8ff);
|
||||
box-shadow: 0 2px 8px 0 rgba(64,158,255,0.08);
|
||||
border-color: var(--el-color-primary-light-7, #c6e2ff);
|
||||
}
|
||||
|
||||
.setting-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
<style></style>
|
@ -162,15 +162,16 @@ provide('tabStorage', tabStorage);
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.el-switch__core {
|
||||
.tools-dialog-container .el-switch__core {
|
||||
border: 1px solid var(--main-color) !important;
|
||||
}
|
||||
|
||||
.el-switch .el-switch__action {
|
||||
|
||||
.tools-dialog-container .el-switch .el-switch__action {
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
|
||||
.el-switch.is-checked .el-switch__action {
|
||||
.tools-dialog-container .el-switch.is-checked .el-switch__action {
|
||||
background-color: var(--sidebar);
|
||||
}
|
||||
|
||||
|
@ -234,7 +234,6 @@ function handleCompositionEnd() {
|
||||
.rich-editor {
|
||||
min-height: 100px;
|
||||
outline: none;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.rich-editor:empty::before {
|
||||
|
@ -48,7 +48,6 @@ export class TaskLoop {
|
||||
private bridge: MessageBridge;
|
||||
private streamingContent: Ref<string>;
|
||||
private streamingToolCalls: Ref<ToolCall[]>;
|
||||
private aborted = false;
|
||||
|
||||
private currentChatId = '';
|
||||
private onError: (error: IErrorMssage) => void = (msg) => { };
|
||||
@ -319,7 +318,6 @@ export class TaskLoop {
|
||||
});
|
||||
this.streamingContent.value = '';
|
||||
this.streamingToolCalls.value = [];
|
||||
this.aborted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -547,7 +545,6 @@ export class TaskLoop {
|
||||
maxEpochs = 50,
|
||||
verbose = 0
|
||||
} = this.taskOptions || {};
|
||||
this.aborted = false;
|
||||
|
||||
for (let i = 0; i < maxEpochs; ++i) {
|
||||
|
||||
@ -573,12 +570,6 @@ export class TaskLoop {
|
||||
// 发送请求
|
||||
const doConverationResult = await this.doConversation(chatData, toolcallIndexAdapter);
|
||||
|
||||
// 如果在调用过程中出发了 abort,则直接中断
|
||||
if (this.aborted) {
|
||||
this.aborted = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果存在需要调度的工具
|
||||
if (this.streamingToolCalls.value.length > 0) {
|
||||
|
||||
@ -606,19 +597,8 @@ export class TaskLoop {
|
||||
|
||||
// ready to call tools
|
||||
toolCall = this.consumeToolCalls(toolCall);
|
||||
|
||||
if (this.aborted) {
|
||||
this.aborted = false;
|
||||
break;
|
||||
}
|
||||
|
||||
let toolCallResult = await handleToolCalls(toolCall);
|
||||
|
||||
if (this.aborted) {
|
||||
this.aborted = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// hook : finish call tools
|
||||
toolCallResult = this.consumeToolCalleds(toolCallResult);
|
||||
|
||||
@ -676,11 +656,6 @@ export class TaskLoop {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.aborted) {
|
||||
this.aborted = false;
|
||||
break;
|
||||
}
|
||||
|
||||
} else if (this.streamingContent.value) {
|
||||
tabStorage.messages.push({
|
||||
role: 'assistant',
|
||||
|
@ -318,9 +318,6 @@ function updateToolCallResultItem(value: any, toolIndex: number, index: number)
|
||||
.tool-call-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tool-call-header.result {
|
||||
|
@ -1,28 +1,39 @@
|
||||
<template>
|
||||
<div class="main-panel-container">
|
||||
<div class="tabs-container">
|
||||
<div class="main-panel-container">
|
||||
<div class="tabs-container">
|
||||
<el-scrollbar>
|
||||
<div class="scroll-tabs-container">
|
||||
<span class="tab" v-for="(tab, index) of tabs.content" :key="tab.id"
|
||||
:class="{ 'active-tab': tabs.activeIndex === index }" @click="setActiveTab(index)">
|
||||
<span
|
||||
class="tab"
|
||||
v-for="(tab, index) of tabs.content"
|
||||
:key="tab.id"
|
||||
:class="{ 'active-tab': tabs.activeIndex === index }"
|
||||
@click="setActiveTab(index)"
|
||||
>
|
||||
<span>
|
||||
<span :class="`iconfont ${tab.icon}`"></span>
|
||||
<span class="tab-name">{{ tab.name }}</span>
|
||||
</span>
|
||||
<span class="iconfont icon-close" @click.stop="closeTab(index)"></span>
|
||||
<span
|
||||
class="iconfont icon-close"
|
||||
@click.stop="closeTab(index)"
|
||||
></span>
|
||||
</span>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<span class="add-button iconfont icon-add" @click="pageAddNewTab">
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="add-button iconfont icon-add"
|
||||
@click="pageAddNewTab"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="main-panel">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
<div class="main-panel">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -39,41 +50,41 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
function pageAddNewTab() {
|
||||
addNewTab();
|
||||
addNewTab();
|
||||
|
||||
// 如果当前不在 debug 路由,则切换到 debug 路由
|
||||
if (route.name !== 'debug') {
|
||||
router.push(baseURL + 'debug');
|
||||
}
|
||||
// 如果当前不在 debug 路由,则切换到 debug 路由
|
||||
if (route.name !== 'debug') {
|
||||
router.push(baseURL + 'debug');
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveTab(index: number) {
|
||||
if (index >= 0 && index < tabs.content.length) {
|
||||
tabs.activeIndex = index;
|
||||
// 如果不在 debug 路由,则进入
|
||||
if (route.name !== 'debug') {
|
||||
router.push(baseURL + 'debug');
|
||||
}
|
||||
}
|
||||
if (index >= 0 && index < tabs.content.length) {
|
||||
tabs.activeIndex = index;
|
||||
// 如果不在 debug 路由,则进入
|
||||
if (route.name !== 'debug') {
|
||||
router.push(baseURL + 'debug');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.main-panel-container {
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
height: 100%;
|
||||
margin-left: 5px;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
height: 100%;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
background-color: var(--sidebar);
|
||||
border-radius: 1.2em;
|
||||
width: 100%;
|
||||
height: calc(100% - 35px);
|
||||
background-color: var(--sidebar);
|
||||
border-radius: 1.2em;
|
||||
width: 100%;
|
||||
height: calc(100% - 35px);
|
||||
}
|
||||
|
||||
.scroll-tabs-container {
|
||||
@ -82,13 +93,13 @@ function setActiveTab(index: number) {
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
height: 30px;
|
||||
width: 90%;
|
||||
background-color: var(--background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
width: 90%;
|
||||
background-color: var(--background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tabs-container .el-scrollbar {
|
||||
@ -96,47 +107,42 @@ function setActiveTab(index: number) {
|
||||
}
|
||||
|
||||
.tabs-container .tab {
|
||||
white-space: nowrap;
|
||||
margin-right: 5px;
|
||||
white-space: nowrap;
|
||||
margin-right: 5px;
|
||||
font-size: 12px;
|
||||
width: 120px;
|
||||
border-radius: .5em;
|
||||
background-color: var(--sidebar);
|
||||
padding: 3px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: var(--animation-3s);
|
||||
border-radius: .5em;
|
||||
background-color: var(--sidebar);
|
||||
padding: 3px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: var(--animation-3s);
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tabs-container .tab:active {
|
||||
transform: scale(0.95);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.tabs-container .tab>span:first-child {
|
||||
.tabs-container .tab > span:first-child {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabs-container .tab .tab-name {
|
||||
max-width: 70px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 70px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tabs-container .tab:hover {
|
||||
background-color: var(--input-active-background);
|
||||
background-color: var(--input-active-background);
|
||||
}
|
||||
|
||||
.tabs-container .tab.active-tab {
|
||||
background-color: var(--main-color);
|
||||
color: white;
|
||||
background-color: var(--main-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tabs-container .tab .iconfont {
|
||||
margin-right: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.tabs-container .icon-close {
|
||||
@ -154,30 +160,30 @@ function setActiveTab(index: number) {
|
||||
}
|
||||
|
||||
.tabs-container .add-button {
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
margin-left: 5px;
|
||||
border-radius: .5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: var(--animation-3s);
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
margin-left: 5px;
|
||||
border-radius: .5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.tabs-container .add-button:hover {
|
||||
color: var(--main-color);
|
||||
background-color: var(--sidebar);
|
||||
transition: var(--animation-3s);
|
||||
color: var(--main-color);
|
||||
background-color: var(--sidebar);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
padding: 2px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.close-icon:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
</style>
|
@ -4,7 +4,7 @@
|
||||
<div class="left">
|
||||
<h2>
|
||||
<span class="iconfont icon-chat"></span>
|
||||
{{ t('prompt-module') }}
|
||||
提示词模块
|
||||
</h2>
|
||||
|
||||
<PromptTemplates :tab-id="props.tabId"></PromptTemplates>
|
||||
@ -24,9 +24,6 @@ import { defineProps } from 'vue';
|
||||
import PromptTemplates from './prompt-templates.vue';
|
||||
import PromptReader from './prompt-reader.vue';
|
||||
import PromptLogger from './prompt-logger.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
tabId: {
|
||||
|
@ -86,7 +86,7 @@ const formRules = computed<FormRules>(() => {
|
||||
currentPrompt.value?.arguments.forEach(param => {
|
||||
rules[param.name] = [
|
||||
{
|
||||
message: `${param.name} ` + t('is-required'),
|
||||
message: `${param.name} 是必填字段`,
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
|
@ -8,27 +8,23 @@
|
||||
<span>prompts/list</span>
|
||||
<span @click.stop="reloadPrompts(client, { first: false })" class="iconfont icon-restart"></span>
|
||||
</h3>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- body -->
|
||||
|
||||
<div class="prompt-template-container-scrollbar">
|
||||
<el-scrollbar height="fit-content" v-if="(client.promptTemplates?.size || 0) > 0">
|
||||
<el-scrollbar height="500px">
|
||||
<div class="prompt-template-container">
|
||||
<div class="item"
|
||||
:class="{ 'active': props.tabId >= 0 && tabStorage.currentPromptName === template.name }"
|
||||
v-for="template of client.promptTemplates?.values()" :key="template.name"
|
||||
@click="handleClick(template)">
|
||||
<span class="prompt-title">{{ template.name }}</span>
|
||||
<span class="prompt-description">{{ template.description || '' }}</span>
|
||||
<span>{{ template.name }}</span>
|
||||
<span>{{ template.description || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<div v-else style="padding: 10px;">
|
||||
<div class="empty-description">
|
||||
<span class="iconfont icon-empty" style="font-size: 22px; opacity: 0.4; margin-right: 6px;"></span>
|
||||
<span style="opacity: 0.6;">No prompts found.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
@ -130,8 +126,8 @@ onMounted(async () => {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
@ -140,40 +136,24 @@ onMounted(async () => {
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.prompt-template-container>.item:active {
|
||||
transform: scale(0.95);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.prompt-template-container>.item.active {
|
||||
background-color: var(--main-light-color);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.prompt-title {
|
||||
font-weight: bold;
|
||||
max-width: 250px;
|
||||
.prompt-template-container>.item>span:first-child {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.prompt-description {
|
||||
.prompt-template-container>.item>span:last-child {
|
||||
opacity: 0.6;
|
||||
font-size: 12.5px;
|
||||
max-width: 250px;
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-placeholder, #bbb);
|
||||
font-size: 15px;
|
||||
min-height: 40px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
@ -115,7 +115,7 @@ const formRules = computed<FormRules>(() => {
|
||||
currentResource.value?.params.forEach(param => {
|
||||
rules[param] = [
|
||||
{
|
||||
message: `${param} ` + t('is-required'),
|
||||
message: `${param} 是必填字段`,
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
|
@ -12,22 +12,19 @@
|
||||
|
||||
<!-- body -->
|
||||
<div class="resource-template-container-scrollbar">
|
||||
<el-scrollbar height="fit-content" v-if="(client.resourceTemplates?.size || 0) > 0">
|
||||
<el-scrollbar height="500px" v-if="(client.resourceTemplates?.size || 0) > 0">
|
||||
<div class="resource-template-container">
|
||||
<div class="item"
|
||||
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'template' && tabStorage.currentResourceName === template.name }"
|
||||
v-for="template of client.resourceTemplates?.values()" :key="template.name"
|
||||
@click="handleClick(template)">
|
||||
<span class="resource-title">{{ template.name }}</span>
|
||||
<span class="resource-description">{{ template.description || '' }}</span>
|
||||
<span>{{ template.name }}</span>
|
||||
<span>{{ template.description || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<div v-else style="padding: 10px;">
|
||||
<div class="empty-description">
|
||||
<span class="iconfont icon-empty" style="font-size: 22px; opacity: 0.4; margin-right: 6px;"></span>
|
||||
<span style="opacity: 0.6;">No resource templates found.</span>
|
||||
</div>
|
||||
empty
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
@ -140,57 +137,41 @@ h3.resource-template .iconfont.icon-restart:hover {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.resource-template-container > .item {
|
||||
margin: 3px;
|
||||
padding: 5px 10px;
|
||||
border-radius: .3em;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
transition: var(--animation-3s);
|
||||
.resource-template-container>.item {
|
||||
margin: 3px;
|
||||
padding: 5px 10px;
|
||||
border-radius: .3em;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.resource-template-container > .item:hover {
|
||||
background-color: var(--main-light-color);
|
||||
transition: var(--animation-3s);
|
||||
.resource-template-container>.item:hover {
|
||||
background-color: var(--main-light-color);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.resource-template-container > .item:active {
|
||||
transform: scale(0.95);
|
||||
transition: var(--animation-3s);
|
||||
.resource-template-container>.item.active {
|
||||
background-color: var(--main-light-color);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.resource-template-container > .item.active {
|
||||
background-color: var(--main-light-color);
|
||||
transition: var(--animation-3s);
|
||||
.resource-template-container>.item>span:first-child {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.resource-title {
|
||||
font-weight: bold;
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.resource-description {
|
||||
opacity: 0.6;
|
||||
font-size: 12.5px;
|
||||
max-width: 250px;
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-placeholder, #bbb);
|
||||
font-size: 15px;
|
||||
min-height: 40px;
|
||||
.resource-template-container>.item>span:last-child {
|
||||
opacity: 0.6;
|
||||
font-size: 12.5px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
@ -12,23 +12,17 @@
|
||||
|
||||
<!-- body -->
|
||||
<div class="resource-template-container-scrollbar">
|
||||
<el-scrollbar height="fit-content" v-if="(client.resources?.size || 0) > 0">
|
||||
<el-scrollbar height="500px">
|
||||
<div class="resource-template-container">
|
||||
<div class="item"
|
||||
:class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'resource' && tabStorage.currentResourceName === resource.name }"
|
||||
v-for="resource of client.resources?.values()" :key="resource.uri"
|
||||
@click="handleClick(resource)">
|
||||
<span class="resource-title">{{ resource.name }}</span>
|
||||
<span class="resource-description">{{ resource.mimeType }}</span>
|
||||
<span>{{ resource.name }}</span>
|
||||
<span>{{ resource.mimeType }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<div v-else style="padding: 10px;">
|
||||
<div class="empty-description">
|
||||
<span class="iconfont icon-empty" style="font-size: 22px; opacity: 0.4; margin-right: 6px;"></span>
|
||||
<span style="opacity: 0.6;">No resources found.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
@ -145,58 +139,41 @@ h3.resource-template .iconfont.icon-restart:hover {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.resource-template-container > .item {
|
||||
.resource-template-container>.item {
|
||||
margin: 3px;
|
||||
padding: 5px 10px;
|
||||
border-radius: .3em;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.resource-template-container > .item:hover {
|
||||
.resource-template-container>.item:hover {
|
||||
background-color: var(--main-light-color);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.resource-template-container > .item.active {
|
||||
.resource-template-container>.item.active {
|
||||
background-color: var(--main-light-color);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.resource-template-container > .item:active {
|
||||
transform: scale(0.95);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.resource-title {
|
||||
font-weight: bold;
|
||||
max-width: 250px;
|
||||
.resource-template-container>.item>span:first-child {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.resource-description {
|
||||
.resource-template-container>.item>span:last-child {
|
||||
opacity: 0.6;
|
||||
font-size: 12.5px;
|
||||
max-width: 250px;
|
||||
/* Remove ellipsis and allow full text wrap */
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-placeholder, #bbb);
|
||||
font-size: 15px;
|
||||
min-height: 40px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
@ -1,160 +0,0 @@
|
||||
<template>
|
||||
<div class="diagram-item-record" v-if="props.dataView && props.dataView.tool">
|
||||
<div class="item-header">
|
||||
<span class="item-title">{{ props.dataView.tool.name }}</span>
|
||||
<span class="item-status" :class="props.dataView.status">{{ props.dataView.status }}</span>
|
||||
</div>
|
||||
<div class="item-desc">{{ props.dataView.tool.description }}</div>
|
||||
|
||||
<div v-if="props.dataView.function !== undefined" class="item-result">
|
||||
<div class="item-label">Function</div>
|
||||
<div class="item-json">{{ props.dataView.function.name }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="props.dataView.function !== undefined" class="item-result">
|
||||
<span class="item-label">Arguments</span>
|
||||
<json-render :json="props.dataView.function.arguments" />
|
||||
</div>
|
||||
|
||||
<div v-if="props.dataView.result !== undefined" class="item-result">
|
||||
<span class="item-label">Result</span>
|
||||
<template v-if="Array.isArray(props.dataView.result)">
|
||||
<div v-for="(item, idx) in props.dataView.result" :key="idx" class="result-block"
|
||||
:class="[props.dataView.status]">
|
||||
<pre class="item-json"
|
||||
v-if="typeof item === 'object' && item.text !== undefined">{{ item.text }}</pre>
|
||||
<pre class="item-json" v-else>{{ formatJson(item) }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
<pre class="item-json"
|
||||
v-else-if="typeof props.dataView.result === 'string'">{{ props.dataView.result }}</pre>
|
||||
<pre class="item-json" v-else>{{ formatJson(props.dataView.result) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="diagram-item-record">
|
||||
<div class="item-header">
|
||||
<span class="item-title">No Tool Selected</span>
|
||||
</div>
|
||||
<div class="item-desc">Please select a tool to view its details.</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import type { NodeDataView } from './diagram';
|
||||
|
||||
import JsonRender from '@/components/json-render/index.vue';
|
||||
|
||||
const props = defineProps({
|
||||
dataView: {
|
||||
type: Object as PropType<NodeDataView | undefined | null>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
function formatJson(obj: any) {
|
||||
try {
|
||||
return JSON.stringify(obj, null, 2)
|
||||
} catch {
|
||||
return String(obj)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.diagram-item-record {
|
||||
padding: 14px 18px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
font-size: 15px;
|
||||
max-width: 1000px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-weight: bold;
|
||||
font-size: 17px;
|
||||
color: var(--main-color, #409EFF);
|
||||
}
|
||||
|
||||
.item-status {
|
||||
font-size: 13px;
|
||||
padding: 2px 10px;
|
||||
border-radius: 12px;
|
||||
margin-left: 8px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.item-status.running {
|
||||
color: #2196f3;
|
||||
}
|
||||
|
||||
.item-status.success {
|
||||
color: #43a047;
|
||||
}
|
||||
|
||||
.item-status.error {
|
||||
color: #e53935;
|
||||
}
|
||||
|
||||
.item-status.waiting {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.item-status.default {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.8;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
color: var(--main-color, #409EFF);
|
||||
}
|
||||
|
||||
.item-json {
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
font-size: 13px;
|
||||
font-family: var(--code-font-family, monospace);
|
||||
margin: 2px 0 8px 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.item-result {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.result-block {
|
||||
margin-bottom: 6px;
|
||||
border-radius: .5em;
|
||||
margin: 5px 0;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.result-block.error {
|
||||
background-color: rgba(245, 108, 108, 0.5);
|
||||
}
|
||||
|
||||
.result-block.success {
|
||||
background-color: rgba(67, 160, 71, 0.5);
|
||||
}
|
||||
</style>
|
@ -1,248 +0,0 @@
|
||||
import type { ElkNode } from 'elkjs/lib/elk-api';
|
||||
import { MessageState, TaskLoop } from '../../chat/core/task-loop';
|
||||
import type { Reactive } from 'vue';
|
||||
import type { ChatStorage } from '../../chat/chat-box/chat';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { ToolItem } from '@/hook/type';
|
||||
|
||||
import I18n from '@/i18n';
|
||||
import type { ChatCompletionChunk } from 'openai/resources/index.mjs';
|
||||
|
||||
const { t } = I18n.global;
|
||||
|
||||
export interface Edge {
|
||||
id: string;
|
||||
sources: string[];
|
||||
targets: string[];
|
||||
sections?: any; // { startPoint: { x, y }, endPoint: { x,
|
||||
}
|
||||
|
||||
export type Node = ElkNode & {
|
||||
[key: string]: any;
|
||||
width: number;
|
||||
height: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export interface DiagramState {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
selectedNodeId: string | null;
|
||||
dataView: Map<string, NodeDataView>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface CanConnectResult {
|
||||
canConnect: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface NodeDataView {
|
||||
tool: ToolItem;
|
||||
status: 'default' | 'running' | 'waiting' | 'success' | 'error';
|
||||
function?: ChatCompletionChunk.Choice.Delta.ToolCall.Function;
|
||||
result?: any;
|
||||
}
|
||||
|
||||
export interface DiagramContext {
|
||||
reset: () => void,
|
||||
render: () => void,
|
||||
state?: DiagramState,
|
||||
setCaption: (value: string) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 判断两个节点是否可以连接
|
||||
*/
|
||||
export function invalidConnectionDetector(state: DiagramState, d: Node): CanConnectResult {
|
||||
const from = state.selectedNodeId;
|
||||
const to = d.id;
|
||||
|
||||
if (!from) {
|
||||
return { canConnect: false, reason: t('not-select-begin-node') };
|
||||
}
|
||||
|
||||
if (from === to) {
|
||||
return { canConnect: false, reason: '' };
|
||||
}
|
||||
|
||||
// 建立邻接表
|
||||
const adjacencyList: Record<string, Set<string>> = {};
|
||||
state.edges.forEach(edge => {
|
||||
const src = edge.sources[0];
|
||||
const tgt = edge.targets[0];
|
||||
if (!adjacencyList[src]) {
|
||||
adjacencyList[src] = new Set();
|
||||
}
|
||||
adjacencyList[src].add(tgt);
|
||||
});
|
||||
|
||||
// DFS 检测是否存在
|
||||
function hasPath(current: string, target: string, visited: Set<string>): boolean {
|
||||
if (current === target) return true;
|
||||
visited.add(current);
|
||||
const neighbors = adjacencyList[current] || new Set();
|
||||
for (const neighbor of neighbors) {
|
||||
if (!visited.has(neighbor)) {
|
||||
if (hasPath(neighbor, target, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasPath(to, from, new Set())) {
|
||||
return { canConnect: false, reason: t('can-make-loop') };
|
||||
}
|
||||
|
||||
if (hasPath(from, to, new Set())) {
|
||||
return { canConnect: false, reason: t('this-is-repeat-connection') };
|
||||
}
|
||||
|
||||
return {
|
||||
canConnect: true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 拓扑排序,输出每一层可以并行调度的节点id数组
|
||||
* @returns string[][] 每一层可以并行调度的节点id数组
|
||||
*/
|
||||
export function topoSortParallel(state: DiagramState): string[][] {
|
||||
// 统计每个节点的入度
|
||||
const inDegree: Record<string, number> = {};
|
||||
state.nodes.forEach(node => {
|
||||
inDegree[node.id] = 0;
|
||||
});
|
||||
state.edges.forEach(edge => {
|
||||
const tgt = edge.targets[0];
|
||||
if (tgt in inDegree) {
|
||||
inDegree[tgt]++;
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化队列,收集所有入度为0的节点
|
||||
const result: string[][] = [];
|
||||
let queue: string[] = Object.keys(inDegree).filter(id => inDegree[id] === 0);
|
||||
|
||||
const visited = new Set<string>();
|
||||
|
||||
while (queue.length > 0) {
|
||||
// 当前层可以并行的节点
|
||||
result.push([...queue]);
|
||||
const nextQueue: string[] = [];
|
||||
for (const id of queue) {
|
||||
visited.add(id);
|
||||
// 遍历所有以当前节点为源的边,减少目标节点的入度
|
||||
state.edges.forEach(edge => {
|
||||
if (edge.sources[0] === id) {
|
||||
const tgt = edge.targets[0];
|
||||
inDegree[tgt]--;
|
||||
// 如果目标节点入度为0且未访问过,加入下一层
|
||||
if (inDegree[tgt] === 0 && !visited.has(tgt)) {
|
||||
nextQueue.push(tgt);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
queue = nextQueue;
|
||||
}
|
||||
|
||||
// 检查是否有环
|
||||
if (visited.size !== state.nodes.length) {
|
||||
throw new Error('图中存在环,无法进行拓扑排序');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function makeNodeTest(
|
||||
dataView: Reactive<NodeDataView>,
|
||||
enableXmlWrapper: boolean,
|
||||
prompt: string | null = null,
|
||||
context: DiagramContext
|
||||
) {
|
||||
if (!dataView.tool.inputSchema) {
|
||||
return;
|
||||
}
|
||||
|
||||
dataView.status = 'running';
|
||||
context.render();
|
||||
|
||||
try {
|
||||
const loop = new TaskLoop({ maxEpochs: 1 });
|
||||
const usePrompt = (prompt || 'please call the tool {tool} to make some test').replace('{tool}', dataView.tool.name);
|
||||
const chatStorage = {
|
||||
messages: [],
|
||||
settings: {
|
||||
temperature: 0.6,
|
||||
systemPrompt: '',
|
||||
enableTools: [{
|
||||
name: dataView.tool.name,
|
||||
description: dataView.tool.description,
|
||||
inputSchema: dataView.tool.inputSchema,
|
||||
enabled: true
|
||||
}],
|
||||
enableWebSearch: false,
|
||||
contextLength: 5,
|
||||
enableXmlWrapper,
|
||||
parallelToolCalls: false
|
||||
}
|
||||
} as ChatStorage;
|
||||
|
||||
loop.setMaxEpochs(1);
|
||||
|
||||
let aiMockJson: any = undefined;
|
||||
|
||||
loop.registerOnToolCall(toolCall => {
|
||||
dataView.function = toolCall.function;
|
||||
|
||||
if (toolCall.function?.name === dataView.tool?.name) {
|
||||
try {
|
||||
const toolArgs = JSON.parse(toolCall.function?.arguments || '{}');
|
||||
aiMockJson = toolArgs;
|
||||
} catch (e) {
|
||||
// ElMessage.error('AI 生成的 JSON 解析错误');
|
||||
dataView.status = 'error';
|
||||
dataView.result = t('ai-gen-error-json');
|
||||
context.render();
|
||||
loop.abort();
|
||||
}
|
||||
} else {
|
||||
// ElMessage.error('AI 调用了未知的工具');
|
||||
dataView.status = 'error';
|
||||
dataView.result = t('ai-invoke-unknown-tool') + ' ' + toolCall.function?.name;
|
||||
context.render();
|
||||
loop.abort();
|
||||
}
|
||||
return toolCall;
|
||||
});
|
||||
|
||||
loop.registerOnToolCalled(toolCalled => {
|
||||
if (toolCalled.state === MessageState.Success) {
|
||||
dataView.status = 'success';
|
||||
dataView.result = toolCalled.content;
|
||||
} else {
|
||||
dataView.status = 'error';
|
||||
dataView.result = toolCalled.content;
|
||||
}
|
||||
loop.abort();
|
||||
return toolCalled;
|
||||
})
|
||||
|
||||
loop.registerOnError(error => {
|
||||
dataView.status = 'error';
|
||||
dataView.result = error;
|
||||
context.render();
|
||||
});
|
||||
|
||||
await loop.start(chatStorage, usePrompt);
|
||||
|
||||
} finally {
|
||||
if (dataView.status === 'running') {
|
||||
dataView.status = 'success';
|
||||
context.render();
|
||||
}
|
||||
}
|
||||
};
|
@ -1,654 +0,0 @@
|
||||
<template>
|
||||
<div style="display: flex; align-items: center; gap: 16px;">
|
||||
<div ref="svgContainer" class="diagram-container"></div>
|
||||
|
||||
<template v-for="(node, index) in state.nodes" :key="node.id + '-popup'">
|
||||
<transition name="collapse-from-top" mode="out-in">
|
||||
<div
|
||||
v-show="state.hoverNodeId === node.id && state.dataView.get(node.id)?.status !== 'waiting'"
|
||||
@mouseenter="setHoverItem(node.id)"
|
||||
@mouseleave="clearHoverItem()"
|
||||
:style="getNodePopupStyle(node)"
|
||||
class="node-popup"
|
||||
>
|
||||
<el-scrollbar height="100%" width="100%">
|
||||
<DiagramItemRecord :data-view="state.dataView.get(node.id)"/>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick, reactive, inject } from 'vue';
|
||||
import * as d3 from 'd3';
|
||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||
import { mcpClientAdapter } from '@/views/connect/core';
|
||||
import { invalidConnectionDetector, type Edge, type Node, type NodeDataView } from './diagram';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
|
||||
import DiagramItemRecord from './diagram-item-record.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { ToolStorage } from '../tools';
|
||||
import { tabs } from '../../panel';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
tabId: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const svgContainer = ref<HTMLDivElement | null>(null);
|
||||
let prevNodes: any[] = [];
|
||||
let prevEdges: any[] = [];
|
||||
|
||||
const state = reactive({
|
||||
nodes: [] as Node[],
|
||||
edges: [] as Edge[],
|
||||
selectedNodeId: null as string | null,
|
||||
draggingNodeId: null as string | null,
|
||||
hoverNodeId: null as string | null,
|
||||
offset: { x: 0, y: 0 },
|
||||
dataView: new Map<string, NodeDataView>
|
||||
});
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ToolStorage;
|
||||
const autoDetectDiagram = tabStorage.autoDetectDiagram;
|
||||
|
||||
if (autoDetectDiagram) {
|
||||
// 将 tabStorage.autoDetectDiagram 中的 dataView 保存到 state 中
|
||||
autoDetectDiagram.views?.forEach(item => {
|
||||
state.dataView.set(item.tool.name, {
|
||||
tool: item.tool,
|
||||
status: item.status || 'waiting',
|
||||
result: item.result || null
|
||||
});
|
||||
});
|
||||
} else {
|
||||
tabStorage.autoDetectDiagram = {
|
||||
edges: [],
|
||||
views: []
|
||||
};
|
||||
}
|
||||
|
||||
console.log(tabStorage.autoDetectDiagram!.views);
|
||||
console.log(state.dataView);
|
||||
|
||||
|
||||
let cancelHoverHandler: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
const setHoverItem = (id: string) => {
|
||||
if (cancelHoverHandler) {
|
||||
clearTimeout(cancelHoverHandler);
|
||||
}
|
||||
state.hoverNodeId = id;
|
||||
}
|
||||
|
||||
const clearHoverItem = () => {
|
||||
cancelHoverHandler = setTimeout(() => {
|
||||
if (cancelHoverHandler) {
|
||||
clearTimeout(cancelHoverHandler);
|
||||
}
|
||||
if (state.hoverNodeId) {
|
||||
state.hoverNodeId = null;
|
||||
}
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const getAllTools = async () => {
|
||||
const items = [];
|
||||
for (const client of mcpClientAdapter.clients) {
|
||||
const clientTools = await client.getTools();
|
||||
items.push(...clientTools.values());
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
const recomputeLayout = async () => {
|
||||
const elk = new ELK();
|
||||
const elkGraph = {
|
||||
id: 'root',
|
||||
layoutOptions: {
|
||||
'elk.direction': 'DOWN',
|
||||
'elk.spacing.nodeNode': '40',
|
||||
'elk.layered.spacing.nodeNodeBetweenLayers': '40'
|
||||
},
|
||||
children: state.nodes,
|
||||
edges: state.edges
|
||||
};
|
||||
const layout = await elk.layout(elkGraph) as unknown as Node;
|
||||
|
||||
state.nodes.forEach((n, i) => {
|
||||
const ln = layout.children?.find(c => c.id === n.id);
|
||||
if (ln) {
|
||||
n.x = ln.x;
|
||||
n.y = ln.y;
|
||||
n.width = ln.width || 200; // 默认宽度
|
||||
n.height = ln.height || 64; // 默认高度
|
||||
}
|
||||
});
|
||||
state.edges = layout.edges || [];
|
||||
|
||||
// 保存拓扑信息到 tabStorage
|
||||
tabStorage.autoDetectDiagram!.edges = state.edges.map(edge => ({
|
||||
id: edge.id,
|
||||
sources: edge.sources || [],
|
||||
targets: edge.targets || []
|
||||
}));
|
||||
|
||||
return layout;
|
||||
};
|
||||
|
||||
const drawDiagram = async () => {
|
||||
const tools = await getAllTools();
|
||||
|
||||
// 默认按照链表进行串联
|
||||
const nodes = [] as Node[];
|
||||
const edges = [] as Edge[];
|
||||
|
||||
// 如果保存了 edges 信息,则需要进行同步
|
||||
const reservedEdges = autoDetectDiagram?.edges;
|
||||
if (reservedEdges) {
|
||||
for (const edge of reservedEdges) {
|
||||
if (edge.sources && edge.targets && edge.sources.length > 0 && edge.targets.length > 0) {
|
||||
edges.push({
|
||||
id: edge.id,
|
||||
sources: edge.sources || [],
|
||||
targets: edge.targets || [],
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < tools.length - 1; ++i) {
|
||||
const prev = tools[i];
|
||||
const next = tools[i + 1];
|
||||
edges.push({
|
||||
id: prev.name + '-' + next.name,
|
||||
sources: [prev.name],
|
||||
targets: [next.name]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const tool of tools) {
|
||||
nodes.push({
|
||||
id: tool.name,
|
||||
width: 200,
|
||||
height: 64, // 增加高度
|
||||
labels: [{ text: tool.name || 'Tool' }]
|
||||
});
|
||||
|
||||
if (!state.dataView.has(tool.name)) {
|
||||
// 如果 dataView 中没有该工具,则初始化
|
||||
state.dataView.set(tool.name, {
|
||||
tool,
|
||||
status: 'waiting'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
state.edges = edges;
|
||||
state.nodes = nodes;
|
||||
|
||||
// 重新计算布局
|
||||
await recomputeLayout();
|
||||
|
||||
// 绘制 svg
|
||||
renderSvg();
|
||||
};
|
||||
|
||||
function renderSvg() {
|
||||
const prevNodeMap = new Map(prevNodes.map(n => [n.id, n]));
|
||||
const prevEdgeMap = new Map(prevEdges.map(e => [e.id, e]));
|
||||
|
||||
// 计算所有节点的最小x和最大x
|
||||
const xs = state.nodes.map(n => (n.x || 0));
|
||||
const minX = Math.min(...xs);
|
||||
const maxX = Math.max(...xs.map((x, i) => x + (state.nodes[i].width || 160)));
|
||||
const contentWidth = maxX - minX;
|
||||
const svgWidth = Math.max(contentWidth + 120, 400); // 120为两侧留白
|
||||
const offsetX = (svgWidth - contentWidth) / 2 - minX;
|
||||
|
||||
const height = Math.max(...state.nodes.map(n => (n.y || 0) + (n.height || 48)), 300) + 60;
|
||||
|
||||
// 不再全量清空,只清空 svg 元素
|
||||
let svg = d3.select(svgContainer.value).select('svg');
|
||||
if (svg.empty()) {
|
||||
svg = d3
|
||||
.select(svgContainer.value)
|
||||
.append('svg')
|
||||
.attr('width', svgWidth)
|
||||
.attr('height', height)
|
||||
.style('user-select', 'none') as any;
|
||||
} else {
|
||||
svg.attr('width', svgWidth).attr('height', height);
|
||||
svg.selectAll('defs').remove();
|
||||
}
|
||||
|
||||
// Arrow marker
|
||||
svg
|
||||
.append('defs')
|
||||
.append('marker')
|
||||
.attr('id', 'arrow')
|
||||
.attr('viewBox', '0 0 8 8')
|
||||
.attr('refX', 6)
|
||||
.attr('refY', 4)
|
||||
.attr('markerWidth', 5)
|
||||
.attr('markerHeight', 5)
|
||||
.attr('orient', 'auto')
|
||||
.append('path')
|
||||
.attr('d', 'M 0 0 L 8 4 L 0 8 z')
|
||||
.attr('fill', 'var(--main-color)');
|
||||
|
||||
// 1. 创建/获取 main group
|
||||
let mainGroup = svg.select('g.main-group');
|
||||
if (mainGroup.empty()) {
|
||||
mainGroup = svg.append('g').attr('class', 'main-group') as any;
|
||||
}
|
||||
mainGroup
|
||||
.transition()
|
||||
.duration(600)
|
||||
.attr('transform', `translate(${offsetX}, 0)`);
|
||||
|
||||
// Draw edges with enter animation
|
||||
const allSections: { id: string, section: any }[] = [];
|
||||
(state.edges || []).forEach(edge => {
|
||||
const sections = edge.sections || [];
|
||||
sections.forEach((section: any, idx: number) => {
|
||||
allSections.push({
|
||||
id: (edge.id || '') + '-' + (section.id || idx),
|
||||
section
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const edgeSelection = mainGroup.selectAll<SVGLineElement, any>('.edge')
|
||||
.data(allSections, d => d.id);
|
||||
|
||||
edgeSelection.exit().remove();
|
||||
|
||||
const edgeEnter = edgeSelection.enter()
|
||||
.append('line')
|
||||
.attr('class', 'edge')
|
||||
.attr('x1', d => {
|
||||
const prev = prevEdgeMap.get(d.id);
|
||||
return prev && prev.sections && prev.sections[0]
|
||||
? prev.sections[0].startPoint.x + 30
|
||||
: d.section.startPoint.x + 30;
|
||||
})
|
||||
.attr('y1', d => {
|
||||
const prev = prevEdgeMap.get(d.id);
|
||||
return prev && prev.sections && prev.sections[0]
|
||||
? prev.sections[0].startPoint.y + 30
|
||||
: d.section.startPoint.y + 30;
|
||||
})
|
||||
.attr('x2', d => {
|
||||
const prev = prevEdgeMap.get(d.id);
|
||||
return prev && prev.sections && prev.sections[0]
|
||||
? prev.sections[0].endPoint.x + 30
|
||||
: d.section.endPoint.x + 30;
|
||||
})
|
||||
.attr('y2', d => {
|
||||
const prev = prevEdgeMap.get(d.id);
|
||||
return prev && prev.sections && prev.sections[0]
|
||||
? prev.sections[0].endPoint.y + 30
|
||||
: d.section.endPoint.y + 30;
|
||||
})
|
||||
.attr('stroke', 'var(--main-color)')
|
||||
.attr('stroke-width', 2.5)
|
||||
.attr('marker-end', 'url(#arrow)')
|
||||
.attr('opacity', 0);
|
||||
|
||||
edgeEnter
|
||||
.transition()
|
||||
.duration(600)
|
||||
.attr('opacity', 1)
|
||||
.attr('x1', d => d.section.startPoint.x + 30)
|
||||
.attr('y1', d => d.section.startPoint.y + 30)
|
||||
.attr('x2', d => d.section.endPoint.x + 30)
|
||||
.attr('y2', d => d.section.endPoint.y + 30);
|
||||
|
||||
// update + 动画(注意这里不再 transition opacity)
|
||||
edgeSelection.merge(edgeEnter)
|
||||
.transition()
|
||||
.duration(600)
|
||||
.ease(d3.easeCubicInOut)
|
||||
.attr('x1', d => d.section.startPoint.x + 30)
|
||||
.attr('y1', d => d.section.startPoint.y + 30)
|
||||
.attr('x2', d => d.section.endPoint.x + 30)
|
||||
.attr('y2', d => d.section.endPoint.y + 30)
|
||||
.attr('opacity', 1);
|
||||
|
||||
// --- 节点动画部分 ---
|
||||
const nodeGroup = mainGroup.selectAll<SVGGElement, any>('.node')
|
||||
.data(state.nodes, d => d.id);
|
||||
|
||||
nodeGroup.exit().remove();
|
||||
|
||||
// 节点 enter
|
||||
const nodeGroupEnter = nodeGroup.enter()
|
||||
.append('g')
|
||||
.attr('class', 'node')
|
||||
.attr('transform', d => {
|
||||
const prev = prevNodeMap.get(d.id);
|
||||
if (prev) {
|
||||
return `translate(${(prev.x || 0) + 30}, ${(prev.y || 0) + 30})`;
|
||||
}
|
||||
return `translate(${(d.x || 0) + 30}, ${(d.y || 0) + 30})`;
|
||||
})
|
||||
.style('cursor', 'pointer')
|
||||
.attr('opacity', 0)
|
||||
.on('mousedown', null)
|
||||
.on('mouseup', function (event, d) {
|
||||
event.stopPropagation();
|
||||
if (state.selectedNodeId) {
|
||||
|
||||
const { canConnect, reason } = invalidConnectionDetector(state, d);
|
||||
console.log(reason);
|
||||
|
||||
if (reason) {
|
||||
ElMessage.warning(reason);
|
||||
}
|
||||
|
||||
if (canConnect) {
|
||||
state.edges.push({
|
||||
id: `e${state.selectedNodeId}_${d.id}_${Date.now()}`,
|
||||
sources: [state.selectedNodeId],
|
||||
targets: [d.id]
|
||||
});
|
||||
state.selectedNodeId = null;
|
||||
recomputeLayout().then(renderSvg);
|
||||
} else {
|
||||
// 已存在则只取消选中
|
||||
state.selectedNodeId = null;
|
||||
renderSvg();
|
||||
}
|
||||
context.setCaption('');
|
||||
|
||||
} else {
|
||||
state.selectedNodeId = d.id;
|
||||
renderSvg();
|
||||
context.setCaption(t('select-node-define-test-tomo'));
|
||||
}
|
||||
state.draggingNodeId = null;
|
||||
})
|
||||
.on('mouseover', function (event, d) {
|
||||
setHoverItem(d.id);
|
||||
d3.select(this).select('rect')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('stroke', 'var(--main-color)')
|
||||
.attr('stroke-width', 2);
|
||||
})
|
||||
.on('mouseout', function (event, d) {
|
||||
clearHoverItem();
|
||||
if (state.selectedNodeId === d.id) return;
|
||||
d3.select(this).select('rect')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('stroke', 'var(--main-light-color-10)')
|
||||
.attr('stroke-width', 1);
|
||||
});
|
||||
|
||||
nodeGroupEnter.append('rect')
|
||||
.attr('width', (d: any) => d.width)
|
||||
.attr('height', (d: any) => d.height)
|
||||
.attr('rx', 16)
|
||||
.attr('fill', 'var(--main-light-color-20)')
|
||||
.attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)')
|
||||
.attr('stroke-width', 2);
|
||||
|
||||
// 节点文字
|
||||
nodeGroupEnter.append('text')
|
||||
.attr('x', d => d.width / 2)
|
||||
.attr('y', d => d.height / 2 - 6) // 上移一点
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('font-size', 16)
|
||||
.attr('fill', 'var(--main-color)')
|
||||
.attr('font-weight', 600)
|
||||
.text(d => d.labels?.[0]?.text || 'Tool');
|
||||
|
||||
nodeGroupEnter.append('g').attr('class', 'node-status');
|
||||
|
||||
// 合并 enter+update
|
||||
const nodeStatusGroup = nodeGroup.merge(nodeGroupEnter).select('.node-status');
|
||||
|
||||
// 先清空再重绘
|
||||
nodeStatusGroup.each(function (d) {
|
||||
const g = d3.select(this);
|
||||
g.selectAll('*').remove(); // 清空旧内容
|
||||
|
||||
const status = state.dataView.get(d.id)?.status || 'waiting';
|
||||
if (status === 'running') {
|
||||
g.append('circle')
|
||||
.attr('cx', d.width / 2 - 32)
|
||||
.attr('cy', d.height - 16)
|
||||
.attr('r', 6)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', 'var(--main-color)')
|
||||
.attr('stroke-width', 3)
|
||||
.attr('stroke-dasharray', 20)
|
||||
.attr('stroke-dashoffset', 0)
|
||||
.append('animateTransform')
|
||||
.attr('attributeName', 'transform')
|
||||
.attr('attributeType', 'XML')
|
||||
.attr('type', 'rotate')
|
||||
.attr('from', `0 ${(d.width / 2 - 32)} ${(d.height - 16)}`)
|
||||
.attr('to', `360 ${(d.width / 2 - 32)} ${(d.height - 16)}`)
|
||||
.attr('dur', '1s')
|
||||
.attr('repeatCount', 'indefinite');
|
||||
g.append('text')
|
||||
.attr('x', d.width / 2 - 16)
|
||||
.attr('y', d.height - 12)
|
||||
.attr('font-size', 13)
|
||||
.attr('fill', 'var(--main-color)')
|
||||
.text('running');
|
||||
} else if (status === 'waiting') {
|
||||
g.append('circle')
|
||||
.attr('cx', d.width / 2 - 32)
|
||||
.attr('cy', d.height - 16)
|
||||
.attr('r', 6)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#bdbdbd')
|
||||
.attr('stroke-width', 3);
|
||||
g.append('text')
|
||||
.attr('x', d.width / 2 - 16)
|
||||
.attr('y', d.height - 12)
|
||||
.attr('font-size', 13)
|
||||
.attr('fill', '#bdbdbd')
|
||||
.text('waiting');
|
||||
} else if (status === 'success') {
|
||||
g.append('circle')
|
||||
.attr('cx', d.width / 2 - 32)
|
||||
.attr('cy', d.height - 16)
|
||||
.attr('r', 6)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#4caf50')
|
||||
.attr('stroke-width', 3);
|
||||
g.append('text')
|
||||
.attr('x', d.width / 2 - 16)
|
||||
.attr('y', d.height - 12)
|
||||
.attr('font-size', 13)
|
||||
.attr('fill', '#4caf50')
|
||||
.text('success');
|
||||
} else if (status === 'error') {
|
||||
g.append('circle')
|
||||
.attr('cx', d.width / 2 - 32)
|
||||
.attr('cy', d.height - 16)
|
||||
.attr('r', 6)
|
||||
.attr('fill', 'none')
|
||||
.attr('stroke', '#f44336')
|
||||
.attr('stroke-width', 3);
|
||||
g.append('text')
|
||||
.attr('x', d.width / 2 - 16)
|
||||
.attr('y', d.height - 12)
|
||||
.attr('font-size', 13)
|
||||
.attr('fill', '#f44336')
|
||||
.text('error');
|
||||
}
|
||||
});
|
||||
// 节点 enter 动画
|
||||
nodeGroupEnter
|
||||
.transition()
|
||||
.duration(600)
|
||||
.attr('opacity', 1)
|
||||
.attr('transform', d => `translate(${(d.x || 0) + 30}, ${(d.y || 0) + 30})`);
|
||||
|
||||
// 节点 update 动画
|
||||
nodeGroup
|
||||
.transition()
|
||||
.duration(600)
|
||||
.ease(d3.easeCubicInOut)
|
||||
.attr('transform', d => `translate(${(d.x || 0) + 30}, ${(d.y || 0) + 30})`);
|
||||
|
||||
// 高亮选中节点动画
|
||||
nodeGroup.select('rect')
|
||||
.transition()
|
||||
.duration(400)
|
||||
.attr('stroke', d => state.selectedNodeId === d.id ? 'var(--main-color)' : 'var(--main-light-color-10)');
|
||||
|
||||
// 边高亮
|
||||
svg.selectAll<SVGLineElement, any>('.edge')
|
||||
.on('mouseover', function () {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('stroke', 'var(--main-color)')
|
||||
.attr('stroke-width', 4.5);
|
||||
|
||||
context.setCaption(t('click-edge-to-delete'));
|
||||
|
||||
})
|
||||
.on('mouseout', function () {
|
||||
d3.select(this)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr('stroke', 'var(--main-color)')
|
||||
.attr('stroke-width', 2.5);
|
||||
|
||||
context.setCaption('');
|
||||
})
|
||||
.on('click', function (event, d) {
|
||||
// 只删除当前 edge
|
||||
state.edges = state.edges.filter(e => {
|
||||
// 多段 edge 情况
|
||||
if (e.sections) {
|
||||
// 只保留不是当前 section 的
|
||||
return !e.sections.some((section: any, idx: number) =>
|
||||
((e.id || '') + '-' + (section.id || idx)) === d.id
|
||||
);
|
||||
}
|
||||
// 单段 edge 情况
|
||||
return e.id !== d.id && e.id !== d.section?.id;
|
||||
});
|
||||
recomputeLayout().then(renderSvg);
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
// 渲染结束后保存当前快照
|
||||
prevNodes = state.nodes.map(n => ({ ...n }));
|
||||
prevEdges = (state.edges || []).map(e => ({ ...e, sections: e.sections ? e.sections.map((s: any) => ({ ...s })) : [] }));
|
||||
}
|
||||
|
||||
// 重置连接为链表结构
|
||||
function resetConnections() {
|
||||
if (!state.nodes.length) return;
|
||||
const edges = [];
|
||||
for (let i = 0; i < state.nodes.length - 1; ++i) {
|
||||
const prev = state.nodes[i];
|
||||
const next = state.nodes[i + 1];
|
||||
edges.push({
|
||||
id: prev.id + '-' + next.id,
|
||||
sources: [prev.id],
|
||||
targets: [next.id]
|
||||
});
|
||||
}
|
||||
state.edges = edges;
|
||||
recomputeLayout().then(renderSvg);
|
||||
}
|
||||
|
||||
const context = inject('context') as any;
|
||||
context.reset = resetConnections;
|
||||
context.state = state;
|
||||
context.render = renderSvg;
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(drawDiagram);
|
||||
});
|
||||
|
||||
// 4. 计算窗口位置
|
||||
function getNodePopupStyle(node: any): any {
|
||||
// 节点的 svg 坐标转为容器内绝对定位
|
||||
// 注意:这里假设 offsetX、node.x、node.y 已经是最新的
|
||||
const marginX = 50;
|
||||
const marginY = 80;
|
||||
const popupWidth = 300;
|
||||
const popupHeight = 500;
|
||||
|
||||
let left = (node.x || 0) + (node.width || 160) + 100;
|
||||
let top = (node.y || 0) + 30;
|
||||
|
||||
// 获取容器宽高
|
||||
const container = svgContainer.value;
|
||||
let containerWidth = 1200, containerHeight = 800; // 默认值
|
||||
if (container) {
|
||||
const rect = container.getBoundingClientRect();
|
||||
containerWidth = rect.width;
|
||||
containerHeight = rect.height;
|
||||
}
|
||||
|
||||
// 限制 left 和 top 不超出容器
|
||||
left = Math.max(marginX, Math.min(left, containerWidth - popupWidth - marginX));
|
||||
top = Math.max(marginY, Math.min(top, containerHeight - popupHeight - marginY));
|
||||
|
||||
return {
|
||||
position: 'absolute',
|
||||
left: `${left}px`,
|
||||
top: `${top}px`,
|
||||
width: `${popupWidth}px`,
|
||||
height: `${popupHeight}px`
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.diagram-container {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
border-radius: 8px;
|
||||
padding: 24px 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.node-popup {
|
||||
position: absolute;
|
||||
background: var(--background);
|
||||
border: 1px solid var(--main-color);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
white-space: nowrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
.status-running-circle {
|
||||
animation: spin 1s linear infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,173 +0,0 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" width="800px" class="no-padding-dialog">
|
||||
<template #header>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span>Tool Diagram</span>
|
||||
 
|
||||
<el-button size="small" type="primary" @click="() => context.reset()">{{ t("reset") }}</el-button>
|
||||
<!-- 自检程序弹出表单 -->
|
||||
<el-popover placement="top" width="350" trigger="click" v-model:visible="testFormVisible">
|
||||
<template #reference>
|
||||
<el-button size="small" type="primary">
|
||||
{{ t('start-auto-detect') }}
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<el-input type="textarea" v-model="testPrompt" :rows="2" style="margin-bottom: 8px;"
|
||||
placeholder="请输入 prompt" />
|
||||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||
<el-switch v-model="enableXmlWrapper" style="margin-right: 8px;" />
|
||||
<span :style="{
|
||||
opacity: enableXmlWrapper ? 1 : 0.7,
|
||||
color: enableXmlWrapper ? 'var(--main-color)' : undefined
|
||||
}">XML</span>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<el-button size="small" @click="testFormVisible = false">{{ t("cancel") }}</el-button>
|
||||
<el-button size="small" type="primary" @click="onTestConfirm">
|
||||
{{ t("confirm") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar height="80vh">
|
||||
<Diagram :tab-id="props.tabId" />
|
||||
</el-scrollbar>
|
||||
<transition name="main-fade" mode="out-in">
|
||||
<div class="caption" v-show="showCaption">
|
||||
{{ caption }}
|
||||
</div>
|
||||
</transition>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, provide, ref } from 'vue';
|
||||
import Diagram from './diagram.vue';
|
||||
import { makeNodeTest, topoSortParallel, type DiagramContext, type DiagramState } from './diagram';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { ToolStorage } from '../tools';
|
||||
import { tabs } from '../../panel';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const caption = ref('');
|
||||
const showCaption = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
tabId: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const showDialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: v => emit('update:modelValue', v)
|
||||
});
|
||||
|
||||
function setCaption(text: string) {
|
||||
caption.value = text;
|
||||
if (caption.value) {
|
||||
nextTick(() => {
|
||||
showCaption.value = true;
|
||||
});
|
||||
} else {
|
||||
nextTick(() => {
|
||||
showCaption.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const context: DiagramContext = {
|
||||
reset: () => { },
|
||||
render: () => { },
|
||||
state: undefined,
|
||||
setCaption
|
||||
};
|
||||
|
||||
provide('context', context);
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ToolStorage;
|
||||
const autoDetectDiagram = tabStorage.autoDetectDiagram;
|
||||
|
||||
if (autoDetectDiagram) {
|
||||
// ...
|
||||
} else {
|
||||
tabStorage.autoDetectDiagram = {
|
||||
edges: [],
|
||||
views: []
|
||||
};
|
||||
}
|
||||
|
||||
// 新增:自检参数表单相关
|
||||
const testFormVisible = ref(false);
|
||||
const enableXmlWrapper = ref(false);
|
||||
const testPrompt = ref('please call the tool {tool} to make some test');
|
||||
|
||||
async function onTestConfirm() {
|
||||
testFormVisible.value = false;
|
||||
// 这里可以将 enableXmlWrapper.value 和 testPrompt.value 传递给自检逻辑
|
||||
const state = context.state;
|
||||
|
||||
|
||||
tabStorage.autoDetectDiagram!.views = [];
|
||||
|
||||
if (state) {
|
||||
const dispatches = topoSortParallel(state);
|
||||
for (const nodeIds of dispatches) {
|
||||
for (const id of nodeIds) {
|
||||
const view = state.dataView.get(id);
|
||||
if (view) {
|
||||
await makeNodeTest(view, enableXmlWrapper.value, testPrompt.value, context)
|
||||
tabStorage.autoDetectDiagram!.views!.push({
|
||||
tool: view.tool,
|
||||
status: view.status,
|
||||
function: view.function,
|
||||
result: view.result
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('error');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.no-padding-dialog {
|
||||
margin-top: 30px !important;
|
||||
}
|
||||
|
||||
.no-padding-dialog .caption {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
bottom: 10px;
|
||||
margin: 0 auto;
|
||||
width: fit-content;
|
||||
min-height: 32px;
|
||||
background: rgba(245, 247, 250, 0.05);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.06);
|
||||
color: var(--main-color);
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 16px;
|
||||
z-index: 10;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
</style>
|
@ -1,41 +1,29 @@
|
||||
<template>
|
||||
<el-scrollbar height="100%">
|
||||
<AutoDetector :tab-id="props.tabId" />
|
||||
<div class="tool-module">
|
||||
<div class="left">
|
||||
<h2>
|
||||
<span class="iconfont icon-tool"></span>
|
||||
{{ t('tool-module') }}
|
||||
<el-button
|
||||
style="font-size: 12px;"
|
||||
@click="showAutoDetector = true"
|
||||
>
|
||||
{{ t('tool-self-detect') }}
|
||||
</el-button>
|
||||
工具模块
|
||||
</h2>
|
||||
<ToolList :tab-id="props.tabId"></ToolList>
|
||||
|
||||
</div>
|
||||
<div class="right">
|
||||
<ToolExecutor :tab-id="props.tabId"></ToolExecutor>
|
||||
|
||||
<ToolLogger :tab-id="props.tabId"></ToolLogger>
|
||||
</div>
|
||||
</div>
|
||||
<AutoDetector
|
||||
v-model="showAutoDetector"
|
||||
:tab-id="props.tabId"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref } from 'vue';
|
||||
import { defineProps } from 'vue';
|
||||
import ToolList from './tool-list.vue';
|
||||
import ToolExecutor from './tool-executor.vue';
|
||||
import ToolLogger from './tool-logger.vue';
|
||||
import AutoDetector from './auto-detector/index.vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
tabId: {
|
||||
@ -43,8 +31,6 @@ const props = defineProps({
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const showAutoDetector = ref(false);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<div class="swim-pool">
|
||||
<transition-group name="lane-move" tag="div">
|
||||
<div v-for="(lane, laneIdx) in lanes" :key="lane.id" class="swim-lane" @dragover.prevent
|
||||
@drop="onDrop(laneIdx)">
|
||||
<div class="lane-title">Group {{ laneIdx + 1 }}</div>
|
||||
<div v-for="tool in lane.tools" :key="tool.name" class="tool-card" draggable="true"
|
||||
@dragstart="onDragStart(tool, laneIdx)">
|
||||
{{ tool.name }}
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ToolItem } from '@/hook/type';
|
||||
import { mcpClientAdapter } from '@/views/connect/core';
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
// 工具类型
|
||||
interface Tool {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Lane {
|
||||
id: string;
|
||||
tools: ToolItem[];
|
||||
}
|
||||
|
||||
// 工具列表
|
||||
const tools = ref<ToolItem[]>([]);
|
||||
|
||||
// 泳道列表
|
||||
const lanes = ref<Lane[]>([]);
|
||||
|
||||
// 获取所有工具
|
||||
const getAllTools = async () => {
|
||||
const items = [];
|
||||
for (const client of mcpClientAdapter.clients) {
|
||||
const clientTools = await client.getTools();
|
||||
items.push(...clientTools.values());
|
||||
}
|
||||
return items;
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(async () => {
|
||||
tools.value = await getAllTools();
|
||||
console.log(tools.value);
|
||||
|
||||
lanes.value = tools.value.map((tool, idx) => ({
|
||||
id: `lane-${idx}`,
|
||||
tools: [tool]
|
||||
}));
|
||||
});
|
||||
|
||||
// 拖拽信息
|
||||
let dragInfo: { tool: ToolItem | null; fromLane: number } = { tool: null, fromLane: -1 };
|
||||
|
||||
// 拖拽开始
|
||||
function onDragStart(tool: ToolItem, fromLane: number) {
|
||||
dragInfo = { tool, fromLane };
|
||||
}
|
||||
|
||||
// 拖拽释放
|
||||
function onDrop(toLane: number) {
|
||||
if (dragInfo.tool && dragInfo.fromLane !== -1 && dragInfo.fromLane !== toLane) {
|
||||
// 从原泳道移除
|
||||
lanes.value[dragInfo.fromLane].tools = lanes.value[dragInfo.fromLane].tools.filter(
|
||||
t => t.name !== dragInfo.tool!.name
|
||||
);
|
||||
// 加入新泳道
|
||||
lanes.value[toLane].tools.push(dragInfo.tool);
|
||||
|
||||
// 如果原泳道已空,删除该泳道
|
||||
if (lanes.value[dragInfo.fromLane].tools.length === 0) {
|
||||
lanes.value.splice(dragInfo.fromLane, 1);
|
||||
}
|
||||
|
||||
// 重新排序,确保泳道顺序和索引一致
|
||||
lanes.value = lanes.value.map((lane, idx) => ({
|
||||
...lane,
|
||||
// 可选:如果你希望泳道 id 也随序号变化
|
||||
id: `lane-${idx}`
|
||||
}));
|
||||
}
|
||||
dragInfo = { tool: null, fromLane: -1 };
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.swim-pool {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.swim-lane {
|
||||
border: 1px solid var(--sidebar);
|
||||
min-height: 60px;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
transition: box-shadow 0.3s;
|
||||
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.lane-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.tool-card {
|
||||
border: 1px solid var(--main-color);
|
||||
border-radius: 4px;
|
||||
padding: 4px 12px;
|
||||
margin-bottom: 4px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 动画样式 */
|
||||
.lane-move-move {
|
||||
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
</style>
|
@ -5,31 +5,42 @@
|
||||
<div class="tool-executor-container">
|
||||
<el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
|
||||
<template v-if="currentTool?.inputSchema?.properties">
|
||||
<el-form-item v-for="[name, property] in Object.entries(currentTool.inputSchema.properties)" :key="name"
|
||||
:label="property.title || name" :prop="name"
|
||||
:required="currentTool.inputSchema.required?.includes(name)">
|
||||
<el-input v-if="property.type === 'string'" v-model="tabStorage.formData[name]" type="text"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
|
||||
@keydown.enter.prevent="handleExecute" />
|
||||
|
||||
<el-input-number v-else-if="property.type === 'number' || property.type === 'integer'"
|
||||
v-model="tabStorage.formData[name]" controls-position="right"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
|
||||
@keydown.enter.prevent="handleExecute" />
|
||||
|
||||
<el-switch v-else-if="property.type === 'boolean'" active-text="true" inactive-text="false"
|
||||
v-model="tabStorage.formData[name]" />
|
||||
|
||||
<el-input-tag
|
||||
v-else-if="property.type === 'array'"
|
||||
<el-form-item
|
||||
v-for="[name, property] in Object.entries(currentTool.inputSchema.properties)"
|
||||
:key="name"
|
||||
:label="property.title || name"
|
||||
:prop="name"
|
||||
:required="currentTool.inputSchema.required?.includes(name)"
|
||||
>
|
||||
<el-input
|
||||
v-if="property.type === 'string'"
|
||||
v-model="tabStorage.formData[name]"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name) + ' (逗号分隔)'"
|
||||
type="text"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
|
||||
@keydown.enter.prevent="handleExecute"
|
||||
/>
|
||||
|
||||
<k-input-object v-else-if="property.type === 'object'" v-model="tabStorage.formData[name]"
|
||||
:schema="property"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)" />
|
||||
<el-input-number
|
||||
v-else-if="property.type === 'number' || property.type === 'integer'"
|
||||
v-model="tabStorage.formData[name]"
|
||||
controls-position="right"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
|
||||
@keydown.enter.prevent="handleExecute"
|
||||
/>
|
||||
|
||||
<el-switch
|
||||
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]"
|
||||
:schema="property"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
@ -40,35 +51,6 @@
|
||||
<el-button @click="resetForm">
|
||||
{{ t('reset') }}
|
||||
</el-button>
|
||||
<el-button @click="generateMockData" :loading="mockLoading"
|
||||
:disabled="loading || aiMockLoading || mockLoading">
|
||||
{{ 'mook' }}
|
||||
</el-button>
|
||||
|
||||
<el-popover placement="top" width="350" trigger="click" v-model:visible="aiPromptVisible">
|
||||
<template #reference>
|
||||
<el-button :loading="aiMockLoading" :disabled="loading || aiMockLoading || mockLoading">
|
||||
{{ 'AI' }}
|
||||
</el-button>
|
||||
</template>
|
||||
<div style="margin-bottom: 8px; font-weight: bold;">
|
||||
{{ t('edit-ai-mook-prompt') }}
|
||||
</div>
|
||||
<el-input type="textarea" v-model="aiMookPrompt" :rows="2" style="margin-bottom: 8px;" />
|
||||
<div style="display: flex; align-items: center; margin-bottom: 8px;">
|
||||
<el-switch
|
||||
v-model="enableXmlWrapper"
|
||||
style="margin-right: 8px;"
|
||||
/>
|
||||
<span style="opacity: 0.7;">XML</span>
|
||||
</div>
|
||||
<div style="text-align: right;">
|
||||
<el-button size="small" @click="aiPromptVisible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button size="small" type="primary" :loading="aiMockLoading" @click="onAIMookConfirm">
|
||||
{{ t('confirm') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
@ -77,20 +59,15 @@
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, defineProps, watch, ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
import type { ToolStorage } from './tools';
|
||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||
|
||||
import KInputObject from '@/components/k-input-object/index.vue';
|
||||
import { mcpClientAdapter } from '@/views/connect/core';
|
||||
import { JSONSchemaFaker } from 'json-schema-faker';
|
||||
|
||||
defineComponent({ name: 'tool-executor' });
|
||||
const mockLoading = ref(false);
|
||||
const aiMockLoading = ref(false);
|
||||
const aiPromptVisible = ref(false);
|
||||
const enableXmlWrapper = ref(false);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@ -122,7 +99,6 @@ const currentTool = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
const aiMookPrompt = ref(`please call the tool ${currentTool.value?.name || ''} to make some test`);
|
||||
|
||||
const formRules = computed<FormRules>(() => {
|
||||
const rules: FormRules = {};
|
||||
@ -134,7 +110,7 @@ const formRules = computed<FormRules>(() => {
|
||||
rules[name] = [
|
||||
{
|
||||
required: true,
|
||||
message: `${property.title || name} ` + t("is-required"),
|
||||
message: `${property.title || name} 是必填字段`,
|
||||
trigger: 'blur'
|
||||
}
|
||||
];
|
||||
@ -156,8 +132,7 @@ const initFormData = () => {
|
||||
|
||||
Object.entries(currentTool.value.inputSchema.properties).forEach(([name, property]) => {
|
||||
newSchemaDataForm[name] = getDefaultValue(property);
|
||||
const rawType = Array.isArray(tabStorage.formData[name]) ? 'array' : typeof tabStorage.formData[name];
|
||||
const originType = normaliseJavascriptType(rawType);
|
||||
const originType = normaliseJavascriptType(typeof tabStorage.formData[name]);
|
||||
|
||||
if (tabStorage.formData[name] !== undefined && originType === property.type) {
|
||||
newSchemaDataForm[name] = tabStorage.formData[name];
|
||||
@ -171,94 +146,6 @@ const resetForm = () => {
|
||||
formRef.value?.resetFields();
|
||||
};
|
||||
|
||||
import { TaskLoop } from '@/components/main-panel/chat/core/task-loop';
|
||||
import type { ChatStorage } from '../chat/chat-box/chat';
|
||||
|
||||
const onAIMookConfirm = async () => {
|
||||
aiPromptVisible.value = false;
|
||||
await generateAIMockData(aiMookPrompt.value);
|
||||
};
|
||||
|
||||
const generateAIMockData = async (prompt?: string) => {
|
||||
if (!currentTool.value?.inputSchema) return;
|
||||
aiMockLoading.value = true;
|
||||
try {
|
||||
const loop = new TaskLoop({ maxEpochs: 1 });
|
||||
const usePrompt = prompt || `please call the tool ${currentTool.value.name} to make some test`;
|
||||
const chatStorage = {
|
||||
messages: [],
|
||||
settings: {
|
||||
temperature: 0.6,
|
||||
systemPrompt: '',
|
||||
enableTools: [{
|
||||
name: currentTool.value.name,
|
||||
description: currentTool.value.description,
|
||||
inputSchema: currentTool.value.inputSchema,
|
||||
enabled: true
|
||||
}],
|
||||
enableWebSearch: false,
|
||||
contextLength: 5,
|
||||
enableXmlWrapper: enableXmlWrapper.value,
|
||||
parallelToolCalls: false
|
||||
}
|
||||
} as ChatStorage;
|
||||
|
||||
loop.setMaxEpochs(1);
|
||||
|
||||
let aiMockJson: any = undefined;
|
||||
|
||||
loop.registerOnToolCall(toolCall => {
|
||||
console.log(toolCall);
|
||||
|
||||
if (toolCall.function?.name === currentTool.value?.name) {
|
||||
try {
|
||||
const toolArgs = JSON.parse(toolCall.function?.arguments || '{}');
|
||||
aiMockJson = toolArgs;
|
||||
} catch (e) {
|
||||
ElMessage.error('AI 生成的 JSON 解析错误');
|
||||
}
|
||||
} else {
|
||||
ElMessage.error('AI 调用了未知的工具');
|
||||
}
|
||||
loop.abort();
|
||||
return toolCall;
|
||||
});
|
||||
|
||||
loop.registerOnError(error => {
|
||||
ElMessage.error(error + '');
|
||||
});
|
||||
|
||||
await loop.start(chatStorage, usePrompt);
|
||||
|
||||
if (aiMockJson && typeof aiMockJson === 'object') {
|
||||
Object.keys(aiMockJson).forEach(key => {
|
||||
tabStorage.formData[key] = aiMockJson[key];
|
||||
});
|
||||
formRef.value?.clearValidate?.();
|
||||
}
|
||||
} finally {
|
||||
aiMockLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const generateMockData = async () => {
|
||||
if (!currentTool.value?.inputSchema) return;
|
||||
mockLoading.value = true;
|
||||
try {
|
||||
JSONSchemaFaker.option({
|
||||
useDefaultValue: true,
|
||||
alwaysFakeOptionals: true
|
||||
});
|
||||
const mockData = await JSONSchemaFaker.resolve(currentTool.value.inputSchema as any) as any;
|
||||
Object.keys(mockData).forEach(key => {
|
||||
tabStorage.formData[key] = mockData[key];
|
||||
});
|
||||
formRef.value?.clearValidate?.();
|
||||
} finally {
|
||||
mockLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
async function handleExecute() {
|
||||
if (!currentTool.value) return;
|
||||
loading.value = true;
|
||||
@ -271,15 +158,10 @@ async function handleExecute() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(currentTool, (tool) => {
|
||||
aiMookPrompt.value = `please call the tool ${tool?.name || ''} to make some test`;
|
||||
});
|
||||
|
||||
watch(() => tabStorage.currentToolName, () => {
|
||||
initFormData();
|
||||
resetForm();
|
||||
}, { immediate: true });
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@ -303,13 +185,4 @@ watch(() => tabStorage.currentToolName, () => {
|
||||
border: 1px solid var(--main-color) !important;
|
||||
}
|
||||
|
||||
.el-button:active {
|
||||
transform: scale(0.95);
|
||||
transition: transform 0.08s;
|
||||
}
|
||||
|
||||
.el-tag.el-tag--info {
|
||||
background-color: var(--main-color);
|
||||
}
|
||||
|
||||
</style>
|
@ -8,34 +8,23 @@
|
||||
<span>tools/list</span>
|
||||
<span class="iconfont icon-restart" @click.stop="reloadTools(client, { first: false })"></span>
|
||||
|
||||
<span>
|
||||
<span>
|
||||
<span class="cilent-name-tag">
|
||||
{{ client.name }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<!-- body -->
|
||||
<div class="tool-list-container-scrollbar">
|
||||
<el-scrollbar height="fit-content">
|
||||
<div class="tool-list-container-scrollbar">
|
||||
<el-scrollbar height="fit-content" v-if="(client.tools?.size || 0) > 0">
|
||||
<div class="tool-list-container">
|
||||
<div class="item" :class="{ 'active': tabStorage.currentToolName === tool.name }"
|
||||
v-for="tool of client.tools?.values()" :key="tool.name" @click="handleClick(tool)">
|
||||
<span>{{ tool.name }}</span>
|
||||
<br>
|
||||
<span class="tool-description">{{ tool.description || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<div v-else style="padding: 10px;">
|
||||
<div class="empty-description">
|
||||
<span class="iconfont icon-empty"
|
||||
style="font-size: 22px; opacity: 0.4; margin-right: 6px;"></span>
|
||||
<span style="opacity: 0.6;">No tools found.</span>
|
||||
</div>
|
||||
<div class="tool-list-container">
|
||||
<div class="item" :class="{ 'active': tabStorage.currentToolName === tool.name }"
|
||||
v-for="tool of client.tools?.values()" :key="tool.name" @click="handleClick(tool)">
|
||||
<span>{{ tool.name }}</span>
|
||||
<br>
|
||||
<span class="tool-description">{{ tool.description || '' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
@ -117,7 +106,7 @@ onMounted(async () => {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.tool-list-container>.item {
|
||||
.tool-list-container > .item {
|
||||
margin: 3px;
|
||||
padding: 5px 10px;
|
||||
border-radius: .3em;
|
||||
@ -128,11 +117,6 @@ onMounted(async () => {
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.tool-list-container>.item:active {
|
||||
transform: scale(0.95);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.tool-list-container>.item:hover {
|
||||
background-color: var(--main-light-color);
|
||||
transition: var(--animation-3s);
|
||||
@ -174,13 +158,4 @@ onMounted(async () => {
|
||||
opacity: 0.6;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-placeholder, #bbb);
|
||||
font-size: 15px;
|
||||
min-height: 40px;
|
||||
}
|
||||
</style>
|
@ -20,14 +20,8 @@
|
||||
|
||||
<div v-else>
|
||||
<!-- 展示原本的信息 -->
|
||||
<template v-if="!showRawJson && tabStorage.lastToolCallResponse">
|
||||
<div
|
||||
v-for="(c, idx) in tabStorage.lastToolCallResponse!.content"
|
||||
:key="idx"
|
||||
class="tool-call-block"
|
||||
>
|
||||
<pre class="tool-call-text">{{ c.text }}</pre>
|
||||
</div>
|
||||
<template v-if="!showRawJson">
|
||||
{{tabStorage.lastToolCallResponse?.content.map(c => c.text).join('\n')}}
|
||||
</template>
|
||||
|
||||
<!-- 展示 json -->
|
||||
@ -111,21 +105,4 @@ const showRawJson = ref(false);
|
||||
padding: 5px 9px;
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
.tool-call-block {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(0,0,0,0.04);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.tool-call-text {
|
||||
font-family: var(--code-font-family, monospace);
|
||||
font-size: 15px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
color: var(--el-text-color-primary, #222);
|
||||
}
|
||||
</style>
|
@ -1,14 +1,8 @@
|
||||
import type { ToolCallResponse } from '@/hook/type';
|
||||
import type { Edge, Node, NodeDataView } from './auto-detector/diagram';
|
||||
|
||||
export interface ToolStorage {
|
||||
activeNames: any[];
|
||||
currentToolName: string;
|
||||
lastToolCallResponse?: ToolCallResponse | string;
|
||||
formData: Record<string, any>;
|
||||
autoDetectDiagram?: {
|
||||
edges?: Edge[];
|
||||
views?: NodeDataView[];
|
||||
[key: string]: any;
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,6 @@ export function getDefaultValue(property: TypeAble): any {
|
||||
return false;
|
||||
} else if (property.type === 'object') {
|
||||
return {};
|
||||
} else if (property.type === 'array') {
|
||||
return [];
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
@ -28,8 +26,6 @@ export function normaliseJavascriptType(type: string) {
|
||||
return 'boolean';
|
||||
case 'string':
|
||||
return 'string';
|
||||
case 'array':
|
||||
return 'array';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
|
@ -91,15 +91,3 @@ export function getImageBlobUrlByBase64(base64String: string, mimeType: string,
|
||||
}
|
||||
return blobUrl;
|
||||
}
|
||||
|
||||
export function gotoWebsite(url: string) {
|
||||
const platform = getPlatform();
|
||||
const bridge = useMessageBridge();
|
||||
if (platform === 'vscode') {
|
||||
// For VSCode, use the webview API to open external links
|
||||
bridge.commandRequest('vscode/openExternal', { url });
|
||||
} else if (platform === 'web') {
|
||||
// For web, use the standard window.open method
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
@ -182,18 +182,5 @@
|
||||
"copy": "نسخ",
|
||||
"export": "تصدير",
|
||||
"export-filename": "اسم ملف التصدير",
|
||||
"how-to-use": "كيفية الاستخدام؟",
|
||||
"is-required": "هو حقل مطلوب",
|
||||
"edit-ai-mook-prompt": "تحرير إشارات AI Mook",
|
||||
"start-auto-detect": "بدء عملية الفحص الذاتي",
|
||||
"tool-module": "وحدة الأدوات",
|
||||
"prompt-module": "وحدة المطالبات",
|
||||
"not-select-begin-node": "لم يتم تحديد عقدة البداية",
|
||||
"can-make-loop": "سيؤدي الاتصال إلى تكوين حلقة",
|
||||
"this-is-repeat-connection": "هذا رابط مكرر",
|
||||
"ai-gen-error-json": "خطأ في تحليل JSON الذي تم إنشاؤه بواسطة الذكاء الاصطناعي",
|
||||
"ai-invoke-unknown-tool": "استدعت الذكاء الاصطناعي أداة غير معروفة",
|
||||
"click-edge-to-delete": "انقر على الحافة للحذف",
|
||||
"select-node-define-test-tomo": "اختر عقدة أخرى لتحديد طوبولوجيا الاختبار",
|
||||
"tool-self-detect": "الفحص الذاتي للأداة"
|
||||
"how-to-use": "كيفية الاستخدام؟"
|
||||
}
|
@ -182,18 +182,5 @@
|
||||
"copy": "Kopieren",
|
||||
"export": "Exportieren",
|
||||
"export-filename": "Exportdateiname",
|
||||
"how-to-use": "Wie benutzt man?",
|
||||
"is-required": "ist ein Pflichtfeld",
|
||||
"edit-ai-mook-prompt": "AI Mook-Prompts bearbeiten",
|
||||
"start-auto-detect": "Selbsttest starten",
|
||||
"tool-module": "Werkzeugmodul",
|
||||
"prompt-module": "Aufforderungsmodul",
|
||||
"not-select-begin-node": "Kein Startknoten ausgewählt",
|
||||
"can-make-loop": "Die Verbindung wird eine Schleife bilden",
|
||||
"this-is-repeat-connection": "Dies ist ein doppelter Link",
|
||||
"ai-gen-error-json": "Fehler beim Parsen von KI-generiertem JSON",
|
||||
"ai-invoke-unknown-tool": "KI hat ein unbekanntes Tool aufgerufen",
|
||||
"click-edge-to-delete": "Klicken Sie auf die Kante, um sie zu löschen",
|
||||
"select-node-define-test-tomo": "Wählen Sie einen anderen Knoten aus, um die Testtopologie zu definieren",
|
||||
"tool-self-detect": "Werkzeug-Selbsttest"
|
||||
"how-to-use": "Wie benutzt man?"
|
||||
}
|
@ -182,18 +182,5 @@
|
||||
"copy": "Copy",
|
||||
"export": "Export",
|
||||
"export-filename": "Export filename",
|
||||
"how-to-use": "How to use?",
|
||||
"is-required": "is a required field",
|
||||
"edit-ai-mook-prompt": "Edit AI Mook prompts",
|
||||
"start-auto-detect": "Start self-check",
|
||||
"tool-module": "Tool module",
|
||||
"prompt-module": "Prompt Module",
|
||||
"not-select-begin-node": "No starting node selected",
|
||||
"can-make-loop": "The connection will form a loop",
|
||||
"this-is-repeat-connection": "This is a duplicate link",
|
||||
"ai-gen-error-json": "AI-generated JSON parsing error",
|
||||
"ai-invoke-unknown-tool": "AI called an unknown tool",
|
||||
"click-edge-to-delete": "Click the edge to delete",
|
||||
"select-node-define-test-tomo": "Select another node to define the test topology",
|
||||
"tool-self-detect": "Tool Self-Check"
|
||||
"how-to-use": "How to use?"
|
||||
}
|
@ -182,18 +182,5 @@
|
||||
"copy": "Copier",
|
||||
"export": "Exporter",
|
||||
"export-filename": "Nom du fichier d'exportation",
|
||||
"how-to-use": "Comment utiliser ?",
|
||||
"is-required": "est un champ obligatoire",
|
||||
"edit-ai-mook-prompt": "Modifier les invites AI Mook",
|
||||
"start-auto-detect": "Démarrer l'autovérification",
|
||||
"tool-module": "Module d'outils",
|
||||
"prompt-module": "Module d'invite",
|
||||
"not-select-begin-node": "Aucun nœud de départ sélectionné",
|
||||
"can-make-loop": "La connexion formera une boucle",
|
||||
"this-is-repeat-connection": "Ceci est un lien en double",
|
||||
"ai-gen-error-json": "Erreur d'analyse JSON générée par IA",
|
||||
"ai-invoke-unknown-tool": "L'IA a appelé un outil inconnu",
|
||||
"click-edge-to-delete": "Cliquez sur le bord pour supprimer",
|
||||
"select-node-define-test-tomo": "Sélectionnez un autre nœud pour définir la topologie de test",
|
||||
"tool-self-detect": "Auto-vérification de l'outil"
|
||||
"how-to-use": "Comment utiliser ?"
|
||||
}
|
@ -182,18 +182,5 @@
|
||||
"copy": "コピー",
|
||||
"export": "エクスポート",
|
||||
"export-filename": "エクスポートファイル名",
|
||||
"how-to-use": "使用方法",
|
||||
"is-required": "は必須フィールドです",
|
||||
"edit-ai-mook-prompt": "AI Mookプロンプトを編集",
|
||||
"start-auto-detect": "自己診断を開始",
|
||||
"tool-module": "ツールモジュール",
|
||||
"prompt-module": "プロンプトモジュール",
|
||||
"not-select-begin-node": "開始ノードが選択されていません",
|
||||
"can-make-loop": "接続によりループが形成されます",
|
||||
"this-is-repeat-connection": "これは重複したリンクです",
|
||||
"ai-gen-error-json": "AI生成JSONの解析エラー",
|
||||
"ai-invoke-unknown-tool": "AIが未知のツールを呼び出しました",
|
||||
"click-edge-to-delete": "クリックして削除",
|
||||
"select-node-define-test-tomo": "テストトポロジを定義するために別のノードを選択してください",
|
||||
"tool-self-detect": "ツール自己診断"
|
||||
"how-to-use": "使用方法"
|
||||
}
|
@ -182,18 +182,5 @@
|
||||
"copy": "복사",
|
||||
"export": "내보내기",
|
||||
"export-filename": "내보내기 파일 이름",
|
||||
"how-to-use": "사용 방법?",
|
||||
"is-required": "는 필수 필드입니다",
|
||||
"edit-ai-mook-prompt": "AI Mook 프롬프트 편집",
|
||||
"start-auto-detect": "자체 점검 시작",
|
||||
"tool-module": "도구 모듈",
|
||||
"prompt-module": "프롬프트 모듈",
|
||||
"not-select-begin-node": "시작 노드가 선택되지 않았습니다",
|
||||
"can-make-loop": "연결이 루프를 형성합니다",
|
||||
"this-is-repeat-connection": "이것은 중복된 링크입니다",
|
||||
"ai-gen-error-json": "AI 생성 JSON 구문 분석 오류",
|
||||
"ai-invoke-unknown-tool": "AI가 알 수 없는 도구를 호출했습니다",
|
||||
"click-edge-to-delete": "가장자리를 클릭하여 삭제",
|
||||
"select-node-define-test-tomo": "테스트 토폴로지를 정의하려면 다른 노드를 선택하세요",
|
||||
"tool-self-detect": "도구 자체 점검"
|
||||
"how-to-use": "사용 방법?"
|
||||
}
|
@ -182,18 +182,5 @@
|
||||
"copy": "Копировать",
|
||||
"export": "Экспорт",
|
||||
"export-filename": "Имя файла экспорта",
|
||||
"how-to-use": "Как использовать?",
|
||||
"is-required": "является обязательным полем",
|
||||
"edit-ai-mook-prompt": "Редактировать подсказки AI Mook",
|
||||
"start-auto-detect": "Запустить самопроверку",
|
||||
"tool-module": "Модуль инструментов",
|
||||
"prompt-module": "Модуль подсказок",
|
||||
"not-select-begin-node": "Начальный узел не выбран",
|
||||
"can-make-loop": "Соединение образует петлю",
|
||||
"this-is-repeat-connection": "Это повторяющаяся ссылка",
|
||||
"ai-gen-error-json": "Ошибка разбора JSON, созданного ИИ",
|
||||
"ai-invoke-unknown-tool": "ИИ вызвал неизвестный инструмент",
|
||||
"click-edge-to-delete": "Нажмите на край, чтобы удалить",
|
||||
"select-node-define-test-tomo": "Выберите другой узел для определения тестовой топологии",
|
||||
"tool-self-detect": "Самопроверка инструмента"
|
||||
"how-to-use": "Как использовать?"
|
||||
}
|
@ -182,18 +182,5 @@
|
||||
"copy": "复制",
|
||||
"export": "导出",
|
||||
"export-filename": "导出文件名",
|
||||
"how-to-use": "如何使用?",
|
||||
"is-required": "是必填字段",
|
||||
"edit-ai-mook-prompt": "编辑 AI Mook 提示词",
|
||||
"start-auto-detect": "开启自检程序",
|
||||
"tool-module": "工具模块",
|
||||
"prompt-module": "提示词模块",
|
||||
"not-select-begin-node": "未选择起始节点",
|
||||
"can-make-loop": "连接会形成环路",
|
||||
"this-is-repeat-connection": "这是一个重复的连接",
|
||||
"ai-gen-error-json": "AI 生成的 JSON 解析错误",
|
||||
"ai-invoke-unknown-tool": "AI 调用了未知的工具",
|
||||
"click-edge-to-delete": "点击边以删除",
|
||||
"select-node-define-test-tomo": "选择另一个节点以定义测试拓扑",
|
||||
"tool-self-detect": "工具自检"
|
||||
"how-to-use": "如何使用?"
|
||||
}
|
@ -182,18 +182,5 @@
|
||||
"copy": "複製",
|
||||
"export": "導出",
|
||||
"export-filename": "導出文件名",
|
||||
"how-to-use": "如何使用?",
|
||||
"is-required": "是必填欄位",
|
||||
"edit-ai-mook-prompt": "編輯AI Mook提示詞",
|
||||
"start-auto-detect": "開啟自檢程序",
|
||||
"tool-module": "工具模組",
|
||||
"prompt-module": "提示詞模組",
|
||||
"not-select-begin-node": "未選擇起始節點",
|
||||
"can-make-loop": "連接會形成環路",
|
||||
"this-is-repeat-connection": "這是一個重複的連結",
|
||||
"ai-gen-error-json": "AI 生成的 JSON 解析錯誤",
|
||||
"ai-invoke-unknown-tool": "AI調用了未知的工具",
|
||||
"click-edge-to-delete": "點擊邊緣以刪除",
|
||||
"select-node-define-test-tomo": "選擇另一個節點以定義測試拓撲",
|
||||
"tool-self-detect": "工具自檢"
|
||||
"how-to-use": "如何使用?"
|
||||
}
|
@ -27,7 +27,7 @@
|
||||
|
||||
<div style="display: inline-flex;">
|
||||
<el-button class="join-qq" type="primary"
|
||||
@click="gotoWebsite('https://qm.qq.com/cgi-bin/qm/qr?k=C6ZUTZvfqWoI12lWe7L93cWa1hUsuVT0&jump_from=webapi&authKey=McW6B1ogTPjPDrCyGttS890tMZGQ1KB3QLuG4aqVNRaYp4vlTSgf2c6dMcNjMuBD')">
|
||||
@click="joinQQGroup('https://qm.qq.com/cgi-bin/qm/qr?k=C6ZUTZvfqWoI12lWe7L93cWa1hUsuVT0&jump_from=webapi&authKey=McW6B1ogTPjPDrCyGttS890tMZGQ1KB3QLuG4aqVNRaYp4vlTSgf2c6dMcNjMuBD')">
|
||||
<span class="iconfont icon-QQ"></span>
|
||||
{{ t('join-discussion') }}
|
||||
</el-button>
|
||||
@ -42,17 +42,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { gotoWebsite } from '@/hook/util';
|
||||
import { defineComponent } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const version = '0.1.9';
|
||||
const version = '0.1.8';
|
||||
const author = 'LSTM-Kirigaya (锦恢)';
|
||||
|
||||
defineComponent({ name: 'about' });
|
||||
|
||||
function joinQQGroup(url: string) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
function gotoWebsite(url: string) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -108,7 +108,6 @@ function chooseDebugMode(index: number) {
|
||||
.welcome-container > span {
|
||||
flex: 1 1 calc(50% - 100px);
|
||||
box-sizing: border-box;
|
||||
transition: .3s transform ease-in-out;
|
||||
}
|
||||
|
||||
.debug-option {
|
||||
|
@ -32,11 +32,6 @@ export function onGeneralColorChange(colorString: string) {
|
||||
|
||||
document?.documentElement.style.setProperty(
|
||||
'--main-light-color', `rgba(${r}, ${g}, ${b}, 0.7)`);
|
||||
|
||||
for (let i = 1; i <= 9; ++ i) {
|
||||
document?.documentElement.style.setProperty(
|
||||
`--main-light-color-${i}0`, `rgba(${r}, ${g}, ${b}, 0.${i})`);
|
||||
}
|
||||
}
|
||||
|
||||
export const predefinedColors = [
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,7 +1,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { setRunningCWD, setVscodeWorkspace } from '../openmcp-sdk/service/index.js';
|
||||
import { launch } from './common/entry.js';
|
||||
import { initialiseI18n, getAvailableKeys } from './i18n/index.js';
|
||||
import { initialiseI18n } from './i18n/index.js';
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
console.log('activate openmcp');
|
||||
@ -13,11 +13,6 @@ export function activate(context: vscode.ExtensionContext) {
|
||||
setVscodeWorkspace(workspace);
|
||||
setRunningCWD(context.extensionPath);
|
||||
initialiseI18n(context);
|
||||
|
||||
// 添加i18n调试信息
|
||||
console.log('Current language:', vscode.env.language);
|
||||
console.log('Available i18n keys:', getAvailableKeys().length);
|
||||
|
||||
launch(context);
|
||||
}
|
||||
|
||||
|
117
src/global.ts
117
src/global.ts
@ -2,7 +2,6 @@ import * as vscode from 'vscode';
|
||||
import * as os from 'os';
|
||||
import * as fspath from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { t } from './i18n';
|
||||
|
||||
export type FsPath = string;
|
||||
export const panels = new Map<FsPath, vscode.WebviewPanel>();
|
||||
@ -97,7 +96,7 @@ export function getConnectionConfig() {
|
||||
export function getWorkspaceConnectionConfigPath() {
|
||||
const workspace = getWorkspacePath();
|
||||
if (!workspace) {
|
||||
return null; // 如果没有工作区,则返回 null
|
||||
throw new Error('No workspace found. Please open a folder in VSCode first.');
|
||||
}
|
||||
const configDir = fspath.join(workspace, '.openmcp');
|
||||
if (!fs.existsSync(configDir)) {
|
||||
@ -111,14 +110,14 @@ export function getWorkspaceConnectionConfigPath() {
|
||||
* @description 获取工作区的连接信息,工作区的连接文件的路径都是相对路径,以 {workspace} 开头
|
||||
* @param workspace
|
||||
*/
|
||||
export function getWorkspaceConnectionConfig():IConnectionConfig| null {
|
||||
export function getWorkspaceConnectionConfig() {
|
||||
if (_workspaceConnectionConfig) {
|
||||
return _workspaceConnectionConfig;
|
||||
}
|
||||
|
||||
const workspace = getWorkspacePath();
|
||||
if (!workspace) {
|
||||
return null; // 如果没有工作区,则返回 null
|
||||
throw new Error('No workspace found. Please open a folder in VSCode first.');
|
||||
}
|
||||
const configDir = fspath.join(workspace, '.openmcp');
|
||||
const connectionConfig = fspath.join(configDir, CONNECTION_CONFIG_NAME);
|
||||
@ -224,17 +223,24 @@ export function saveWorkspaceConnectionConfig(workspace: string) {
|
||||
|
||||
|
||||
export function updateWorkspaceConnectionConfig(
|
||||
name: string,
|
||||
absPath: string,
|
||||
data: McpOptions[]
|
||||
) {
|
||||
const connectionItem = getWorkspaceConnectionConfigItemByName(name);
|
||||
const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath);
|
||||
const workspaceConnectionConfig = getWorkspaceConnectionConfig();
|
||||
if (!workspaceConnectionConfig) {
|
||||
console.error('没有工作区连接配置文件,请先创建一个工作区连接');
|
||||
return;
|
||||
|
||||
// 如果存在,删除老的 connectionItem
|
||||
if (connectionItem) {
|
||||
const index = workspaceConnectionConfig.items.indexOf(connectionItem);
|
||||
if (index !== -1) {
|
||||
workspaceConnectionConfig.items.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 对于第一个 item 添加 filePath
|
||||
// 对路径进行标准化
|
||||
data.forEach(item => {
|
||||
item.filePath = absPath.replace(/\\/g, '/');
|
||||
item.cwd = item.cwd?.replace(/\\/g, '/');
|
||||
item.name = item.serverInfo?.name;
|
||||
item.version = item.serverInfo?.version;
|
||||
@ -243,25 +249,8 @@ export function updateWorkspaceConnectionConfig(
|
||||
|
||||
console.log('get connectionItem: ', data);
|
||||
|
||||
// 如果存在,替换老的 connectionItem
|
||||
if (connectionItem) {
|
||||
console.log("存在的 connection")
|
||||
const index = workspaceConnectionConfig.items.indexOf(connectionItem);
|
||||
if (index !== -1) {
|
||||
// 替换现有项目而不是删除后插入到开头
|
||||
console.log("替换现有项目而不是删除后插入到开头")
|
||||
workspaceConnectionConfig.items[index] = data;
|
||||
} else {
|
||||
// 如果索引查找失败,则插入到第一个
|
||||
console.log("没有找到现有项目,插入到第一个")
|
||||
workspaceConnectionConfig.items.unshift(data);
|
||||
}
|
||||
} else {
|
||||
// 没有找到现有项目,插入到第一个
|
||||
console.log("没有找到现有项目,插入到第一个")
|
||||
workspaceConnectionConfig.items.unshift(data);
|
||||
}
|
||||
|
||||
// 插入到第一个
|
||||
workspaceConnectionConfig.items.unshift(data);
|
||||
const workspacePath = getWorkspacePath();
|
||||
saveWorkspaceConnectionConfig(workspacePath);
|
||||
vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh');
|
||||
@ -269,15 +258,24 @@ export function updateWorkspaceConnectionConfig(
|
||||
}
|
||||
|
||||
export function updateInstalledConnectionConfig(
|
||||
name: string,
|
||||
absPath: string,
|
||||
data: McpOptions[]
|
||||
) {
|
||||
const connectionItem = getInstalledConnectionConfigItemByName(name);
|
||||
const connectionItem = getInstalledConnectionConfigItemByPath(absPath);
|
||||
const installedConnectionConfig = getConnectionConfig();
|
||||
|
||||
// 如果存在,删除老的 connectionItem
|
||||
if (connectionItem) {
|
||||
const index = installedConnectionConfig.items.indexOf(connectionItem);
|
||||
if (index !== -1) {
|
||||
installedConnectionConfig.items.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 对于第一个 item 添加 filePath
|
||||
// 对路径进行标准化
|
||||
data.forEach(item => {
|
||||
item.filePath = absPath.replace(/\\/g, '/');
|
||||
item.cwd = item.cwd?.replace(/\\/g, '/');
|
||||
item.name = item.serverInfo?.name;
|
||||
item.version = item.serverInfo?.version;
|
||||
@ -286,21 +284,8 @@ export function updateInstalledConnectionConfig(
|
||||
|
||||
console.log('get connectionItem: ', data);
|
||||
|
||||
// 如果存在,替换老的 connectionItem
|
||||
if (connectionItem) {
|
||||
const index = installedConnectionConfig.items.indexOf(connectionItem);
|
||||
if (index !== -1) {
|
||||
// 替换现有项目而不是删除后插入到开头
|
||||
installedConnectionConfig.items[index] = data;
|
||||
} else {
|
||||
// 如果索引查找失败,则插入到第一个
|
||||
installedConnectionConfig.items.unshift(data);
|
||||
}
|
||||
} else {
|
||||
// 没有找到现有项目,插入到第一个
|
||||
installedConnectionConfig.items.unshift(data);
|
||||
}
|
||||
|
||||
// 插入到第一个
|
||||
installedConnectionConfig.items.unshift(data);
|
||||
saveConnectionConfig();
|
||||
vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh');
|
||||
}
|
||||
@ -331,10 +316,6 @@ export function getWorkspacePath() {
|
||||
export function getWorkspaceConnectionConfigItemByPath(absPath: string) {
|
||||
const workspacePath = getWorkspacePath();
|
||||
const workspaceConnectionConfig = getWorkspaceConnectionConfig();
|
||||
if (!workspaceConnectionConfig) {
|
||||
return null; // 如果没有工作区连接配置文件,则返回 null
|
||||
}
|
||||
|
||||
|
||||
const normaliseAbsPath = absPath.replace(/\\/g, '/');
|
||||
for (let item of workspaceConnectionConfig.items) {
|
||||
@ -349,44 +330,6 @@ export function getWorkspaceConnectionConfigItemByPath(absPath: string) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据输入的名称,获取该文件的 mcp 连接签名
|
||||
* @param absPath
|
||||
*/
|
||||
export function getWorkspaceConnectionConfigItemByName(name: string) {
|
||||
const workspaceConnectionConfig = getWorkspaceConnectionConfig();
|
||||
if (!workspaceConnectionConfig) {
|
||||
return null; // 如果没有工作区连接配置文件,则返回 null
|
||||
}
|
||||
for (let item of workspaceConnectionConfig.items) {
|
||||
const nItem = Array.isArray(item) ? item[0] : item;
|
||||
if (nItem.name === name) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 根据输入的名称,获取该文件的 mcp 连接签名
|
||||
* @param absPath
|
||||
*/
|
||||
export function getInstalledConnectionConfigItemByName(name: string) {
|
||||
const installedConnectionConfig = getConnectionConfig();
|
||||
|
||||
for (let item of installedConnectionConfig.items) {
|
||||
const nItem = Array.isArray(item) ? item[0] : item;
|
||||
|
||||
if (nItem.name) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @description 根据输入的文件路径,获取该文件的 mcp 连接签名
|
||||
* @param absPath
|
||||
|
@ -1,81 +1,26 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const defaultBundle: Record<string, string> = {}
|
||||
|
||||
export function initialiseI18n(context: vscode.ExtensionContext) {
|
||||
if (vscode.l10n.bundle === undefined) {
|
||||
// 获取用户的语言设置
|
||||
const userLanguage = vscode.env.language;
|
||||
|
||||
// 尝试加载用户语言对应的语言包
|
||||
let bundlePath = context.asAbsolutePath(`l10n/bundle.l10n.${userLanguage}.json`);
|
||||
|
||||
// 如果用户语言的语言包不存在,回退到英语
|
||||
if (!fs.existsSync(bundlePath)) {
|
||||
bundlePath = context.asAbsolutePath('l10n/bundle.l10n.en.json');
|
||||
}
|
||||
|
||||
try {
|
||||
const bundle = JSON.parse(fs.readFileSync(bundlePath, { encoding: 'utf-8' })) as Record<string, string>;
|
||||
Object.assign(defaultBundle, bundle);
|
||||
} catch (error) {
|
||||
console.error('Failed to load i18n bundle:', error);
|
||||
// 如果加载失败,尝试加载英语包作为最后的回退
|
||||
try {
|
||||
const fallbackPath = context.asAbsolutePath('l10n/bundle.l10n.en.json');
|
||||
const fallbackBundle = JSON.parse(fs.readFileSync(fallbackPath, { encoding: 'utf-8' })) as Record<string, string>;
|
||||
Object.assign(defaultBundle, fallbackBundle);
|
||||
} catch (fallbackError) {
|
||||
console.error('Failed to load fallback i18n bundle:', fallbackError);
|
||||
}
|
||||
}
|
||||
const bundlePath = context.asAbsolutePath('l10n/bundle.l10n.en.json');
|
||||
const bundle = JSON.parse(fs.readFileSync(bundlePath, { encoding: 'utf-8' })) as Record<string, string>;
|
||||
Object.assign(defaultBundle, bundle);
|
||||
}
|
||||
}
|
||||
|
||||
export function t(message: string, ...args: string[]): string {
|
||||
if (vscode.l10n.bundle === undefined) {
|
||||
// 使用自定义的语言包
|
||||
let translateMessage = defaultBundle[message] || message;
|
||||
|
||||
// 替换占位符 {0}, {1}, {2} 等
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
for (let i = 0; i < args.length; ++ i) {
|
||||
translateMessage = translateMessage.replace(`{${i}}`, args[i]);
|
||||
}
|
||||
|
||||
return translateMessage;
|
||||
} else {
|
||||
// 使用 VS Code 的 l10n API
|
||||
return vscode.l10n.t(message, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前使用的语言
|
||||
*/
|
||||
export function getCurrentLanguage(): string {
|
||||
return vscode.env.language;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已加载的翻译键列表,用于调试
|
||||
*/
|
||||
export function getAvailableKeys(): string[] {
|
||||
if (vscode.l10n.bundle === undefined) {
|
||||
return Object.keys(defaultBundle);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有特定键的翻译
|
||||
*/
|
||||
export function hasTranslation(key: string): boolean {
|
||||
if (vscode.l10n.bundle === undefined) {
|
||||
return key in defaultBundle;
|
||||
}
|
||||
// VS Code 的 l10n API 没有直接的方法检查,我们尝试翻译并比较
|
||||
const translated = vscode.l10n.t(key);
|
||||
return translated !== key;
|
||||
}
|
@ -44,6 +44,20 @@ export async function deleteInstalledConnection(item: McpOptions[] | McpOptions)
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateAndGetCommandPath(commandString: string, cwd?: string): Promise<string> {
|
||||
try {
|
||||
const commands = commandString.split(' ');
|
||||
const command = commands[0];
|
||||
const args = commands.slice(1);
|
||||
const process = spawn(command, args || [], { shell: true, cwd });
|
||||
process.disconnect();
|
||||
|
||||
return '';
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error(`Cannot find command: ${commandString.split(' ')[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function acquireInstalledConnection(): Promise<McpOptions[]> {
|
||||
// 让用户选择连接类型
|
||||
@ -74,6 +88,14 @@ export async function acquireInstalledConnection(): Promise<McpOptions[]> {
|
||||
placeHolder: t('please-enter-cwd-placeholder')
|
||||
});
|
||||
|
||||
// 校验 command + cwd 是否有效
|
||||
try {
|
||||
const commandPath = await validateAndGetCommandPath(commandString, cwd);
|
||||
console.log('Command Path:', commandPath);
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Invalid command: ${error}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const commands = commandString.split(' ');
|
||||
const command = commands[0];
|
||||
|
@ -4,7 +4,6 @@ import { getWorkspaceConnectionConfig, getWorkspaceConnectionConfigPath, getWork
|
||||
import { ConnectionViewItem } from './common.js';
|
||||
import { revealOpenMcpWebviewPanel } from '../webview/webview.service.js';
|
||||
import { acquireUserCustomConnection, deleteUserConnection } from './workspace.service.js';
|
||||
import { t } from '../i18n/index.js';
|
||||
|
||||
@RegisterTreeDataProvider('openmcp.sidebar.workspace-connection')
|
||||
export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<ConnectionViewItem> {
|
||||
@ -61,18 +60,14 @@ export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<Conn
|
||||
|
||||
@RegisterCommand('addConnection')
|
||||
public async addConnection(context: vscode.ExtensionContext) {
|
||||
const workspaceConnectionConfig = getWorkspaceConnectionConfig();
|
||||
if (!workspaceConnectionConfig) {
|
||||
vscode.window.showErrorMessage('OpenMCP: ' + t('error.notOpenWorkspace'));
|
||||
return;
|
||||
}
|
||||
|
||||
const item = await acquireUserCustomConnection();
|
||||
|
||||
if (item.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const workspaceConnectionConfig = getWorkspaceConnectionConfig();
|
||||
workspaceConnectionConfig.items.push(item);
|
||||
saveWorkspaceConnectionConfig(getWorkspacePath());
|
||||
|
||||
@ -83,10 +78,6 @@ export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<Conn
|
||||
@RegisterCommand('openConfiguration')
|
||||
public async openConfiguration(context: vscode.ExtensionContext, view: ConnectionViewItem) {
|
||||
const configPath = getWorkspaceConnectionConfigPath();
|
||||
if (!configPath) {
|
||||
vscode.window.showErrorMessage('OpenMCP: ' + t('error.notOpenWorkspace'));
|
||||
return;
|
||||
}
|
||||
const uri = vscode.Uri.file(configPath);
|
||||
vscode.commands.executeCommand('vscode.open', uri);
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { getFirstValidPathFromCommand, getWorkspaceConnectionConfig, getWorkspacePath, McpOptions, panels, saveWorkspaceConnectionConfig } from "../global.js";
|
||||
import * as vscode from 'vscode';
|
||||
import { t } from "../i18n/index.js";
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
export async function deleteUserConnection(item: McpOptions[] | McpOptions) {
|
||||
// 弹出确认对话框
|
||||
@ -22,10 +22,6 @@ export async function deleteUserConnection(item: McpOptions[] | McpOptions) {
|
||||
}
|
||||
|
||||
const workspaceConnectionConfig = getWorkspaceConnectionConfig();
|
||||
if (!workspaceConnectionConfig) {
|
||||
vscode.window.showErrorMessage(t('error.notOpenWorkspace'));
|
||||
return; // 没有打开工作区
|
||||
}
|
||||
|
||||
// 从配置中移除该连接项
|
||||
|
||||
@ -50,6 +46,18 @@ export async function deleteUserConnection(item: McpOptions[] | McpOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateAndGetCommandPath(command: string, cwd?: string): Promise<string> {
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(`which ${command.split(' ')[0]}`, { cwd });
|
||||
return stdout.trim();
|
||||
} catch (error) {
|
||||
throw new Error(`Cannot find command: ${command.split(' ')[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function acquireUserCustomConnection(): Promise<McpOptions[]> {
|
||||
// 让用户选择连接类型
|
||||
const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE', 'STREAMABLE_HTTP'], {
|
||||
@ -79,6 +87,14 @@ export async function acquireUserCustomConnection(): Promise<McpOptions[]> {
|
||||
placeHolder: t('please-enter-cwd-placeholder')
|
||||
});
|
||||
|
||||
// 校验 command + cwd 是否有效
|
||||
try {
|
||||
const commandPath = await validateAndGetCommandPath(commandString, cwd);
|
||||
console.log('Command Path:', commandPath);
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Invalid command: ${error}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const commands = commandString.split(' ');
|
||||
const command = commands[0];
|
||||
|
@ -4,60 +4,41 @@ import * as sinon from 'sinon';
|
||||
|
||||
|
||||
suite('连接管理测试', () => {
|
||||
vscode.window.showInformationMessage('开始测试连接管理');
|
||||
vscode.window.showInformationMessage('开始测试连接管理');
|
||||
|
||||
let inputBoxStub: sinon.SinonStub;
|
||||
let quickPickStub: sinon.SinonStub;
|
||||
let inputBoxStub: sinon.SinonStub;
|
||||
let quickPickStub: sinon.SinonStub;
|
||||
|
||||
setup(async () => {
|
||||
// mock选择连接类型
|
||||
quickPickStub = sinon.stub(vscode.window, 'showQuickPick');
|
||||
// mock 连接地址和认证输入框
|
||||
inputBoxStub = sinon.stub(vscode.window, 'showInputBox');
|
||||
await vscode.commands.executeCommand('workbench.view.extension.openmcp-sidebar');
|
||||
deleteAllConnection();
|
||||
setup(async () => {
|
||||
// mock showQuickPick
|
||||
// quickPickStub = sinon.stub(vscode.window, 'showQuickPick');
|
||||
// // mock showInputBox
|
||||
// inputBoxStub = sinon.stub(vscode.window, 'showInputBox');
|
||||
await vscode.commands.executeCommand('workbench.view.extension.openmcp-sidebar');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
teardown(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
const deleteAllConnection = async () => {
|
||||
//在开始之前删除所有链接
|
||||
}
|
||||
test('新建STDIO连接', async function () {
|
||||
this.timeout(15000);
|
||||
quickPickStub.onFirstCall().resolves('STDIO');
|
||||
inputBoxStub.onFirstCall().resolves('echo'); // command
|
||||
inputBoxStub.onSecondCall().resolves(''); // cwd
|
||||
test('新建STDIO连接', async function () {
|
||||
this.timeout(15000);
|
||||
// await vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.addConnection');
|
||||
// quickPickStub.onFirstCall().resolves('STDIO');
|
||||
// await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
// inputBoxStub.onFirstCall().resolves('echo'); // command
|
||||
// await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
// inputBoxStub.onSecondCall().resolves(''); // cwd
|
||||
|
||||
await vscode.commands.executeCommand('openmcp.sidebar.installed-connection.addConnection');
|
||||
|
||||
});
|
||||
|
||||
test('新建SSE连接', async function () {
|
||||
quickPickStub.onFirstCall().resolves('SSE');
|
||||
inputBoxStub.onFirstCall().resolves('http://localhost/sse'); // command
|
||||
inputBoxStub.onSecondCall().resolves(''); // cwd
|
||||
|
||||
await vscode.commands.executeCommand('openmcp.sidebar.installed-connection.addConnection');
|
||||
await vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.addConnection');
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
test('新建StreamableHttp连接', async function () {
|
||||
quickPickStub.onFirstCall().resolves('STREAMABLE_HTTP');
|
||||
inputBoxStub.onFirstCall().resolves('http://localhost/mcp'); // command
|
||||
inputBoxStub.onSecondCall().resolves(''); // cwd
|
||||
await vscode.commands.executeCommand('openmcp.sidebar.installed-connection.addConnection');
|
||||
|
||||
});
|
||||
|
||||
test('等待以便观察窗口', async function () {
|
||||
this.timeout(15000);
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
});
|
||||
test('等待以便观察窗口', async function () {
|
||||
this.timeout(15000);
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
});
|
||||
|
||||
|
||||
});
|
@ -105,12 +105,6 @@ export function revealOpenMcpWebviewPanel(
|
||||
exportFile(data.filename, data.content);
|
||||
break;
|
||||
|
||||
case 'vscode/openExternal':
|
||||
if (data.url) {
|
||||
vscode.env.openExternal(vscode.Uri.parse(data.url));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'vscode/clipboard/writeText':
|
||||
vscode.env.clipboard.writeText(data.text);
|
||||
break;
|
||||
|
Loading…
x
Reference in New Issue
Block a user