Compare commits

...

98 Commits

Author SHA1 Message Date
0f947c5b09 发布 0.0.7 2025-05-07 22:05:26 +08:00
74c18dcd2b 支持富文本编辑器,支持在客户端对提词和资源进行选择 2025-05-07 21:59:20 +08:00
18db01af6b 支持富文本编辑器,支持在客户端对提词和资源进行选择 2025-05-07 21:57:42 +08:00
96d36c0706 支持富文本编辑器,支持在客户端对提词和资源进行选择 2025-05-07 21:56:07 +08:00
li1553770945
3ecacfc6e8 feat:修改 issue template 为 yaml 2025-05-07 17:53:04 +08:00
li1553770945
5c0462751a feat:添加 issue template 2025-05-07 17:52:17 +08:00
cefa6b2af5 准备实现富文本编辑器 2025-05-07 02:50:05 +08:00
ea44620dee 准备实现富文本编辑器 2025-05-07 02:46:43 +08:00
5a2a699a51 实现客户端部分的 prompt 支持 2025-05-06 21:53:43 +08:00
cf16cff7cf 修改刷新按钮的位置 2025-05-06 20:55:09 +08:00
8d549d17e2 支持 resources/list 的调度 2025-05-06 20:45:47 +08:00
li1553770945
c716cbc6ca feat(README):添加discord频道 2025-05-03 19:55:21 +08:00
Kirigaya Kazuto
b056618e11
Update README.md 2025-05-03 18:14:46 +08:00
7aad1e6729 update 2025-05-03 03:36:25 +08:00
f835d90f39 增加演示视频 2025-05-03 03:31:05 +08:00
8f8106a0a3 实现 tesseract 的核心部分打包 2025-05-03 02:26:21 +08:00
1c92b6fddd 实现 tesseract 的核心部分打包 2025-05-03 01:56:41 +08:00
73a5b05a5d 添加没有填写 token 提醒 2025-05-02 23:39:13 +08:00
b7015d7532 修复无法进行离线 OCR 的问题 2025-05-02 21:02:33 +08:00
e206b502b5 修复无法进行离线 OCR 的问题 2025-05-02 20:00:06 +08:00
56145bfdf9 修复全局安装的 mcp 服务器 name 更新的问题 2025-05-02 18:00:22 +08:00
dca2a9c820 增加全局安装的 mcp 服务器控制面板 2025-05-02 03:07:29 +08:00
96d029f906 增加引导页面 2025-05-02 01:50:24 +08:00
c1b06313d7 更新插件端架构 2025-05-01 03:17:20 +08:00
34936d944d 更新插件端架构 2025-04-30 19:49:14 +08:00
354380cf23 统一多端模态的消息桥接层 2025-04-30 14:54:41 +08:00
0bea084c35 优化 web 端开发的隐私保护 2025-04-30 03:58:54 +08:00
2b1c0c30dd 解决编译后体积过大的问题 2025-04-29 22:19:50 +08:00
11ff54e2f1 更新桌面端编译配置 2025-04-29 18:00:46 +08:00
63ed5d7256 隐藏工具栏 2025-04-29 15:19:14 +08:00
9c98a636f9 增加概念说明 2025-04-28 21:08:39 +08:00
7d2afb053e 增加概念说明 2025-04-28 19:33:04 +08:00
9ca03ef0fe 制作桌面端软件 2025-04-28 19:16:07 +08:00
1c696fa585 制作桌面端软件 2025-04-28 19:15:45 +08:00
07ceb7cb84 实现 system prompt 的设置和保存 2025-04-28 17:40:10 +08:00
85c26a3cbf 实现 system prompt 的设置和保存 2025-04-28 17:31:19 +08:00
27e94efa26 重构 chat 模块的 setting 面板 2025-04-28 15:47:29 +08:00
a535690bc6 修复关闭标签页 key 重排序错误的问题 2025-04-28 14:57:46 +08:00
4f9900a64c openmcp 完成闭环 2025-04-28 02:39:21 +08:00
dd0d6016fa 测试 OCR 全流程 2025-04-27 22:28:29 +08:00
31fa5ead4f 重构 OpenMCPService,采用新的架构 2025-04-27 19:11:24 +08:00
8ef6ddd1ed enable decorator 2025-04-27 16:05:00 +08:00
a55759d92f update ocr 2025-04-27 15:23:43 +08:00
45ba33119c 对 tool message 进行后处理 2025-04-27 01:10:21 +08:00
46db790304 美化 2025-04-26 17:17:57 +08:00
a328929296 实现中间对话的修改 2025-04-26 17:04:19 +08:00
beaf4f5ba1 实现中间对话的修改 2025-04-26 17:03:56 +08:00
cf3a3a57ce 数据格式标准化 2025-04-26 15:12:36 +08:00
dddb786aa4 数据格式标准化 2025-04-26 15:12:19 +08:00
91cff239ab 使用对象数据库进行重载 2025-04-26 14:07:36 +08:00
5fc7a9e468 支持 OCR 2025-04-25 21:42:30 +08:00
ca64ce040f 增加小型对象数据库 2025-04-25 20:14:59 +08:00
f925da7d7d 修复工具调用的错误 2025-04-25 19:16:48 +08:00
ad857e6544 实现响应结果的重排和对象输入框的实现 2025-04-25 03:46:29 +08:00
f14687b1d8 给 tool use 设计负反馈机制 2025-04-24 20:51:55 +08:00
dd9c117df7 优化页面布局 2025-04-24 19:03:32 +08:00
8887da8ba9 优化页面布局 2025-04-24 19:03:08 +08:00
f484688a4b 支持预设环境变量与stdio启动的 cwd 自定 2025-04-24 16:00:54 +08:00
ddb4dfb565 支持 SSE 2025-04-23 19:14:31 +08:00
Kirigaya Kazuto
1e201db87a
Merge pull request #9 from appli456/chore/editorconfig
chore: 项目编辑配置与git配置文件
2025-04-23 15:55:24 +08:00
6c982f1800 push 支持基本的 MCP 项目管理 to 90% 2025-04-23 15:29:07 +08:00
lirz
3b95a57bd9 chore: 项目编辑配置与git配置文件 2025-04-23 15:21:00 +08:00
4473421708 实现 vscode 内 sidebar 的实现 2025-04-23 12:29:52 +08:00
5bac8f8726 实现连接参数的局部保存 2025-04-23 03:31:16 +08:00
b2b80c1a3f push 支持基本的 MCP 项目管理 to 70% 2025-04-22 21:58:07 +08:00
31a25f27bc 修改 about 页面 2025-04-22 17:14:43 +08:00
3b1afa70bd 修改 about 页面 2025-04-22 17:10:09 +08:00
859d506ea9 push save MVP to 100% 2025-04-22 15:23:35 +08:00
3976670295 push save MVP to 95% 2025-04-22 15:13:54 +08:00
03d79d0222 更新 win 上的打包脚本 2025-04-22 04:10:18 +08:00
fcf3b8cb9f 对话中可以直接进行工具测试 2025-04-22 02:25:07 +08:00
493580ba3b publish 0.0.4 2025-04-21 22:11:56 +08:00
799c37fc76 add new feat 2025-04-21 21:45:53 +08:00
7414a905d4 优化 mcp 服务器命名呈现 2025-04-21 21:42:34 +08:00
299bb30df7 优化 openmcp 图标 2025-04-21 21:32:27 +08:00
8105bfde85 修复只有空页面时,标签页恢复报错的 bug 2025-04-21 19:43:29 +08:00
36744f2a71 支持自定义大模型接入 80% 2025-04-21 18:47:11 +08:00
39919e7273 update mvp 2025-04-21 12:36:48 +08:00
75191f1144 update mvp 2025-04-21 12:28:07 +08:00
ce31ee739d 解决无法重新连接的问题 | 优化调试器布局 2025-04-21 01:02:39 +08:00
471ad41c8e 解决无法重新连接的问题 | 优化调试器布局 2025-04-21 01:02:21 +08:00
69b907901f 增加多样化的错误信息 2025-04-20 03:11:20 +08:00
5c9f46d2f1 修复无法重新连接的问题 2025-04-20 02:07:01 +08:00
3e0622f7e7 修复 mcp 项目初始化点击工具全部都是空的 bug 2025-04-18 21:20:32 +08:00
3ffcbdd9a8 save 2025-04-18 21:10:40 +08:00
698bbf0f84 解决 code 内部颜色重叠的问题 2025-04-18 20:51:00 +08:00
b7a4ab8706 update some i18n support 2025-04-18 20:31:14 +08:00
b1a083dcd4 fix issue#7 问题2 2025-04-18 19:08:57 +08:00
96772ad422 修复 bug 2025-04-17 20:30:33 +08:00
75ed81b5ef 修复 bug 2025-04-17 20:29:20 +08:00
b33eab402d 修复 bug 2025-04-17 20:22:18 +08:00
3de7ef68ba 增加成本统计信息 2025-04-17 16:44:39 +08:00
4e8caf4c53 update dev.sh 2025-04-14 12:42:40 +08:00
200a53fafc update readme 2025-04-13 17:34:47 +08:00
03eb664a1a publish 0.0.2 2025-04-13 17:28:58 +08:00
a0968902f4 修复 vscode/trae 下 tab 恢复的问题 2025-04-13 16:12:58 +08:00
e306388f14 修复 vscode/trae 下 tab 恢复的问题 2025-04-13 15:19:13 +08:00
37b162155c service 增加开发热更新 2025-04-13 14:39:50 +08:00
210 changed files with 19500 additions and 2873 deletions

15
.editorconfig Normal file
View File

@ -0,0 +1,15 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.ps1]
end_of_line = crlf
[*.md]
trim_trailing_whitespace = false

318
.gitattributes vendored Normal file
View File

@ -0,0 +1,318 @@
# Common settings that generally should always be used with your language specific settings
# Auto detect text files and perform LF normalization
* text=auto
#
# The above will handle all files NOT found below
#
# Documents
*.bibtex text diff=bibtex
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.md text diff=markdown
*.mdx text diff=markdown
*.tex text diff=tex
*.adoc text
*.textile text
*.mustache text
*.csv text eol=crlf
*.tab text
*.tsv text
*.txt text
*.sql text
*.epub diff=astextplain
# Graphics
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.tif binary
*.tiff binary
*.ico binary
# SVG treated as text by default.
*.svg text
# If you want to treat it as binary,
# use the following line instead.
# *.svg binary
*.eps binary
# Scripts
*.bash text eol=lf
*.fish text eol=lf
*.ksh text eol=lf
*.sh text eol=lf
*.zsh text eol=lf
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Serialisation
*.json text
*.toml text
*.xml text
*.yaml text
*.yml text
# Archives
*.7z binary
*.bz binary
*.bz2 binary
*.bzip2 binary
*.gz binary
*.lz binary
*.lzma binary
*.rar binary
*.tar binary
*.taz binary
*.tbz binary
*.tbz2 binary
*.tgz binary
*.tlz binary
*.txz binary
*.xz binary
*.Z binary
*.zip binary
*.zst binary
# Text files where line endings should be preserved
*.patch -text
#
# Exclude files from exporting
#
.gitattributes export-ignore
.gitignore export-ignore
.gitkeep export-ignore
## GITATTRIBUTES FOR WEB PROJECTS
#
# These settings are for any web project.
#
# Details per file setting:
# text These files should be normalized (i.e. convert CRLF to LF).
# binary These files are binary and should be left untouched.
#
# Note that binary is a macro for -text -diff.
######################################################################
# Auto detect
## Handle line endings automatically for files detected as
## text and leave all files detected as binary untouched.
## This will handle all files NOT defined below.
* text=auto
# Source code
*.bash text eol=lf
*.bat text eol=crlf
*.cmd text eol=crlf
*.coffee text
*.css text diff=css
*.htm text diff=html
*.html text diff=html
*.inc text
*.ini text
*.js text
*.mjs text
*.cjs text
*.json text
*.jsx text
*.less text
*.ls text
*.map text -diff
*.od text
*.onlydata text
*.php text diff=php
*.pl text
*.ps1 text eol=crlf
*.py text diff=python
*.rb text diff=ruby
*.sass text
*.scm text
*.scss text diff=css
*.sh text eol=lf
.husky/* text eol=lf
*.sql text
*.styl text
*.tag text
*.ts text
*.tsx text
*.xml text
*.xhtml text diff=html
# Docker
Dockerfile text
# Documentation
*.ipynb text eol=lf
*.markdown text diff=markdown
*.md text diff=markdown
*.mdwn text diff=markdown
*.mdown text diff=markdown
*.mkd text diff=markdown
*.mkdn text diff=markdown
*.mdtxt text
*.mdtext text
*.txt text
AUTHORS text
CHANGELOG text
CHANGES text
CONTRIBUTING text
COPYING text
copyright text
*COPYRIGHT* text
INSTALL text
license text
LICENSE text
NEWS text
readme text
*README* text
TODO text
# Templates
*.dot text
*.ejs text
*.erb text
*.haml text
*.handlebars text
*.hbs text
*.hbt text
*.jade text
*.latte text
*.mustache text
*.njk text
*.phtml text
*.svelte text
*.tmpl text
*.tpl text
*.twig text
*.vue text
# Configs
*.cnf text
*.conf text
*.config text
.editorconfig text
*.env text
.gitattributes text
.gitconfig text
.htaccess text
*.lock text -diff
package.json text eol=lf
package-lock.json text eol=lf -diff
pnpm-lock.yaml text eol=lf -diff
.prettierrc text
yarn.lock text -diff
*.toml text
*.yaml text
*.yml text
browserslist text
Makefile text
makefile text
# Fixes syntax highlighting on GitHub to allow comments
tsconfig.json linguist-language=JSON-with-Comments
# Heroku
Procfile text
# Graphics
*.ai binary
*.bmp binary
*.eps binary
*.gif binary
*.gifv binary
*.ico binary
*.jng binary
*.jp2 binary
*.jpg binary
*.jpeg binary
*.jpx binary
*.jxr binary
*.pdf binary
*.png binary
*.psb binary
*.psd binary
# SVG treated as an asset (binary) by default.
*.svg text
# If you want to treat it as binary,
# use the following line instead.
# *.svg binary
*.svgz binary
*.tif binary
*.tiff binary
*.wbmp binary
*.webp binary
# Audio
*.kar binary
*.m4a binary
*.mid binary
*.midi binary
*.mp3 binary
*.ogg binary
*.ra binary
# Video
*.3gpp binary
*.3gp binary
*.as binary
*.asf binary
*.asx binary
*.avi binary
*.fla binary
*.flv binary
*.m4v binary
*.mng binary
*.mov binary
*.mp4 binary
*.mpeg binary
*.mpg binary
*.ogv binary
*.swc binary
*.swf binary
*.webm binary
# Archives
*.7z binary
*.gz binary
*.jar binary
*.rar binary
*.tar binary
*.zip binary
# Fonts
*.ttf binary
*.eot binary
*.otf binary
*.woff binary
*.woff2 binary
# Executables
*.exe binary
*.pyc binary
# Prevents massive diffs caused by vendored, minified files
**/.yarn/releases/** binary
**/.yarn/plugins/** binary
# RC files (like .babelrc or .eslintrc)
*.*rc text
# Ignore files (like .npmignore or .gitignore)
*.*ignore text
# Prevents massive diffs from built files
dist/* binary
.vscodeignore text

47
.github/ISSUE_TEMPLATE/bug-report.yaml vendored Normal file
View File

@ -0,0 +1,47 @@
name: 🐛 Bug Report
description: 提交程序错误报告
title: "[Bug] 简明问题描述"
labels: [bug]
body:
- type: markdown
attributes:
value: |
**请按格式填写以下内容**
- type: input
id: environment
attributes:
label: 环境信息
description: "操作系统/浏览器/项目版本/dev模式 or vscode插件"
placeholder: "例如: Windows 11 / Chrome 120 / v2.1.0 / dev模式"
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: 复现步骤
description: 如何触发的错误?(按数字列表格式)"
placeholder: |
1.
2.
validations:
required: true
- type: textarea
id: expected
attributes:
label: 预期行为
- type: dropdown
id: priority
attributes:
label: 严重程度
options:
- "高(阻断正常使用)"
- "中(部分功能受限)"
- "低(轻微影响)"
validations:
required: true
- type: textarea
id: additional_notes
attributes:
label: 其他备注信息
description: "补充说明"
placeholder: "例如: 相关截图/日志/错误信息"

View File

@ -0,0 +1,54 @@
name: 🚀 Feature Request
description: 提交新功能建议或改进请求
title: "[Feature] 简短描述功能需求"
labels: [enhancement]
body:
- type: markdown
attributes:
value: |
**请详细描述你的功能需求**
- type: input
id: problem
attributes:
label: 想要解决的问题
description: "说明当前痛点或未满足的需求"
placeholder: "例如:无法支持mcp中函数参数是自定义类型的情况"
validations:
required: true
- type: textarea
id: description
attributes:
label: 功能描述
description: "你想要的新功能具体是什么?"
placeholder: "例如:支持mcp中函数参数是自定义类型的情况"
validations:
required: true
- type: textarea
id: scenario
attributes:
label: 使用场景
description: "这个功能会在什么情况下使用?"
- type: dropdown
id: priority
attributes:
label: 功能优先级
options:
- "高(核心功能缺失)"
- "中(显著提升效率)"
- "低(优化体验)"
validations:
required: true
- type: checkboxes
id: contribution
attributes:
label: "是否愿意参与贡献?"
options:
- label: "我愿意为此功能提供代码"
- label: "我可以提供详细设计文档"
- label: "暂时无法参与开发"
- type: textarea
id: additional_notes
attributes:
label: 其他备注信息
description: "补充说明"
placeholder: "如技术约束、参考案例等"

11
.gitignore vendored
View File

@ -4,4 +4,13 @@ node_modules
.vscode-test/ .vscode-test/
*.vsix *.vsix
.env .env
resources openmcp-sdk
.DS_Store
.exe
.idea
resources/ocr/*.js
resources/ocr/*.wasm
resources/renderer
resources/service
*.traineddata

View File

@ -1,5 +0,0 @@
import { defineConfig } from '@vscode/test-cli';
export default defineConfig({
files: 'out/test/**/*.test.js',
});

View File

@ -17,5 +17,10 @@ service/**
test/** test/**
servers/** servers/**
scripts/** scripts/**
software/**
*.sh *.sh
*.ps1 *.ps1
.editorconfig
.gitattributes
*.vsix

View File

@ -1,5 +1,46 @@
# Change Log # Change Log
## [main] 0.0.7
- 优化页面布局,使得调试窗口可以显示更多内容
- 扩大默认的上下文长度 10 -> 20
- 增加「通用选项」 -> 「MCP工具最长调用时间 (sec)」
- 支持富文本输入框,现在可以将 prompt 和 resource 嵌入到输入框中 进行 大规模 prompt engineering 调试工作了
## [main] 0.0.6
- 修复部分因为服务器名称特殊字符而导致的保存实效的错误
- 插件模式下左侧管理面板中的「MCP连接工作区」视图可以进行增删改查了
- 新增「安装的 MCP 服务器」,用于安装全局范围的 mcp server
- 增加引导页面
- 修复无法进行离线 OCR 的问题
- 修复全局安装的 mcp 服务器 name 更新的问题
## [main] 0.0.5
- 支持对已经打开过的文件项目进行管理
- 支持对用户对应服务器的调试工作内容进行保存
- 支持连续工具调用和错误警告的显示
- 实现小型本地对象数据库,用于对对话产生的多媒体进行数据持久化
- 支持对于调用结果进行一键复现
- 支持对中间结果进行修改
- 支持 system prompt 的保存和修改
## [main] 0.0.4
- 修复选择模型后点击确认跳转回 deepseek 的 bug
- 修复 mcp 项目初始化点击工具全部都是空的 bug
- 修复无法重新连接的 bug
- 支持自定义第三方 openai 兼容的模型服务
## [main] 0.0.3
- 增加每一条信息的成本统计信息
- 修复初始化页面路由不为 debug 导致页面空白的 bug
## [main] 0.0.2
- 优化页面布局
- 解决更新标签页后打开无法显示的 bug
- 解决不如输入组件按下回车直接黑屏的 bug
- 更加完整方便的开发脚本
## [main] 0.0.1 ## [main] 0.0.1
- 完成 openmcp 的基础 inspector 功能 - 完成 openmcp 的基础 inspector 功能

149
README.md
View File

@ -4,34 +4,120 @@
<h3>OpenMCP: 一体化 MCP Server 调试器</h3> <h3>OpenMCP: 一体化 MCP Server 调试器</h3>
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=C6ZUTZvfqWoI12lWe7L93cWa1hUsuVT0&jump_from=webapi&authKey=McW6B1ogTPjPDrCyGttS890tMZGQ1KB3QLuG4aqVNRaYp4vlTSgf2c6dMcNjMuBD" target="_blank">加入 OpenMCP正式级技术组</a> <a href="https://qm.qq.com/cgi-bin/qm/qr?k=C6ZUTZvfqWoI12lWe7L93cWa1hUsuVT0&jump_from=webapi&authKey=McW6B1ogTPjPDrCyGttS890tMZGQ1KB3QLuG4aqVNRaYp4vlTSgf2c6dMcNjMuBD" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: #CB81DA; color: white; border-radius: .5em; text-decoration: none;">👉 加入 OpenMCP正式级技术组</a>
<a href="https://qm.qq.com/q/qyVJ189OUg" target="_blank">加入 OpenMCP咖啡厅</a>
<a href="https://qm.qq.com/q/AO0sJS3r7U" target="_blank">加入 OpenMCP正式级宣传组</a> <a href="https://discord.gg/af5cfB9a" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: rgb(84, 176, 84); color: white; border-radius: .5em; text-decoration: none;"> 加入 OpenMCP Discord频道</a>
</div> </div>
## OpenMCP ## OpenMCP
一款用于 MCP 服务端调试的一体化 vscode 插件。 一款用于 MCP 服务端调试的一体化 vscode/trae/cursor 插件。
![](./icons/sreenshot.png) <video src="https://github.com/user-attachments/assets/ab214d58-b77c-4bd3-8b6e-55552f4036ff" width="100%"></video>
- 包含原版 Inpsector 的所有功能 <video src="https://github.com/user-attachments/assets/c17a4ad7-83b4-47ff-8627-85b57ad18940" width="100%"></video>
- 包含一个简易的用于进行测试的大模型对话 & 执行窗口
- 支持多种大模型
集成 Inspector + MCP 客户端基础功能,开发测试一体化。
![](./icons/openmcp.welcome.png)
进行资源协议、工具、Prompt 的 MCP 服务器测试。
![](./icons/openmcp.resource.png)
测试完成的工具可以放入 「交互测试」 模块之间进行大模型交互测试。
![](./icons/openmcp.chatbot.png)
完整的项目级管理面板,更加方便的进行项目和全局的 mcp 项目管理。
![](./icons/openmcp.management.png)
支持多种大模型
![](./icons/openmcp.support.llm.png)
## TODO ## TODO
- [x] 完成最基本的各类基础设施 ## 需求规划
- [ ] 支持同时调试多个 MCP Server
- [ ] 支持通过大模型进行在线验证
- [ ] 支持 completion/complete 协议字段
- [x] 支持 对用户对应服务器的调试工作内容进行保存
- [ ] 高危操作权限确认
- [ ] 对于连接的 mcp server 进行热更新
| 所在模块 | 需求内容 | 功能优先级 | 当前状态 | 修复优先级 |
|---------|---------|--------|---------|-----------|
| `all` | 完成最基本的各类基础设施 | `完整版本` | 100% | `Done` |
| `render` | chat 模式下支持进行成本分析 | `迭代版本` | 100% | `Done` |
| `ext` | 支持基本的 MCP 项目管理 | `迭代版本` | 100% | `P0` |
| `service` | 支持自定义支持 openai 接口协议的大模型接入 | `完整版本` | 100% | `Done` |
| `service` | 支持自定义接口协议的大模型接入 | `MVP` | 0% | `P1` |
| `all` | 支持同时调试多个 MCP Server | `MVP` | 0% | `P1` |
| `all` | 支持通过大模型进行在线验证 | `迭代版本` | 100% | `Done` |
| `all` | 支持对用户对应服务器的调试工作内容进行保存 | `迭代版本` | 100% | `Done` |
| `render` | 高危操作权限确认 | `MVP` | 0% | `P1` |
| `service` | 对于连接的 mcp server 进行热更新 | `MVP` | 0% | `P1` |
| `service` | 系统配置信息云同步 | `MVP` | 0% | `P1` |
| `all` | 系统提示词管理模块 | `迭代版本` | 100% | `Done` |
| `service` | 工具 wise 的日志系统 | `MVP` | 0% | `P1` |
| `service` | 自带 OCR 进行字符识别 | `迭代版本` | 100% | `Done` |
## 项目概念
openmcp 采用分层模块化设计,通过组装不同的模块,可以将它实现成不同平台上的不同模式。
```mermaid
flowchart TD
subgraph OpenMCP核心组件
renderer[Renderer]
openmcpservice[OpenMCPService]
end
subgraph OpenMCP_Web["OpenMCP Web"]
renderer
openmcpservice
nginx[Nginx]
end
subgraph OpenMCP_插件["OpenMCP 插件"]
renderer
openmcpservice
vscode[VSCode 插件代码]
end
subgraph OpenMCP_App["OpenMCP App"]
renderer
openmcpservice
electron[Electron 代码]
end
subgraph QQ机器人["基于 OpenMCP 的 QQ 机器人"]
lagrange[Lagrange.OneBot]
openmcpservice
end
%% 依赖关系
OpenMCP_Web -->|前端渲染| renderer
OpenMCP_Web -->|后端服务| openmcpservice
OpenMCP_Web -->|反向代理| nginx
OpenMCP_插件 -->|UI 界面| renderer
OpenMCP_插件 -->|核心逻辑| openmcpservice
OpenMCP_插件 -->|集成开发| vscode
OpenMCP_App -->|前端界面| renderer
OpenMCP_App -->|本地服务| openmcpservice
OpenMCP_App -->|桌面封装| electron
QQ机器人 -->|协议适配| lagrange
QQ机器人 -->|业务逻辑| openmcpservice
```
---
## Dev ## Dev
@ -50,23 +136,22 @@ B <--mcp--> m(MCP Server)
配置项目 配置项目
```bash ```bash
source configure.sh ## linux
./configure.sh
## windows
./configure.ps1
``` ```
启动 dev server 启动 dev server
```bash ```bash
cd renderer ## linux
npm run serve ./dev.sh
``` ## windows
./dev.ps1
启动 service
```bash
cd service
npm run serve
``` ```
> 端口占用: 8080 (renderer) + 8081 (service)
### Extention Dev ### Extention Dev
@ -86,17 +171,3 @@ B <--mcp--> m(MCP Server)
``` ```
and just press f5, いただきます and just press f5, いただきます
## Flowchart
```mermaid
flowchart TB
A[用户输入问题] --> B[选择工具]
B --> C[大模型处理]
C --> D{是否有tool use?}
D -- 否 --> E[返回 content]
D -- 是 --> F[执行工具]
F --> G[返回工具执行结果]
G --> C
```

View File

@ -1,23 +1,33 @@
# Create resources directory if it doesn't exist # 创建并清理资源目录
New-Item -ItemType Directory -Force -Path .\resources | Out-Null New-Item -ItemType Directory -Path ./openmcp-sdk -Force
Remove-Item -Recurse -Force ./openmcp-sdk/* -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path ./openmcp-sdk -Force
# Start both build tasks in parallel # 获取当前工作目录的绝对路径
$jobs = @( $currentDir = (Get-Location).Path
Start-Job -ScriptBlock {
Set-Location $using:PWD\renderer # 并行构建 renderer 和 service
$rendererJob = Start-Job -ScriptBlock {
param($workDir)
Set-Location -Path "$workDir\renderer"
npm run build npm run build
Move-Item -Force -Path .\dist -Destination ..\resources\renderer Move-Item -Path "./dist" -Destination "$workDir\openmcp-sdk\renderer" -Force
} } -ArgumentList $currentDir
Start-Job -ScriptBlock {
Set-Location $using:PWD\service $serviceJob = Start-Job -ScriptBlock {
param($workDir)
Set-Location -Path "$workDir\service"
npm run build npm run build
Move-Item -Force -Path .\dist -Destination ..\resources\service Move-Item -Path "./dist" -Destination "$workDir\openmcp-sdk\service" -Force
} } -ArgumentList $currentDir
)
# Wait for all jobs to complete # 等待任务完成
Wait-Job -Job $jobs | Out-Null $rendererJob | Wait-Job | Receive-Job
Receive-Job -Job $jobs $serviceJob | Wait-Job | Receive-Job
Remove-Job -Job $jobs
Write-Host "finish building services in ./resources" # 将 openmcp-sdk 目录复制到 software/openmcp-sdk
New-Item -ItemType Directory -Path ./software/openmcp-sdk -Force
Remove-Item -Recurse -Force ./software/openmcp-sdk/* -ErrorAction SilentlyContinue
Copy-Item -Recurse -Path ./openmcp-sdk -Destination ./software/ -Force
Write-Output "finish building services in ./openmcp-sdk"

View File

@ -1,7 +1,16 @@
#!/bin/bash #!/bin/bash
mkdir -p ./resources mkdir -p ./openmcp-sdk
(cd ./renderer && npm run build && mv ./dist ../resources/renderer) & rm -rf ./openmcp-sdk/
(cd ./service && npm run build && mv ./dist ../resources/service) & mkdir -p ./openmcp-sdk
(cd ./renderer && npm run build && mv ./dist ../openmcp-sdk/renderer) &
(cd ./service && npm run build && mv ./dist ../openmcp-sdk/service) &
wait wait
echo "finish building services in ./resources"
mkdir -p ./software/openmcp-sdk
rm -rf ./software/openmcp-sdk
cp -r ./openmcp-sdk ./software/
echo "finish building services in ./openmcp-sdk"

17
configure.ps1 Normal file
View File

@ -0,0 +1,17 @@
# 安装 renderer 依赖
Set-Location renderer
npm i
Set-Location ..
# 安装 service 依赖并打补丁
Set-Location service
npm i
node patch-mcp-sdk.js
Set-Location ..
Set-Location servers
uv sync
Set-Location ..
# 安装根目录依赖
npm i

4
configure.sh Normal file → Executable file
View File

@ -1,3 +1,5 @@
cd renderer && npm i && cd .. cd renderer && npm i && cd ..
cd service && npm i && node patch-mcp-sdk.js && cd .. cd service && npm i && cd ..
cd servers && uv sync
npm i npm i
npm run prepare:ocr

7
dev.ps1 Normal file
View File

@ -0,0 +1,7 @@
npx concurrently `
-n "renderer,service" `
-p " {name} " `
-c "black.bgBlue,black.bgGreen" `
--kill-others `
"cd renderer && npm run serve" `
"cd service && npm run serve"

9
dev.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
npx concurrently \
-n "renderer,service" \
-p " {name} " \
-c "black.bgBlue,black.bgGreen" \
--kill-others \
"cd renderer && npm run serve" \
"cd service && npm run serve"

15
icons/dark/protocol.svg Normal file
View File

@ -0,0 +1,15 @@
<svg t="1745239051577" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1864" width="200" height="200">
<path
d="M504.45208357 26.71824755c-263.34919312 0-476.83711908 213.48792596-476.83711909 476.83711908s213.48792596 476.83711908 476.83711909 476.8371191 476.83711908-213.48792596 476.83711779-476.8371191S767.8012767 26.71824755 504.45208357 26.71824755zM94.36580322 676.77916517Q59.40410586 594.12104017 59.40410586 503.55536663q0-90.56567353 34.96169736-173.22379853 33.77755215-79.86068087 95.38967588-141.47280464 61.61212375-61.61212375 141.47280464-95.38967717Q413.88640875 58.5073889 504.45208357 58.5073889q90.56567353 0 173.22379854 34.96169739 79.86068087 33.77755215 141.47280463 95.38967717 61.61212375 61.61212375 95.3896759 141.47280464Q949.50006001 412.98969311 949.50006001 503.55536663q0 90.56567353-34.96169737 173.22221015-33.77755215 79.86227058-95.3896759 141.47439303-61.61212375 61.61212375-141.47280463 95.38967717Q595.0177571 948.60334436 504.45208357 948.60334436q-90.56567353 0-173.22221013-34.96169738-79.86227058-33.77755215-141.47439434-95.38967717-61.61212375-61.61212375-95.38967588-141.47280464z"
fill="#fefefe" p-id="1865"></path>
<path
d="M313.71723542 385.14081556c0-151.42757481 122.75376958-274.18134308 274.18134309-274.18134308 151.42598512 0 274.18134308 122.75376958 274.18134309 274.18134308 0 151.42598512-122.75535797 274.18134308-274.18134309 274.18134308-151.42757481 0-274.18134308-122.75535797-274.18134309-274.18134308z"
fill="#fefefe" p-id="1866"></path>
<path
d="M504.45208357 700.64804177c0-87.78253404 71.16317146-158.94570679 158.94570551-158.94570551s158.94570679 71.16317146 158.94570678 158.94570551-71.16317146 158.94570679-158.94570678 158.94570679-158.94570679-71.16317146-158.94570551-158.94570679zM103.90890375 542.49706496c0-48.72003846 39.49482864-88.21486711 88.21486711-88.21486711s88.21486711 39.49482864 88.21486581 88.21486711-39.49482864 88.21486711-88.21486581 88.21486711-88.21486711-39.49482864-88.21486711-88.21486711z"
fill="#fefefe" p-id="1867"></path>
<path
d="M625.25081944 323.94671893c0-50.0361088 40.56294439-90.59905319 90.59905319-90.59905319s90.59905319 40.56294439 90.5990519 90.59905319-40.56294439 90.59905319-90.5990519 90.5990519-90.59905319-40.56294439-90.59905319-90.5990519z"
fill="#fefefe" p-id="1868"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

15
icons/light/protocol.svg Normal file
View File

@ -0,0 +1,15 @@
<svg t="1745239051577" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1864" width="200" height="200">
<path
d="M504.45208357 26.71824755c-263.34919312 0-476.83711908 213.48792596-476.83711909 476.83711908s213.48792596 476.83711908 476.83711909 476.8371191 476.83711908-213.48792596 476.83711779-476.8371191S767.8012767 26.71824755 504.45208357 26.71824755zM94.36580322 676.77916517Q59.40410586 594.12104017 59.40410586 503.55536663q0-90.56567353 34.96169736-173.22379853 33.77755215-79.86068087 95.38967588-141.47280464 61.61212375-61.61212375 141.47280464-95.38967717Q413.88640875 58.5073889 504.45208357 58.5073889q90.56567353 0 173.22379854 34.96169739 79.86068087 33.77755215 141.47280463 95.38967717 61.61212375 61.61212375 95.3896759 141.47280464Q949.50006001 412.98969311 949.50006001 503.55536663q0 90.56567353-34.96169737 173.22221015-33.77755215 79.86227058-95.3896759 141.47439303-61.61212375 61.61212375-141.47280463 95.38967717Q595.0177571 948.60334436 504.45208357 948.60334436q-90.56567353 0-173.22221013-34.96169738-79.86227058-33.77755215-141.47439434-95.38967717-61.61212375-61.61212375-95.38967588-141.47280464z"
fill="#1e1e1e" p-id="1865"></path>
<path
d="M313.71723542 385.14081556c0-151.42757481 122.75376958-274.18134308 274.18134309-274.18134308 151.42598512 0 274.18134308 122.75376958 274.18134309 274.18134308 0 151.42598512-122.75535797 274.18134308-274.18134309 274.18134308-151.42757481 0-274.18134308-122.75535797-274.18134309-274.18134308z"
fill="#1e1e1e" p-id="1866"></path>
<path
d="M504.45208357 700.64804177c0-87.78253404 71.16317146-158.94570679 158.94570551-158.94570551s158.94570679 71.16317146 158.94570678 158.94570551-71.16317146 158.94570679-158.94570678 158.94570679-158.94570679-71.16317146-158.94570551-158.94570679zM103.90890375 542.49706496c0-48.72003846 39.49482864-88.21486711 88.21486711-88.21486711s88.21486711 39.49482864 88.21486581 88.21486711-39.49482864 88.21486711-88.21486581 88.21486711-88.21486711-39.49482864-88.21486711-88.21486711z"
fill="#1e1e1e" p-id="1867"></path>
<path
d="M625.25081944 323.94671893c0-50.0361088 40.56294439-90.59905319 90.59905319-90.59905319s90.59905319 40.56294439 90.5990519 90.59905319-40.56294439 90.59905319-90.5990519 90.5990519-90.59905319-40.56294439-90.59905319-90.5990519z"
fill="#1e1e1e" p-id="1868"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
icons/openmcp.chatbot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 54 KiB

BIN
icons/openmcp.resource.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 KiB

BIN
icons/openmcp.welcome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

View File

@ -1,22 +1 @@
<?xml version="1.0" encoding="utf-8"?> <svg t="1745239051577" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1864" width="200" height="200"><path d="M504.45208357 26.71824755c-263.34919312 0-476.83711908 213.48792596-476.83711909 476.83711908s213.48792596 476.83711908 476.83711909 476.8371191 476.83711908-213.48792596 476.83711779-476.8371191S767.8012767 26.71824755 504.45208357 26.71824755zM94.36580322 676.77916517Q59.40410586 594.12104017 59.40410586 503.55536663q0-90.56567353 34.96169736-173.22379853 33.77755215-79.86068087 95.38967588-141.47280464 61.61212375-61.61212375 141.47280464-95.38967717Q413.88640875 58.5073889 504.45208357 58.5073889q90.56567353 0 173.22379854 34.96169739 79.86068087 33.77755215 141.47280463 95.38967717 61.61212375 61.61212375 95.3896759 141.47280464Q949.50006001 412.98969311 949.50006001 503.55536663q0 90.56567353-34.96169737 173.22221015-33.77755215 79.86227058-95.3896759 141.47439303-61.61212375 61.61212375-141.47280463 95.38967717Q595.0177571 948.60334436 504.45208357 948.60334436q-90.56567353 0-173.22221013-34.96169738-79.86227058-33.77755215-141.47439434-95.38967717-61.61212375-61.61212375-95.38967588-141.47280464z" fill="#13227a" p-id="1865"></path><path d="M313.71723542 385.14081556c0-151.42757481 122.75376958-274.18134308 274.18134309-274.18134308 151.42598512 0 274.18134308 122.75376958 274.18134309 274.18134308 0 151.42598512-122.75535797 274.18134308-274.18134309 274.18134308-151.42757481 0-274.18134308-122.75535797-274.18134309-274.18134308z" fill="#13227a" p-id="1866"></path><path d="M504.45208357 700.64804177c0-87.78253404 71.16317146-158.94570679 158.94570551-158.94570551s158.94570679 71.16317146 158.94570678 158.94570551-71.16317146 158.94570679-158.94570678 158.94570679-158.94570679-71.16317146-158.94570551-158.94570679zM103.90890375 542.49706496c0-48.72003846 39.49482864-88.21486711 88.21486711-88.21486711s88.21486711 39.49482864 88.21486581 88.21486711-39.49482864 88.21486711-88.21486581 88.21486711-88.21486711-39.49482864-88.21486711-88.21486711z" fill="#13227a" p-id="1867"></path><path d="M625.25081944 323.94671893c0-50.0361088 40.56294439-90.59905319 90.59905319-90.59905319s90.59905319 40.56294439 90.5990519 90.59905319-40.56294439 90.59905319-90.5990519 90.5990519-90.59905319-40.56294439-90.59905319-90.5990519z" fill="#13227a" p-id="1868"></path></svg>
<svg viewBox="0 0 824 834" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="path_1" d="M300 0C465.708 0 600 134.292 600 300L600 300C600 465.708 465.708 600 300 600L300 600C134.292 600 0 465.708 0 300L0 300C0 134.292 134.292 0 300 0Z" />
<linearGradient id="gradient_2" gradientUnits="userSpaceOnUse" x1="300" y1="0" x2="300" y2="600">
<stop offset="0" stop-color="#A1A7F6" />
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0.2" />
</linearGradient>
</defs>
<g>
<g transform="translate(186 116)">
<use p4:href="#path_1" fill="#5A00FF" xmlns:p4="http://www.w3.org/1999/xlink" />
<use p4:href="#path_1" fill="url(#gradient_2)" xmlns:p4="http://www.w3.org/1999/xlink" />
</g>
<path d="M300 0C465.708 0 600 134.292 600 300L600 300C600 465.708 465.708 600 300 600L300 600C134.292 600 0 465.708 0 300L0 300C0 134.292 134.292 0 300 0Z" />
<path d="M0 110.5C0 49.4725 49.4725 0 110.5 0C171.527 0 221 49.4725 221 110.5C221 171.527 171.527 221 110.5 221C49.4725 221 0 171.527 0 110.5Z" fill="#FFFFFF" fill-rule="evenodd" fill-opacity="0.431" transform="translate(445 458)" />
<path d="M0 55.5C0 24.8482 24.8482 0 55.5 0C86.1518 0 111 24.8482 111 55.5C111 86.1518 86.1518 111 55.5 111C24.8482 111 0 86.1518 0 55.5Z" fill="#FFFFFF" fill-rule="evenodd" fill-opacity="0.431" transform="translate(199 386)" />
<path d="M0 182.5C0 81.708 81.708 0 182.5 0C283.292 0 365 81.708 365 182.5C365 283.292 283.292 365 182.5 365C81.708 365 0 283.292 0 182.5Z" fill="#FFFFFF" fill-rule="evenodd" fill-opacity="0.424" transform="translate(339 156)" />
<path d="M0 57C0 25.5198 25.5198 0 57 0C88.4802 0 114 25.5198 114 57C114 88.4802 88.4802 114 57 114C25.5198 114 0 88.4802 0 57Z" fill="#FFFFFF" fill-rule="evenodd" fill-opacity="0.431" transform="translate(521 188)" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

505
package-lock.json generated
View File

@ -1,18 +1,21 @@
{ {
"name": "openmcp", "name": "openmcp",
"version": "0.0.1", "version": "0.0.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "openmcp", "name": "openmcp",
"version": "0.0.1", "version": "0.0.6",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.9.0", "@modelcontextprotocol/sdk": "^1.10.2",
"@seald-io/nedb": "^4.1.1",
"axios": "^1.7.7", "axios": "^1.7.7",
"bson": "^6.8.0", "bson": "^6.8.0",
"openai": "^4.93.0", "openai": "^4.93.0",
"pako": "^2.1.0", "pako": "^2.1.0",
"tesseract.js": "^6.0.1",
"uuid": "^11.1.0",
"ws": "^8.18.1" "ws": "^8.18.1"
}, },
"devDependencies": { "devDependencies": {
@ -20,9 +23,10 @@
"@types/pako": "^2.0.3", "@types/pako": "^2.0.3",
"@types/showdown": "^2.0.0", "@types/showdown": "^2.0.0",
"@types/vscode": "^1.72.0", "@types/vscode": "^1.72.0",
"copy-webpack-plugin": "^13.0.0",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"typescript": "^5.4.2", "typescript": "^5.4.2",
"webpack": "^5.88.2", "webpack": "^5.99.5",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
}, },
"engines": { "engines": {
@ -97,9 +101,9 @@
} }
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "1.9.0", "version": "1.10.2",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz",
"integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==", "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"content-type": "^1.0.5", "content-type": "^1.0.5",
@ -117,6 +121,22 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@seald-io/binary-search-tree": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@seald-io/binary-search-tree/-/binary-search-tree-1.0.3.tgz",
"integrity": "sha512-qv3jnwoakeax2razYaMsGI/luWdliBLHTdC6jU55hQt1hcFqzauH/HsBollQ7IR4ySTtYhT+xyHoijpA16C+tA=="
},
"node_modules/@seald-io/nedb": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/@seald-io/nedb/-/nedb-4.1.1.tgz",
"integrity": "sha512-u7fVfzKQ/3ZaIOnYQONf2lPZtGUeQtMPjfcaQkCw/GZv5dzn20qKW6sfN0NkVbr0ksJMlWcFXNGcXYsQSb1a1g==",
"license": "MIT",
"dependencies": {
"@seald-io/binary-search-tree": "^1.0.3",
"localforage": "^1.9.0",
"util": "^0.12.4"
}
},
"node_modules/@types/eslint": { "node_modules/@types/eslint": {
"version": "9.6.1", "version": "9.6.1",
"resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-9.6.1.tgz", "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-9.6.1.tgz",
@ -524,6 +544,21 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
"integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
"license": "MIT",
"dependencies": {
"possible-typed-array-names": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/axios": { "node_modules/axios": {
"version": "1.8.4", "version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
@ -535,6 +570,12 @@
"proxy-from-env": "^1.1.0" "proxy-from-env": "^1.1.0"
} }
}, },
"node_modules/bmp-js": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
"integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==",
"license": "MIT"
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@ -623,6 +664,24 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-bind-apply-helpers": { "node_modules/call-bind-apply-helpers": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@ -792,6 +851,30 @@
"node": ">=6.6.0" "node": ">=6.6.0"
} }
}, },
"node_modules/copy-webpack-plugin": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.0.tgz",
"integrity": "sha512-FgR/h5a6hzJqATDGd9YG41SeDViH+0bkHn6WNXCi5zKAZkeESeSxLySSsFLHqLEVCh0E+rITmCf0dusXWYukeQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"glob-parent": "^6.0.1",
"normalize-path": "^3.0.0",
"schema-utils": "^4.2.0",
"serialize-javascript": "^6.0.2",
"tinyglobby": "^0.2.12"
},
"engines": {
"node": ">= 18.12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.1.0"
}
},
"node_modules/cors": { "node_modules/cors": {
"version": "2.8.5", "version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
@ -836,6 +919,23 @@
} }
} }
}, },
"node_modules/define-data-property": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"gopd": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -889,6 +989,17 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/encoding": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"iconv-lite": "^0.6.2"
}
},
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
"version": "5.18.1", "version": "5.18.1",
"resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", "resolved": "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@ -1251,6 +1362,21 @@
} }
} }
}, },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
"integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
"license": "MIT",
"dependencies": {
"is-callable": "^1.2.7"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
@ -1349,6 +1475,19 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/glob-to-regexp": { "node_modules/glob-to-regexp": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
@ -1382,6 +1521,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/has-property-descriptors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"license": "MIT",
"dependencies": {
"es-define-property": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": { "node_modules/has-symbols": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -1458,6 +1609,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/idb-keyval": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
"integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==",
"license": "Apache-2.0"
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-local": { "node_modules/import-local": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.2.0.tgz", "resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.2.0.tgz",
@ -1501,6 +1664,34 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-callable": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
"integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
@ -1516,6 +1707,47 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-generator-function": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
"integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.3",
"get-proto": "^1.0.0",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-number": { "node_modules/is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
@ -1543,6 +1775,45 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
"integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-typed-array": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
"integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
"license": "MIT",
"dependencies": {
"which-typed-array": "^1.1.16"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
"license": "MIT"
},
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -1608,6 +1879,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/loader-runner": { "node_modules/loader-runner": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz", "resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz",
@ -1617,6 +1897,15 @@
"node": ">=6.11.5" "node": ">=6.11.5"
} }
}, },
"node_modules/localforage": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"license": "Apache-2.0",
"dependencies": {
"lie": "3.1.1"
}
},
"node_modules/locate-path": { "node_modules/locate-path": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
@ -1765,6 +2054,16 @@
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true "dev": true
}, },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -1846,6 +2145,15 @@
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
}, },
"node_modules/opencollective-postinstall": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
"integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
"license": "MIT",
"bin": {
"opencollective-postinstall": "index.js"
}
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
@ -1969,6 +2277,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
"integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -2048,6 +2365,12 @@
"node": ">= 10.13.0" "node": ">= 10.13.0"
} }
}, },
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
"license": "MIT"
},
"node_modules/require-from-string": { "node_modules/require-from-string": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
@ -2134,6 +2457,23 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/safe-regex-test": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
"integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"is-regex": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@ -2238,6 +2578,23 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"license": "MIT",
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
"gopd": "^1.0.1",
"has-property-descriptors": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -2471,6 +2828,75 @@
} }
} }
}, },
"node_modules/tesseract.js": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.1.tgz",
"integrity": "sha512-/sPvMvrCtgxnNRCjbTYbr7BRu0yfWDsMZQ2a/T5aN/L1t8wUQN6tTWv6p6FwzpoEBA0jrN2UD2SX4QQFRdoDbA==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"bmp-js": "^0.1.0",
"idb-keyval": "^6.2.0",
"is-url": "^1.2.4",
"node-fetch": "^2.6.9",
"opencollective-postinstall": "^2.0.3",
"regenerator-runtime": "^0.13.3",
"tesseract.js-core": "^6.0.0",
"wasm-feature-detect": "^1.2.11",
"zlibjs": "^0.3.1"
}
},
"node_modules/tesseract.js-core": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz",
"integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==",
"license": "Apache-2.0"
},
"node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
@ -2611,6 +3037,32 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
"integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"is-arguments": "^1.0.4",
"is-generator-function": "^1.0.7",
"is-typed-array": "^1.1.3",
"which-typed-array": "^1.1.2"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -2620,6 +3072,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/wasm-feature-detect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
"license": "Apache-2.0"
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.2.tgz",
@ -2650,9 +3108,10 @@
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.99.5", "version": "5.99.5",
"resolved": "https://registry.npmmirror.com/webpack/-/webpack-5.99.5.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.5.tgz",
"integrity": "sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg==", "integrity": "sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
@ -2796,6 +3255,27 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/which-typed-array": {
"version": "1.1.19",
"resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz",
"integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==",
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
"call-bind": "^1.0.8",
"call-bound": "^1.0.4",
"for-each": "^0.3.5",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wildcard": { "node_modules/wildcard": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.1.tgz", "resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-2.0.1.tgz",
@ -2828,6 +3308,15 @@
} }
} }
}, },
"node_modules/zlibjs": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
"integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/zod": { "node_modules/zod": {
"version": "3.24.2", "version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",

View File

@ -2,7 +2,7 @@
"name": "openmcp", "name": "openmcp",
"displayName": "OpenMCP", "displayName": "OpenMCP",
"description": "An all in one MCP Client/TestTool", "description": "An all in one MCP Client/TestTool",
"version": "0.0.1", "version": "0.0.7",
"publisher": "kirigaya", "publisher": "kirigaya",
"author": { "author": {
"name": "kirigaya", "name": "kirigaya",
@ -28,9 +28,81 @@
"title": "展示 OpenMCP", "title": "展示 OpenMCP",
"category": "openmcp", "category": "openmcp",
"icon": { "icon": {
"light": "./icons/protocol.svg", "light": "./icons/light/protocol.svg",
"dark": "./icons/protocol.svg" "dark": "./icons/dark/protocol.svg"
} }
},
{
"command": "openmcp.sidebar.workspace-connection.revealWebviewPanel",
"title": "连接",
"category": "openmcp",
"icon": {
"light": "./icons/light/protocol.svg",
"dark": "./icons/dark/protocol.svg"
}
},
{
"command": "openmcp.sidebar.workspace-connection.deleteConnection",
"title": "删除连接",
"category": "openmcp",
"icon": "$(trash)"
},
{
"command": "openmcp.sidebar.workspace-connection.refresh",
"title": "刷新",
"category": "openmcp",
"icon": "$(refresh)"
},
{
"command": "openmcp.sidebar.workspace-connection.addConnection",
"title": "添加连接",
"category": "openmcp",
"icon": "$(add)"
},
{
"command": "openmcp.sidebar.workspace-connection.openConfiguration",
"title": "打开配置",
"category": "openmcp",
"icon": "$(gear)"
},
{
"command": "openmcp.sidebar.installed-connection.revealWebviewPanel",
"title": "连接",
"category": "openmcp",
"icon": {
"light": "./icons/light/protocol.svg",
"dark": "./icons/dark/protocol.svg"
}
},
{
"command": "openmcp.sidebar.installed-connection.deleteConnection",
"title": "删除连接",
"category": "openmcp",
"icon": "$(trash)"
},
{
"command": "openmcp.sidebar.installed-connection.refresh",
"title": "刷新",
"category": "openmcp",
"icon": "$(refresh)"
},
{
"command": "openmcp.sidebar.installed-connection.addConnection",
"title": "添加连接",
"category": "openmcp",
"icon": "$(add)"
},
{
"command": "openmcp.sidebar.installed-connection.openConfiguration",
"title": "打开配置",
"category": "openmcp",
"icon": "$(gear)"
},
{
"command": "openmcp.hook.test-ocr",
"title": "测试 OCR",
"category": "openmcp",
"icon": "$(test)"
} }
], ],
"menus": { "menus": {
@ -40,6 +112,72 @@
"group": "navigation", "group": "navigation",
"when": "editorLangId == python || editorLangId == javascript || editorLangId == typescript || editorLangId == java || editorLangId == csharp" "when": "editorLangId == python || editorLangId == javascript || editorLangId == typescript || editorLangId == java || editorLangId == csharp"
} }
],
"view/title": [
{
"command": "openmcp.sidebar.workspace-connection.refresh",
"group": "navigation",
"when": "view == openmcp.sidebar.workspace-connection"
},
{
"command": "openmcp.sidebar.workspace-connection.addConnection",
"group": "navigation",
"when": "view == openmcp.sidebar.workspace-connection"
},
{
"command": "openmcp.sidebar.workspace-connection.openConfiguration",
"group": "navigation",
"when": "view == openmcp.sidebar.workspace-connection"
},
{
"command": "openmcp.sidebar.installed-connection.refresh",
"group": "navigation",
"when": "view == openmcp.sidebar.installed-connection"
},
{
"command": "openmcp.sidebar.installed-connection.addConnection",
"group": "navigation",
"when": "view == openmcp.sidebar.installed-connection"
},
{
"command": "openmcp.sidebar.installed-connection.openConfiguration",
"group": "navigation",
"when": "view == openmcp.sidebar.installed-connection"
}
],
"view/item/context": [
{
"command": "openmcp.sidebar.workspace-connection.revealWebviewPanel",
"group": "inline@1",
"when": "view == openmcp.sidebar.workspace-connection",
"args": {
"view": "${viewItem}"
}
},
{
"command": "openmcp.sidebar.workspace-connection.deleteConnection",
"group": "inline@2",
"when": "view == openmcp.sidebar.workspace-connection",
"args": {
"view": "${viewItem}"
}
},
{
"command": "openmcp.sidebar.installed-connection.revealWebviewPanel",
"group": "inline@1",
"when": "view == openmcp.sidebar.installed-connection",
"args": {
"view": "${viewItem}"
}
},
{
"command": "openmcp.sidebar.installed-connection.deleteConnection",
"group": "inline@2",
"when": "view == openmcp.sidebar.installed-connection",
"args": {
"view": "${viewItem}"
}
}
] ]
}, },
"viewsContainers": { "viewsContainers": {
@ -54,10 +192,22 @@
"views": { "views": {
"openmcp-sidebar": [ "openmcp-sidebar": [
{ {
"id": "webview-sidebar.view", "id": "openmcp.sidebar.workspace-connection",
"icon": "./icons/protocol.svg", "icon": "./icons/protocol.svg",
"name": "chatbot", "name": "MCP 连接 (工作区)",
"type": "webview" "type": "tree"
},
{
"id": "openmcp.sidebar.installed-connection",
"icon": "./icons/protocol.svg",
"name": "安装的 MCP 服务器",
"type": "tree"
},
{
"id": "openmcp.sidebar.help",
"icon": "./icons/protocol.svg",
"name": "入门与帮助",
"type": "tree"
} }
] ]
} }
@ -68,14 +218,18 @@
"watch": "tsc -watch -p ./", "watch": "tsc -watch -p ./",
"pretest": "npm run compile && npm run lint", "pretest": "npm run compile && npm run lint",
"lint": "eslint src --ext ts", "lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js" "test": "node ./out/test/runTest.js",
"prepare:ocr": "webpack --config webpack/webpack.tesseract.js"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.9.0", "@modelcontextprotocol/sdk": "^1.10.2",
"@seald-io/nedb": "^4.1.1",
"axios": "^1.7.7", "axios": "^1.7.7",
"bson": "^6.8.0", "bson": "^6.8.0",
"openai": "^4.93.0", "openai": "^4.93.0",
"pako": "^2.1.0", "pako": "^2.1.0",
"tesseract.js": "^6.0.1",
"uuid": "^11.1.0",
"ws": "^8.18.1" "ws": "^8.18.1"
}, },
"devDependencies": { "devDependencies": {
@ -83,9 +237,10 @@
"@types/pako": "^2.0.3", "@types/pako": "^2.0.3",
"@types/showdown": "^2.0.0", "@types/showdown": "^2.0.0",
"@types/vscode": "^1.72.0", "@types/vscode": "^1.72.0",
"copy-webpack-plugin": "^13.0.0",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"typescript": "^5.4.2", "typescript": "^5.4.2",
"webpack": "^5.88.2", "webpack": "^5.99.5",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
} }
} }

View File

@ -9,16 +9,19 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"core-js": "^3.8.3", "core-js": "^3.8.3",
"element-plus": "^2.9.7", "element-plus": "^2.9.9",
"katex": "^0.16.21", "katex": "^0.16.21",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markdown-it-katex": "^2.0.3", "markdown-it-katex": "^2.0.3",
"openai": "^4.93.0", "openai": "^4.93.0",
"uuid": "^11.1.0",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-i18n": "^11.1.0", "vue-i18n": "^11.1.0",
"vue-router": "^4.0.3" "vue-router": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.17.16",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/parser": "^5.4.0",
@ -6116,9 +6119,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/element-plus": { "node_modules/element-plus": {
"version": "2.9.7", "version": "2.9.9",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.7.tgz", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.9.tgz",
"integrity": "sha512-6vjZh5SXBncLhUwJGTVKS5oDljfgGMh6J4zVTeAZK3YdMUN76FgpvHkwwFXocpJpMbii6rDYU3sgie64FyPerQ==", "integrity": "sha512-gN553+xr7ETkhJhH26YG0fERmd2BSCcQKslbtR8fats0Mc0yCtZOXr00bmoPOt5xGzhuRN1TWc9+f1pCaiA0/Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^3.4.1", "@ctrl/tinycolor": "^3.4.1",
@ -8689,7 +8692,7 @@
}, },
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
@ -11820,6 +11823,16 @@
"websocket-driver": "^0.7.4" "websocket-driver": "^0.7.4"
} }
}, },
"node_modules/sockjs/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"dev": true,
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
@ -12970,13 +12983,16 @@
} }
}, },
"node_modules/uuid": { "node_modules/uuid": {
"version": "8.3.2", "version": "11.1.0",
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"dev": true, "funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"uuid": "dist/bin/uuid" "uuid": "dist/esm/bin/uuid"
} }
}, },
"node_modules/v8-compile-cache": { "node_modules/v8-compile-cache": {

View File

@ -9,16 +9,19 @@
}, },
"dependencies": { "dependencies": {
"core-js": "^3.8.3", "core-js": "^3.8.3",
"element-plus": "^2.9.7", "element-plus": "^2.9.9",
"katex": "^0.16.21", "katex": "^0.16.21",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markdown-it-katex": "^2.0.3", "markdown-it-katex": "^2.0.3",
"openai": "^4.93.0", "openai": "^4.93.0",
"uuid": "^11.1.0",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-i18n": "^11.1.0", "vue-i18n": "^11.1.0",
"vue-router": "^4.0.3" "vue-router": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.17.16",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/parser": "^5.4.0",

View File

@ -22,7 +22,7 @@
--vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6); --vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6);
--vscode-progressBar-background: #0e70c0; --vscode-progressBar-background: #0e70c0;
--vscode-editor-background: #ffffff; --vscode-editor-background: #ffffff;
--vscode-editor-foreground: #000000; --vscode-editor-foreground: #3d3d3d;
--vscode-editorStickyScroll-background: #ffffff; --vscode-editorStickyScroll-background: #ffffff;
--vscode-editorStickyScrollHover-background: #f0f0f0; --vscode-editorStickyScrollHover-background: #f0f0f0;
--vscode-editorStickyScroll-shadow: #dddddd; --vscode-editorStickyScroll-shadow: #dddddd;

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4870215 */ font-family: "iconfont"; /* Project id 4870215 */
src: url('iconfont.woff2?t=1744476757936') format('woff2'), src: url('iconfont.woff2?t=1746529081655') format('woff2'),
url('iconfont.woff?t=1744476757936') format('woff'), url('iconfont.woff?t=1746529081655') format('woff'),
url('iconfont.ttf?t=1744476757936') format('truetype'); url('iconfont.ttf?t=1746529081655') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,78 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-timeout:before {
content: "\edf5";
}
.icon-dui:before {
content: "\e627";
}
.icon-cuo:before {
content: "\ed1a";
}
.icon-video:before {
content: "\e865";
}
.icon-image:before {
content: "\ebc7";
}
.icon-audio:before {
content: "\e768";
}
.icon-error:before {
content: "\e6c6";
}
.icon-warning:before {
content: "\e681";
}
.icon-copy:before {
content: "\e77c";
}
.icon-restart:before {
content: "\e86b";
}
.icon-edit2:before {
content: "\e848";
}
.icon-star:before {
content: "\e80f";
}
.icon-prompt:before {
content: "\eb50";
}
.icon-empty:before {
content: "\e698";
}
.icon-test:before {
content: "\e8ad";
}
.icon-save:before {
content: "\e67c";
}
.icon-more:before {
content: "\e612";
}
.icon-edit:before {
content: "\e63b";
}
.icon-connect:before { .icon-connect:before {
content: "\ecda"; content: "\ecda";
} }

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -5,6 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="referrer" content="no-referrer">
<link rel="icon" href="<%= BASE_URL %>favicon.svg"> <link rel="icon" href="<%= BASE_URL %>favicon.svg">
<link rel="stylesheet" href="default-dark.css"> <link rel="stylesheet" href="default-dark.css">
<link rel="stylesheet" href="vscode.css"> <link rel="stylesheet" href="vscode.css">

View File

@ -65,7 +65,6 @@ body::-webkit-scrollbar {
} }
.el-textarea__inner { .el-textarea__inner {
border-radius: .9em !important;
padding: 10px !important; padding: 10px !important;
box-shadow: 0 0 0 1px var(--main-color) !important; box-shadow: 0 0 0 1px var(--main-color) !important;
} }
@ -124,7 +123,7 @@ a {
} }
.openmcp-code-block pre code { .openmcp-code-block pre code {
background-color: none; background-color: transparent !important;
} }
.openmcp-code-block pre code::-webkit-scrollbar { .openmcp-code-block pre code::-webkit-scrollbar {
@ -133,8 +132,7 @@ a {
} }
.tool-arguments .openmcp-code-block pre code { .tool-arguments .openmcp-code-block pre code {
background: var(--el-fill-color-light); background: transparent !important;
} }
.openmcp-code-block pre code::-webkit-scrollbar-track { .openmcp-code-block pre code::-webkit-scrollbar-track {
@ -206,39 +204,17 @@ a {
background-position: center; background-position: center;
} }
.server-icon { .openmcp-image {
background-image: url('https://picx.zhimg.com/80/v2-a0aa51e8a61f86586e374520995b5df5_1440w.png?source=d16d100b');
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center; background-position: center;
width: 20px;
height: 20px;
margin-right: 8px;
} }
.deepseek-icon { .el-button:hover {
background-image: url('./images/deepseek.com.ico'); color: var(--foreground) !important;
} }
.openai-icon { .el-dropdown-menu__item:hover {
background-image: url('./images/openai.com.ico'); background-color: var(--background) !important;
}
.mistral-icon {
background-image: url('./images/mistral.ai.ico');
}
.ollama-icon {
background-image: url('./images/ollama.png');
}
.grop-icon {
background-image: url('./images/grok.com.png');
}
.perplexity-icon {
background-image: url('./images/perplexity.ai.ico');
}
.kimi-icon {
background-image: url('./images/kimichat.cn.png');
} }

View File

@ -2,78 +2,49 @@
<div class="main"> <div class="main">
<Sidebar></Sidebar> <Sidebar></Sidebar>
<MainPanel></MainPanel> <MainPanel></MainPanel>
<Tour v-if="!userHasReadGuide"/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Connection } from './components/sidebar/sidebar'; import { Connection } from './components/sidebar/sidebar';
import Sidebar from '@/components/sidebar/index.vue'; import Sidebar from '@/components/sidebar/index.vue';
import MainPanel from '@/components/main-panel/index.vue'; import MainPanel from '@/components/main-panel/index.vue';
import { setDefaultCss } from './hook/css'; import { setDefaultCss } from './hook/css';
import { pinkLog } from './views/setting/util'; import { greenLog, pinkLog } from './views/setting/util';
import { acquireVsCodeApi, useMessageBridge } from './api/message-bridge'; import { useMessageBridge } from './api/message-bridge';
import { connectionArgs, connectionMethods, connectionResult, doConnect, getServerVersion, launchConnect } from './views/connect/connection'; import { doConnect, loadEnvVar } from './views/connect/connection';
import { loadSetting } from './hook/setting'; import { getTour, loadSetting } from './hook/setting';
import { loadPanels } from './hook/panel'; import { loadPanels } from './hook/panel';
import { getPlatform } from './api/platform';
import Tour from '@/components/guide/tour.vue';
import { userHasReadGuide } from './components/guide/tour';
import { ElLoading } from 'element-plus';
const bridge = useMessageBridge(); const bridge = useMessageBridge();
// //
bridge.addCommandListener('hello', data => { bridge.addCommandListener('hello', data => {
pinkLog(`${data.name} 上线`); greenLog(`${data.name}`);
pinkLog(`version: ${data.version}`); greenLog(`version: ${data.version}`);
}, { once: true }); }, { once: true });
// connect const route = useRoute();
bridge.addCommandListener('connect', async data => { const router = useRouter();
const { code, msg } = data;
connectionResult.success = (code === 200);
connectionResult.logString = msg;
const res = await getServerVersion() as { name: string, version: string }; onMounted(async () => {
connectionResult.serverInfo.name = res.name || ''; const loading = ElLoading.service({
connectionResult.serverInfo.version = res.version || ''; fullscreen: true,
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)'
});
}, { once: true });
function initDebug() {
connectionArgs.commandString = 'mcp run ../servers/main.py';
connectionMethods.current = 'STDIO';
setTimeout(() => {
//
loadSetting();
// tab
loadPanels();
//
doConnect();
// 200 ws ws
//
}, 200);
}
function initProduce() {
// TODO: get from vscode
connectionArgs.commandString = 'mcp run ../servers/main.py';
connectionMethods.current = 'STDIO';
//
loadSetting();
// tab
loadPanels();
launchConnect();
}
onMounted(() => {
// css // css
setDefaultCss(); setDefaultCss();
@ -83,19 +54,57 @@ onMounted(() => {
pinkLog('OpenMCP Client 启动'); pinkLog('OpenMCP Client 启动');
if (acquireVsCodeApi === undefined) { const platform = getPlatform();
initDebug();
} else { //
initProduce(); if (platform !== 'web') {
if (route.name !== 'debug') {
router.replace('/debug');
router.push('/debug');
} }
}
//
await bridge.awaitForWebsockt();
pinkLog('准备请求设置');
//
loadSetting();
//
loadEnvVar();
//
getTour();
//
await doConnect({
namespace: platform,
updateCommandString: true
});
// loading panels
await loadPanels();
loading.close();
}); });
</script> </script>
<style> <style>
.main { .main {
height: calc(100vh - 50px); height: calc(100vh - 5px);
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.message-text img {
max-width: 300px;
}
.icon-chat:before {
font-weight: 1000;
}
</style> </style>

View File

@ -1,5 +1,6 @@
import { pinkLog } from '@/views/setting/util'; import { pinkLog, redLog } from '@/views/setting/util';
import { onUnmounted, ref } from 'vue'; import { acquireVsCodeApi, electronApi, getPlatform } from './platform';
import { ref } from 'vue';
export interface VSCodeMessage { export interface VSCodeMessage {
command: string; command: string;
@ -7,11 +8,14 @@ export interface VSCodeMessage {
callbackId?: string; callbackId?: string;
} }
export interface RestFulResponse {
code: number;
msg: any;
}
export type MessageHandler = (message: VSCodeMessage) => void; export type MessageHandler = (message: VSCodeMessage) => void;
export type CommandHandler = (data: any) => void; export type CommandHandler = (data: any) => void;
export const acquireVsCodeApi = (window as any)['acquireVsCodeApi'];
interface AddCommandListenerOption { interface AddCommandListenerOption {
once: boolean // 只调用一次就销毁 once: boolean // 只调用一次就销毁
} }
@ -19,27 +23,36 @@ interface AddCommandListenerOption {
class MessageBridge { class MessageBridge {
private ws: WebSocket | null = null; private ws: WebSocket | null = null;
private handlers = new Map<string, Set<CommandHandler>>(); private handlers = new Map<string, Set<CommandHandler>>();
public isConnected = ref(false); private isConnected: Promise<boolean> | null = null;
constructor(private wsUrl: string = 'ws://localhost:8080') { constructor(private wsUrl: string = 'ws://localhost:8080') {
this.init();
}
private init() {
// 环境检测优先级: // 环境检测优先级:
// 1. VS Code WebView 环境 // 1. VS Code WebView 环境
// 2. 浏览器 WebSocket 环境 // 2. 浏览器 WebSocket 环境
if (typeof acquireVsCodeApi !== 'undefined') {
this.setupVSCodeListener(); const platform = getPlatform();
pinkLog('当前模式release');
} else { switch (platform) {
case 'vscode':
this.setupVsCodeListener();
pinkLog('当前模式: vscode');
break;
case 'electron':
this.setupElectronListener();
pinkLog('当前模式: electron');
break;
case 'web':
this.setupWebSocket(); this.setupWebSocket();
pinkLog('当前模式debug'); pinkLog('当前模式: web');
break;
} }
} }
// VS Code 环境监听 // VS Code 环境监听
private setupVSCodeListener() { private setupVsCodeListener() {
const vscode = acquireVsCodeApi(); const vscode = acquireVsCodeApi();
window.addEventListener('message', (event: MessageEvent<VSCodeMessage>) => { window.addEventListener('message', (event: MessageEvent<VSCodeMessage>) => {
@ -47,37 +60,58 @@ class MessageBridge {
}); });
this.postMessage = (message) => vscode.postMessage(message); this.postMessage = (message) => vscode.postMessage(message);
this.isConnected.value = true;
} }
// WebSocket 环境连接 // WebSocket 环境连接
private setupWebSocket() { private setupWebSocket() {
this.ws = new WebSocket(this.wsUrl); this.ws = new WebSocket(this.wsUrl);
this.ws.onopen = () => {
this.isConnected.value = true;
};
this.ws.onmessage = (event) => { this.ws.onmessage = (event) => {
try { try {
const message = JSON.parse(event.data) as VSCodeMessage; const message = JSON.parse(event.data) as VSCodeMessage;
this.dispatchMessage(message); this.dispatchMessage(message);
} catch (err) { } catch (err) {
console.error('Message parse error:', err); console.error('Message parse error:', err);
console.log(event);
} }
}; };
this.ws.onclose = () => { this.ws.onclose = () => {
this.isConnected.value = false; redLog('WebSocket connection closed');
}; };
this.postMessage = (message) => { this.postMessage = (message) => {
if (this.ws?.readyState === WebSocket.OPEN) { if (this.ws?.readyState === WebSocket.OPEN) {
console.log(message); console.log('send', message);
this.ws.send(JSON.stringify(message)); this.ws.send(JSON.stringify(message));
} }
}; };
const ws = this.ws;
this.isConnected = new Promise<boolean>((resolve, reject) => {
ws.onopen = () => {
resolve(true);
};
});
}
public async awaitForWebsockt() {
if (this.isConnected) {
await this.isConnected;
}
}
private setupElectronListener() {
electronApi.onReply((event: MessageEvent<VSCodeMessage>) => {
console.log(event);
this.dispatchMessage(event.data);
});
this.postMessage = (message) => {
console.log(message);
electronApi.sendToMain(message);
};
} }
/** /**
@ -122,6 +156,7 @@ class MessageBridge {
if (!this.handlers.has(command)) { if (!this.handlers.has(command)) {
this.handlers.set(command, new Set<CommandHandler>()); this.handlers.set(command, new Set<CommandHandler>());
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const commandHandlers = this.handlers.get(command)!; const commandHandlers = this.handlers.get(command)!;
const wrapperCommandHandler = option.once ? (data: any) => { const wrapperCommandHandler = option.once ? (data: any) => {
@ -133,6 +168,25 @@ class MessageBridge {
return () => commandHandlers.delete(wrapperCommandHandler); return () => commandHandlers.delete(wrapperCommandHandler);
} }
/**
* @description do as axios does
* @param command
* @param data
* @returns
*/
public commandRequest(command: string, data?: any) {
return new Promise<RestFulResponse>((resolve, reject) => {
this.addCommandListener(command, (data) => {
resolve(data as RestFulResponse);
}, { once: true });
this.postMessage({
command,
data
});
});
}
public destroy() { public destroy() {
this.ws?.close(); this.ws?.close();
this.handlers.clear(); this.handlers.clear();
@ -149,6 +203,7 @@ export function useMessageBridge() {
return { return {
postMessage: bridge.postMessage.bind(bridge), postMessage: bridge.postMessage.bind(bridge),
addCommandListener: bridge.addCommandListener.bind(bridge), addCommandListener: bridge.addCommandListener.bind(bridge),
isConnected: bridge.isConnected commandRequest: bridge.commandRequest.bind(bridge),
awaitForWebsockt: bridge.awaitForWebsockt.bind(bridge)
}; };
} }

View File

@ -0,0 +1,15 @@
export type OpenMcpSupportPlatform = 'web' | 'vscode' | 'electron';
export const acquireVsCodeApi = (window as any)['acquireVsCodeApi'];
export const electronApi = (window as any)['electronApi'];
export function getPlatform(): OpenMcpSupportPlatform {
if (typeof acquireVsCodeApi !== 'undefined') {
return 'vscode';
} else if (typeof electronApi !== 'undefined') {
return 'electron';
} else {
return 'web';
}
}

View File

@ -0,0 +1,25 @@
<template>
<div class="tour-title">
<span class="openmcp-image tour-title-icon"></span>
<slot></slot>
</div>
</template>
<script setup lang="ts">
</script>
<style>
.tour-title {
display: flex;
align-items: center;
margin: 10px 0;
font-size: 18px;
}
.tour-title-icon {
height: 45px;
width: 45px;
margin-right: 5px;
}
</style>

View File

@ -0,0 +1,3 @@
import { ref } from "vue";
export const userHasReadGuide = ref(true);

View File

@ -0,0 +1,291 @@
<template>
<el-tour v-model="openTour">
<el-tour-step
:next-button-props="{ children: '开始' }"
:show-close="false"
>
<template #header>
<TourTitle>介绍</TourTitle>
</template>
<div style="display: flex; padding: 10px; padding-bottom: 20px;">
<div class="tour-common-text">
欢迎来到大模型与 mcp 的世界
<br><br>
OpenMCP 将会助力你快速将任何奇思妙想开发成 mcp 服务器通过接入大模型让你的任何 idea 都可以快速落地
<br><br>
倘若阁下是第一次使用 OpenMCP请务必走完我们准备好的引导
</div>
</div>
</el-tour-step>
<el-tour-step
target="#connected-status-container"
:prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步' }"
:show-close="false"
>
<template #header>
<TourTitle>引导</TourTitle>
</template>
<div class="tour-common-text">
这里会显示当前调试的 mcp 服务器的名称缩写和连接状态只有当连接状态为已连接调试工作才能开始
<br><br>
OpenMCP 通过服务器名称对项目所的所有服务进行统一管理请避免在同一个项目中使用相同的名称
</div>
</el-tour-step>
<el-tour-step
target="#sidebar-connect"
:prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步', onClick: () => router.push('/connect') }"
:show-close="false"
>
<template #header>
<TourTitle>连接</TourTitle>
</template>
<div class="tour-common-text">
如果显示未连接或阁下想要更改连接参数或者连接方式可以点击这里进入连接面板
</div>
</el-tour-step>
<el-tour-step
:target="connectionSettingRef"
:prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步' }"
:show-close="false"
placement="right"
>
<template #header>
<TourTitle>连接</TourTitle>
</template>
<div class="tour-common-text">
阁下可以在左侧面板选择与您心爱的 mcp 服务器进行连接的方式并填入对应的连接参数
<br><br>
对于 openmcp vscode/trae/cursor 插件端的用户当您通过面板按钮进入 openmcp 的时候默认就会选择 STDIO 作为连接方式并根据你的上下文生成启动参数
openmcp desktop 的用户可能就需要自己填写了Anyway这总比在你的好友电脑中植入 chrome 浏览器密码破解木马简单
</div>
</el-tour-step>
<el-tour-step
:target="connectionLogRef"
:prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步' }"
:show-close="false"
placement="left"
>
<template #header>
<TourTitle>连接</TourTitle>
</template>
<div class="tour-common-text">
连接响应会在这个地方打印出来如果出现绿色背景的信息代表连接成功
</div>
</el-tour-step>
<el-tour-step
target="#sidebar-debug"
:prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步', onClick: () => router.push('/debug') }"
:show-close="false"
>
<template #header>
<TourTitle>调试</TourTitle>
</template>
<div class="tour-common-text">
假设你已经成功连接了 mcp 服务器那么点击调试按钮你可以开始你的调试工作
</div>
</el-tour-step>
<el-tour-step
:target="welcomeRef"
:prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步' }"
:show-close="false"
placement="right"
>
<template #header>
<TourTitle>调试</TourTitle>
</template>
<div class="tour-common-text">
我们目前提供了四种主要调试选项资源提词工具分别和 MCP 协议中的 resourcespromptstools 对应
交互测试则允许你直接将写好的 mcp 服务器放入大模型中直接做全链路测试从而更加获取更加真实的反馈和数据进而改进的你的 mcp 服务器
<br><br>
基于我们在 agent rl 方向的最佳实践我们后续还会推出更多的调试和数据集聚合制作选项请期待吧
</div>
</el-tour-step>
<el-tour-step
target="#sidebar-setting"
:prev-button-props="{ children: '上一步', onClick: () => router.push('/debug') }"
:next-button-props="{ children: '下一步', onClick: () => router.push('/setting') }"
:show-close="false"
>
<template #header>
<TourTitle>设置</TourTitle>
</template>
<div class="tour-common-text">
如果要进行交互测试请不要忘记先配置你常用的大模型 API
</div>
</el-tour-step>
<el-tour-step
:target="llmSettingRef"
:prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步' }"
:show-close="false"
placement="right"
>
<template #header>
<TourTitle>设置</TourTitle>
</template>
<div class="tour-common-text">
OpenMCP 目前支持所有支持 openai 接口规范的大模型比如 deepseekopenaikimi 等等
本地部署的 ollama 也正在支持
</div>
</el-tour-step>
<el-tour-step
target="#add-new-server-button"
:prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步' }"
:show-close="false"
>
<template #header>
<TourTitle>设置</TourTitle>
</template>
<div class="tour-common-text">
如果需要添加自定义的大模型服务请点击这里比如火山云阿里云硅基流动等
</div>
</el-tour-step>
<el-tour-step
target="#test-llm-button"
:prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步' }"
:show-close="false"
>
<template #header>
<TourTitle>设置</TourTitle>
</template>
<div class="tour-common-text">
填写完成连接签名后点击这里来测试 大模型服务是否可以访问
</div>
</el-tour-step>
<el-tour-step
target="#save-llm-button"
:prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步' }"
:show-close="false"
>
<template #header>
<TourTitle>设置</TourTitle>
</template>
<div class="tour-common-text">
最后请不要忘记点击保存按钮保存你的设置
</div>
</el-tour-step>
<el-tour-step
:prev-button-props="{ children: '上一步', onClick: () => router.push('/setting') }"
:next-button-props="{ children: '下一步' }"
:show-close="false"
>
<template #header>
<TourTitle>🎉恭喜</TourTitle>
</template>
<div class="tour-common-text">
🎉恭喜我的朋友现在的你已经是半个 mcp 专家了请充好一杯咖啡慢慢享用快乐的开发时间吧
<br><br>
如果是插件用户左侧面板的最下面入门与帮助有一些我们准备好的资料希望能帮到阁下优雅地开发你的 mcp 服务器
让我们一起把越来越多的 api sdk 接入 大模型吧
</div>
</el-tour-step>
<el-tour-step
:prev-button-props="{ children: '上一步', onClick: () => router.push('/setting') }"
:next-button-props="{ children: '结束', onClick: () => finishTour() }"
:show-close="false"
>
<template #header>
<TourTitle>终章</TourTitle>
</template>
<div class="tour-common-text">
<pre><code style="color: unset !important; background-color: unset !important;"
>(base) <span style="color: greenyellow"></span> <span style="color: #6AC2CF">.openmcp</span> <span style="color: #6BC34B">cat</span> <span style="color: #D357DB">KEY</span>
直面恐惧创造未来
Face your fears, create the future
恐怖に直面し未来を創り出</code></pre>
</div>
</el-tour-step>
</el-tour>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import TourTitle from './tour-title.vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { welcomeRef } from '@/views/debug/welcome';
import { connectionLogRef, connectionSettingRef } from '@/views/connect/connection';
import { llmSettingRef } from '@/views/setting/api';
import { userHasReadGuide } from './tour';
import { setTour } from '@/hook/setting';
const openTour = ref(true);
const { t } = useI18n();
const router = useRouter();
function finishTour() {
openTour.value = false;
userHasReadGuide.value = true;
setTour();
}
</script>
<style>
.tour-common-text {
font-size: 1.0rem;
padding: 10px;
padding-bottom: 20px;
line-height: 1.5;
}
.tour-warning {
display: flex;
background-color: rgba(230, 162, 60, 0.5);
border-radius: .5em;
padding: 5px;
margin: 5px 0;
}
.tour-common-text code {
color: unset!important;
background-color: unset!important;
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<el-input
type="textarea"
v-model="model"
:rows="inputHeightLines"
:maxlength="2000"
:placeholder="placeholder"
:resize="resize"
:class="customClass"
class="k-cute-textarea"
@keydown.enter="handleKeydown"
@compositionstart="handleCompositionStart"
@compositionend="handleCompositionEnd"
/>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, defineEmits } from 'vue';
const props = defineProps({
modelValue: {
type: String,
required: true
},
placeholder: {
type: String,
default: '输入消息...'
},
resize: {
type: String,
default: 'none'
},
customClass: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:modelValue', 'pressEnter']);
const model = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
}
});
const inputHeightLines = computed(() => {
const currentLines = props.modelValue.split('\n').length;
return Math.min(12, Math.max(5, currentLines));
});
const isComposing = ref(false);
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey && !isComposing.value) {
event.preventDefault();
emit('pressEnter', event);
}
};
const handleCompositionStart = () => {
isComposing.value = true;
};
const handleCompositionEnd = () => {
isComposing.value = false;
};
</script>
<style>
.k-cute-textarea textarea {
border-radius: .9em;
}
</style>

View File

@ -0,0 +1,182 @@
<template>
<div class="k-input-object">
<textarea ref="textareaRef" v-model="inputValue" class="k-input-object__textarea"
:class="{ 'is-invalid': isInvalid }" @input="handleInput" @blur="handleBlur"
@keydown="handleKeydown"
:placeholder="props.placeholder"
></textarea>
</div>
<div v-if="errorMessage" class="k-input-object__error">
{{ errorMessage }}
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch, nextTick } from 'vue';
import { debounce } from 'lodash';
export default defineComponent({
name: 'KInputObject',
props: {
modelValue: {
type: Object,
default: () => ({})
},
placeholder: {
type: String,
default: '请输入 JSON 对象'
},
debounceTime: {
type: Number,
default: 500
}
},
emits: ['update:modelValue', 'parse-error'],
setup(props, { emit }) {
const textareaRef = ref<HTMLTextAreaElement | null>(null)
const inputValue = ref<string>(JSON.stringify(props.modelValue, null, 2))
const isInvalid = ref<boolean>(false)
const errorMessage = ref<string>('')
//
const debouncedParse = debounce((value: string) => {
if (value.trim() === '') {
errorMessage.value = '';
isInvalid.value = false;
emit('update:modelValue', undefined);
return;
}
try {
const parsed = JSON.parse(value);
isInvalid.value = false;
errorMessage.value = '';
emit('update:modelValue', parsed);
} catch (error) {
isInvalid.value = true;
errorMessage.value = 'JSON 解析错误: ' + (error as Error).message;
emit('parse-error', error);
}
}, props.debounceTime)
const handleInput = () => {
debouncedParse(inputValue.value)
}
const handleBlur = () => {
//
debouncedParse.flush()
}
// modelValue
watch(
() => props.modelValue,
(newVal) => {
const currentParsed = tryParse(inputValue.value)
if (!isDeepEqual(currentParsed, newVal)) {
inputValue.value = JSON.stringify(newVal, null, 2)
}
},
{ deep: true }
)
// JSON
const tryParse = (value: string): any => {
try {
return JSON.parse(value)
} catch {
return undefined
}
}
//
const isDeepEqual = (obj1: any, obj2: any): boolean => {
return JSON.stringify(obj1) === JSON.stringify(obj2)
}
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === '{') {
event.preventDefault();
const start = textareaRef.value!.selectionStart;
const end = textareaRef.value!.selectionEnd;
const value = inputValue.value;
const newValue = value.substring(0, start) + '{\n \n}' + value.substring(end);
inputValue.value = newValue;
nextTick(() => {
textareaRef.value!.setSelectionRange(start + 2, start + 2);
});
} else if (event.key === '"') {
event.preventDefault();
const start = textareaRef.value!.selectionStart;
const end = textareaRef.value!.selectionEnd;
const value = inputValue.value;
const newValue = value.substring(0, start) + '""' + value.substring(end);
inputValue.value = newValue;
nextTick(() => {
textareaRef.value!.setSelectionRange(start + 1, start + 1);
});
} else if (event.key === 'Tab') {
event.preventDefault();
const start = textareaRef.value!.selectionStart;
const end = textareaRef.value!.selectionEnd;
const value = inputValue.value;
const newValue = value.substring(0, start) + ' ' + value.substring(end);
inputValue.value = newValue;
nextTick(() => {
textareaRef.value!.setSelectionRange(start + 1, start + 1);
});
} else if (event.key === 'Enter' && inputValue.value.trim() === '') {
event.preventDefault();
inputValue.value = '{}';
}
};
return {
textareaRef,
inputValue,
isInvalid,
errorMessage,
handleInput,
handleBlur,
handleKeydown,
props
}
}
})
</script>
<style scoped>
.k-input-object {
width: 100%;
background-color: var(--background);
border-radius: .5em;
margin-bottom: 15px;
display: flex;
}
.k-input-object__textarea {
width: 100%;
padding: 8px;
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
font-family: monospace;
resize: vertical;
transition: border-color 0.2s;
background-color: var(--el-bg-color-overlay);
color: var(--el-text-color-primary);
}
.k-input-object__textarea:focus {
outline: none;
border-color: var(--main-color);
}
.k-input-object__textarea.is-invalid {
border-color: var(--el-color-error);
}
.k-input-object__error {
color: var(--el-color-error);
font-size: 12px;
margin-top: 4px;
}
</style>

View File

@ -0,0 +1,129 @@
import { ToolCallContent, ToolItem } from "@/hook/type";
import { Ref, ref } from "vue";
import type { OpenAI } from 'openai';
type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
export enum MessageState {
ServerError = 'server internal error',
ReceiveChunkError = 'receive chunk error',
Timeout = 'timeout',
MaxEpochs = 'max epochs',
Unknown = 'unknown error',
Abort = 'abort',
ToolCall = 'tool call failed',
None = 'none',
Success = 'success',
ParseJsonError = 'parse json error'
}
export interface IExtraInfo {
created: number,
state: MessageState,
serverName: string,
usage?: ChatCompletionChunk['usage'];
[key: string]: any;
}
export interface ToolMessage {
role: 'tool';
content: ToolCallContent[];
tool_call_id?: string
name?: string // 工具名称,当 role 为 tool
tool_calls?: ToolCall[],
extraInfo: IExtraInfo
}
export interface TextMessage {
role: 'user' | 'assistant' | 'system';
content: string;
tool_call_id?: string
name?: string // 工具名称,当 role 为 tool
tool_calls?: ToolCall[],
extraInfo: IExtraInfo
}
export type ChatMessage = ToolMessage | TextMessage;
// 新增状态和工具数据
interface EnableToolItem {
name: string;
description: string;
enabled: boolean;
}
export interface ChatSetting {
modelIndex: number
systemPrompt: string
enableTools: EnableToolItem[]
temperature: number
enableWebSearch: boolean
contextLength: number
}
export interface ChatStorage {
messages: ChatMessage[]
settings: ChatSetting
}
export interface ToolCall {
id?: string;
index?: number;
type: string;
function: {
name: string;
arguments: string;
}
}
interface PromptTextItem {
type: 'prompt'
text: string
}
interface ResourceTextItem {
type: 'resource'
text: string
}
interface TextItem {
type: 'text'
text: string
}
export type RichTextItem = PromptTextItem | ResourceTextItem | TextItem;
export const allTools = ref<ToolItem[]>([]);
export interface IRenderMessage {
role: 'user' | 'assistant/content' | 'assistant/tool_calls' | 'tool';
content: string;
toolResult?: ToolCallContent[];
tool_calls?: ToolCall[];
showJson?: Ref<boolean>;
extraInfo: IExtraInfo;
}
export function getToolSchema(enableTools: EnableToolItem[]) {
const toolsSchema = [];
for (let i = 0; i < enableTools.length; i++) {
if (enableTools[i].enabled) {
const tool = allTools.value[i];
toolsSchema.push({
type: 'function',
function: {
name: tool.name,
description: tool.description || "",
parameters: tool.inputSchema
}
});
}
}
return toolsSchema;
}
export interface EditorContext {
editor: Ref<HTMLDivElement>;
[key: string]: any;
}

View File

@ -0,0 +1,204 @@
<template>
<footer class="chat-footer">
<div class="input-area">
<div class="input-wrapper">
<KRichTextarea
:tabId="tabId"
v-model="userInput"
:placeholder="t('enter-message-dot')"
:customClass="'chat-input'"
@press-enter="handleSend()"
/>
<el-button type="primary" @click="isLoading ? handleAbort() : handleSend()" class="send-button">
<span v-if="!isLoading" class="iconfont icon-send"></span>
<span v-else class="iconfont icon-stop"></span>
</el-button>
</div>
</div>
</footer>
</template>
<script setup lang="ts">
import { provide, onMounted, onUnmounted, ref, defineEmits, defineProps, PropType, inject, Ref } from 'vue';
import { useI18n } from 'vue-i18n';
import KRichTextarea from './rich-textarea.vue';
import { tabs } from '../../panel';
import { ChatMessage, ChatStorage, MessageState, ToolCall, RichTextItem } from './chat';
import { TaskLoop } from '../core/task-loop';
import { llmManager, llms } from '@/views/setting/llm';
import { ElMessage } from 'element-plus';
const { t } = useI18n();
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const emits = defineEmits(['update:scrollToBottom']);
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ChatStorage;
// messages
if (!tabStorage.messages) {
tabStorage.messages = [] as ChatMessage[];
}
const userInput = ref<string>('');
let loop: TaskLoop | undefined = undefined;
const isLoading = inject('isLoading') as Ref<boolean>;
const autoScroll = inject('autoScroll') as Ref<boolean>;
const streamingContent = inject('streamingContent') as Ref<string>;
const streamingToolCalls = inject('streamingToolCalls') as Ref<ToolCall[]>;
const scrollToBottom = inject('scrollToBottom') as () => Promise<void>;
const updateScrollHeight = inject('updateScrollHeight') as () => void;
function handleSend(newMessage?: string) {
//
const userMessage = newMessage || userInput.value;
if (!userMessage || isLoading.value) {
return;
}
isLoading.value = true;
autoScroll.value = true;
loop = new TaskLoop(streamingContent, streamingToolCalls);
loop.registerOnError((error) => {
ElMessage({
message: error.msg,
type: 'error',
duration: 3000
});
if (error.state === MessageState.ReceiveChunkError) {
tabStorage.messages.push({
role: 'assistant',
content: error.msg,
extraInfo: {
created: Date.now(),
state: error.state,
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
}
});
}
isLoading.value = false;
});
loop.registerOnChunk(() => {
scrollToBottom();
});
loop.registerOnDone(() => {
isLoading.value = false;
scrollToBottom();
});
loop.registerOnEpoch(() => {
isLoading.value = true;
scrollToBottom();
});
loop.start(tabStorage, userMessage);
userInput.value = '';
}
function handleAbort() {
if (loop) {
loop.abort();
isLoading.value = false;
ElMessage.info('请求已中止');
}
}
provide('handleSend', handleSend);
onMounted(() => {
updateScrollHeight();
window.addEventListener('resize', updateScrollHeight);
scrollToBottom();
});
onUnmounted(() => {
window.removeEventListener('resize', updateScrollHeight);
});
</script>
<style>
.chat-footer {
padding: 16px;
border-top: 1px solid var(--el-border-color);
flex-shrink: 0;
position: absolute;
height: fit-content !important;
bottom: 0;
width: 100%;
}
.input-area {
max-width: 800px;
margin: 0 auto;
position: relative;
}
.input-wrapper {
position: relative;
}
.chat-input {
padding-right: 80px;
}
.chat-input textarea {
border-radius: .5em;
}
.send-button {
position: absolute !important;
right: 8px !important;
bottom: 8px !important;
height: auto;
padding: 8px 12px;
font-size: 20px;
border-radius: 1.2em !important;
}
:deep(.chat-settings) {
position: absolute;
left: 0;
bottom: 0px;
z-index: 1;
}
.typing-cursor {
animation: blink 1s infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<el-tooltip :content="t('context-length')" placement="top">
<div class="setting-button" @click="showContextLengthDialog = true">
<span class="iconfont icon-length"></span>
<span class="value-badge">{{ tabStorage.settings.contextLength }}</span>
</div>
</el-tooltip>
<!-- 上下文长度设置 - 改为滑块形式 -->
<el-dialog v-model="showContextLengthDialog" :title="t('context-length') + ' ' + tabStorage.settings.contextLength"
width="400px">
<div class="slider-container">
<el-slider v-model="tabStorage.settings.contextLength" :min="1" :max="99" :step="1" />
<div class="slider-tips">
<span> 1: {{ t('single-dialog') }}</span>
<span> >1: {{ t('multi-dialog') }}</span>
</div>
</div>
<template #footer>
<el-button @click="showContextLengthDialog = false">{{ t("cancel") }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { defineComponent, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { ChatStorage } from '../chat';
const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage;
const showContextLengthDialog = ref(false);
</script>
<style>
.icon-length {
font-size: 16px;
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<el-tooltip :content="t('choose-model')" placement="top">
<div class="setting-button" @click="showModelDialog = true">
<span class="iconfont icon-model">
{{ currentServerName }}/{{ currentModelName }}
</span>
</div>
</el-tooltip>
<!-- 模型选择对话框 -->
<el-dialog v-model="showModelDialog" :title="t('choose-model')" width="400px">
<el-radio-group v-model="selectedModelIndex" @change="onRadioGroupChange">
<el-radio v-for="(model, index) in availableModels" :key="index" :value="index">
{{ model }}
</el-radio>
</el-radio-group>
<template #footer>
<el-button @click="showModelDialog = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="confirmModelChange">{{ t("confirm") }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { saveSetting } from '@/hook/setting';
import { llmManager, llms } from '@/views/setting/llm';
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const showModelDialog = ref(false);
const currentModel = llms[llmManager.currentModelIndex].userModel;
const selectedModelIndex = ref(llms[llmManager.currentModelIndex].models.indexOf(currentModel));
const currentServerName = computed(() => {
const currentLlm = llms[llmManager.currentModelIndex];
if (currentLlm) {
return currentLlm.name;
}
return '';
});
const currentModelName = computed(() => {
const currentLlm = llms[llmManager.currentModelIndex];
if (currentLlm) {
return currentLlm.models[selectedModelIndex.value];
}
return '';
});
const availableModels = computed(() => {
return llms[llmManager.currentModelIndex].models;
});
const confirmModelChange = () => {
showModelDialog.value = false;
};
const onRadioGroupChange = () => {
const currentModel = llms[llmManager.currentModelIndex].models[selectedModelIndex.value];
llms[llmManager.currentModelIndex].userModel = currentModel;
saveSetting();
};
</script>
<style></style>

View File

@ -0,0 +1,114 @@
<template>
<el-tooltip :content="t('prompts')" placement="top">
<div class="setting-button" @click="showChoosePrompt = true; saveCursorPosition();">
<span class="iconfont icon-chat"></span>
</div>
</el-tooltip>
<!-- 上下文长度设置 - 改为滑块形式 -->
<el-dialog v-model="showChoosePrompt" :title="t('prompts')" width="400px">
<div class="prompt-template-container-scrollbar" v-if="!selectPrompt">
<PromptTemplates :tab-id="-1" @prompt-selected="prompt => selectPrompt = prompt" />
</div>
<div v-else>
<PromptReader :tab-id="-1" :current-prompt-name="selectPrompt!.name"
@prompt-get-response="msg => whenGetPromptResponse(msg)" />
</div>
<template #footer>
<el-button v-if="selectPrompt" @click="selectPrompt = undefined;">{{ t('return') }}</el-button>
<el-button @click="showChoosePrompt = false; selectPrompt = undefined;">{{ t("cancel") }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { createApp, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { ChatStorage, EditorContext } from '../chat';
import { PromptsGetResponse, PromptTemplate } from '@/hook/type';
import PromptTemplates from '@/components/main-panel/prompt/prompt-templates.vue';
import PromptReader from '@/components/main-panel/prompt/prompt-reader.vue';
import { ElMessage, ElTooltip } from 'element-plus';
import PromptChatItem from '../prompt-chat-item.vue';
const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage;
let selectPrompt = ref<PromptTemplate | undefined>(undefined);
const showChoosePrompt = ref(false);
const editorContext = inject('editorContext') as EditorContext;
let savedSelection: Range | null = null;
function saveCursorPosition() {
const editor = editorContext.editor.value;
if (editor) {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
// selection editor
if (editor.contains(range.startContainer) && editor.contains(range.endContainer)) {
savedSelection = range;
} else {
savedSelection = null;
}
}
}
}
async function whenGetPromptResponse(msg: PromptsGetResponse) {
try {
const content = msg.messages[0].content;
selectPrompt.value = undefined;
const editor = editorContext.editor.value;
if (!content || !editor) {
return;
}
const container = document.createElement('div');
const promptChatItem = createApp(PromptChatItem, {
messages: msg.messages
});
promptChatItem.use(ElTooltip);
promptChatItem.mount(container);
const firstElement = container.firstElementChild!;
if (savedSelection) {
savedSelection.deleteContents();
savedSelection.insertNode(firstElement);
} else {
editor.appendChild(firstElement);
}
//
const newRange = document.createRange();
newRange.setStartAfter(firstElement);
newRange.collapse(true);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(newRange);
editor.dispatchEvent(new Event('input'));
editor.focus();
showChoosePrompt.value = false;
} catch (error) {
ElMessage.error((error as Error).message);
}
}
</script>
<style>
.icon-length {
font-size: 16px;
}
</style>

View File

@ -0,0 +1,116 @@
<template>
<el-tooltip :content="t('resources')" placement="top">
<div class="setting-button" @click="showChooseResource = true; saveCursorPosition();">
<span class="iconfont icon-file"></span>
</div>
</el-tooltip>
<el-dialog v-model="showChooseResource" :title="t('resources')" width="400px">
<div class="resource-template-container-scrollbar" v-if="!selectResource">
<ResourceList :tab-id="-1" @resource-selected="resource => selectResource = resource" />
</div>
<div v-else>
<ResourceReader :tab-id="-1" :current-resource-name="selectResource!.name"
@resource-get-response="msg => whenGetResourceResponse(msg)" />
</div>
<template #footer>
<el-button v-if="selectResource" @click="selectResource = undefined;">{{ t('return') }}</el-button>
<el-button @click="showChooseResource = false; selectResource = undefined;">{{ t("cancel") }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { createApp, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { ChatStorage, EditorContext } from '../chat';
import { ResourcesReadResponse, ResourceTemplate } from '@/hook/type';
import ResourceList from '@/components/main-panel/resource/resource-list.vue';
import ResourceReader from '@/components/main-panel/resource/resouce-reader.vue';
import { ElMessage, ElTooltip, ElProgress, ElPopover } from 'element-plus';
import ResourceChatItem from '../resource-chat-item.vue';
const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage;
let selectResource = ref<ResourceTemplate | undefined>(undefined);
const showChooseResource = ref(false);
const editorContext = inject('editorContext') as EditorContext;
let savedSelection: Range | null = null;
function saveCursorPosition() {
const editor = editorContext.editor.value;
if (editor) {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
if (editor.contains(range.startContainer) && editor.contains(range.endContainer)) {
savedSelection = range;
} else {
savedSelection = null;
}
}
}
}
async function whenGetResourceResponse(msg: ResourcesReadResponse) {
if (!msg) {
return;
}
try {
console.log(msg);
selectResource.value = undefined;
const editor = editorContext.editor.value;
if (msg.contents.length === 0 || !editor) {
return;
}
const container = document.createElement('div');
const resourceChatItem = createApp(ResourceChatItem, {
contents: msg.contents
});
resourceChatItem
.use(ElTooltip)
.use(ElProgress)
.use(ElPopover)
resourceChatItem.mount(container);
const firstElement = container.firstElementChild!;
if (savedSelection) {
savedSelection.deleteContents();
savedSelection.insertNode(firstElement);
} else {
editor.appendChild(firstElement);
}
const newRange = document.createRange();
newRange.setStartAfter(firstElement);
newRange.collapse(true);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(newRange);
editor.dispatchEvent(new Event('input'));
editor.focus();
showChooseResource.value = false;
} catch (error) {
ElMessage.error((error as Error).message);
}
}
</script>
<style>
.icon-length {
font-size: 16px;
}
</style>

View File

@ -0,0 +1,222 @@
<template>
<div class="chat-settings">
<Model />
<SystemPrompt />
<ToolUse />
<Prompt />
<Resource />
<Websearch />
<Temperature />
<ContextLength />
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits, provide, PropType, computed } from 'vue';
import { llmManager } from '@/views/setting/llm';
import { tabs } from '@/components/main-panel/panel';
import type { ChatSetting, ChatStorage } from '../chat';
import Model from './model.vue';
import SystemPrompt from './system-prompt.vue';
import ToolUse from './tool-use.vue';
import Prompt from './prompt.vue';
import Resource from './resource.vue';
import Websearch from './websearch.vue';
import Temperature from './temperature.vue';
import ContextLength from './context-length.vue';
const props = defineProps({
modelValue: {
type: String,
required: true
},
tabId: {
type: Number,
required: true
}
});
const emits = defineEmits(['update:modelValue']);
const modelValue = computed({
get() {
return props.modelValue;
},
set(value) {
emits('update:modelValue', value);
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ChatStorage & { settings: ChatSetting };
if (!tabStorage.settings) {
tabStorage.settings = {
modelIndex: llmManager.currentModelIndex,
enableTools: [],
enableWebSearch: false,
temperature: 0.7,
contextLength: 20,
systemPrompt: ''
} as ChatSetting;
}
provide('tabStorage', tabStorage);
</script>
<style>
.chat-settings {
display: flex;
gap: 2px;
padding: 8px 0;
width: fit-content;
border-radius: 99%;
left: 5px;
bottom: 0px;
z-index: 10;
position: absolute;
}
.setting-button {
padding: 5px 8px;
margin-right: 3px;
border-radius: .5em;
font-size: 12px;
position: relative;
user-select: none;
-webkit-user-drag: none;
display: flex;
align-items: center;
cursor: pointer;
transition: var(--animation-3s);
}
.setting-button.active {
background-color: var(--el-color-primary);
color: var(--el-text-color-primary);
transition: var(--animation-3s);
}
.setting-button.active:hover {
background-color: var(--el-color-primary);
transition: var(--animation-3s);
}
.setting-button:hover {
background-color: var(--background);
transition: var(--animation-3s);
}
.value-badge {
font-size: 10px;
padding: 1px 4px;
border-radius: 4px;
}
.slider-container {
padding: 0 10px;
}
.slider-tips {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
/* 新增工具相关样式 */
.tools-container {
padding: 10px;
}
.tool-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--el-border-color-light);
}
.tool-info {
flex: 1;
margin-right: 20px;
}
.tool-name {
font-weight: 500;
margin-bottom: 4px;
}
.tool-description {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.tools-dialog-container .el-switch__core {
border: 1px solid var(--main-color) !important;
}
.tools-dialog-container .el-switch .el-switch__action {
background-color: var(--main-color);
}
.tools-dialog-container .el-switch.is-checked .el-switch__action {
background-color: var(--sidebar);
}
/* 新增工具对话框样式 */
.tools-dialog-container {
display: flex;
gap: 16px;
}
.tools-list {
flex: 1;
border-right: 1px solid var(--el-border-color);
padding-right: 16px;
}
.schema-viewer {
flex: 1;
}
.schema-viewer pre {
margin: 0;
border-radius: 4px;
white-space: pre-wrap;
word-wrap: break-word;
background-color: var(--el-bg-color-overlay);
}
.schema-viewer .openmcp-code-block {
border: none;
}
.schema-viewer code {
font-family: var(--code-font-family);
font-size: 12px;
color: var(--el-text-color-primary);
}
.badge-outer {
position: relative;
}
.badge-inner {
position: absolute;
color: var(--foreground);
background-color: var(--main-color);
border-radius: 50%;
padding: 2px 6px;
font-size: 10px;
z-index: 10;
top: -16px;
right: -18px;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
}
</style>

View File

@ -0,0 +1,56 @@
import { useMessageBridge } from "@/api/message-bridge";
import { pinkLog } from "@/views/setting/util";
import { ref } from "vue";
interface SystemPrompt {
name: string;
content: string;
}
export const systemPrompts = ref<SystemPrompt[]>([{
name: 'Default',
content: '你是一个AI助手, 你可以回答任何问题。'
}]);
export async function saveSystemPrompts() {
const bridge = useMessageBridge();
const payload = JSON.parse(JSON.stringify(systemPrompts.value));
const res = await bridge.commandRequest('system-prompts/save', { prompts: payload });
if (res.code === 200) {
pinkLog('system prompt 保存成功');
}
}
export async function setSystemPrompt(name: string, content: string) {
const bridge = useMessageBridge();
const res = await bridge.commandRequest('system-prompts/set', { name, content });
if (res.code === 200) {
pinkLog('system prompt 添加成功');
if (!systemPrompts.value.some(prompt => prompt.name === name)) {
systemPrompts.value.push({ name, content });
}
}
return res;
}
export async function deleteSystemPrompt(name: string) {
const bridge = useMessageBridge();
const res = await bridge.commandRequest('system-prompts/delete', { name });
if (res.code === 200) {
pinkLog('system prompt 删除成功');
systemPrompts.value = systemPrompts.value.filter((prompt) => prompt.name !== name);
}
return res;
}
export async function loadSystemPrompts() {
const bridge = useMessageBridge();
const res = await bridge.commandRequest('system-prompts/load');
if (res.code === 200) {
pinkLog('system prompt 加载成功');
systemPrompts.value = res.msg;
}
return res;
}

View File

@ -0,0 +1,171 @@
<template>
<el-tooltip :content="t('system-prompt')" placement="top">
<div class="setting-button" :class="{ 'active': hasSystemPrompt }" size="small"
@click="showSystemPromptDialog = true">
<span class="iconfont icon-prompt"></span>
</div>
</el-tooltip>
<el-dialog v-model="showSystemPromptDialog" :title="t('system-prompt')" width="600px">
<div v-if="!showAdd">
<el-select v-model="tabStorage.settings.systemPrompt"
:placeholder="t('choose-presetting')"
style="width: 100%; margin-bottom: 20px;">
<el-option v-for="prompt in systemPrompts"
:value="prompt.name" :key="prompt.name"
class="choose-system-prompt"
>
<span>{{ prompt.name }}</span>
<el-dropdown trigger="hover" @command="handleCommand">
<span>
<span class="iconfont icon-more"></span>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :command="{ type: 'delete', name: prompt.name }" divided>
<span class="iconfont icon-delete">&emsp;{{ t('delete') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-option>
<el-option label="新建预设" value="$new" @click="showAdd = true; newPromptName = ''; newPromptContent = ''">
<span class="iconfont icon-add"></span>
</el-option>
</el-select>
<el-input v-model="currentPromptValue" type="textarea" :rows="8"
:placeholder="t('system-prompt.placeholder')" clearable />
</div>
<div v-else class="tool-use">
<el-input v-model="newPromptName" :placeholder="t('add-system-prompt.name-placeholder')" />
<el-input v-model="newPromptContent" type="textarea" :rows="8" :placeholder="t('system-prompt.placeholder')"
clearable />
</div>
<template #footer v-if="!showAdd">
<el-button @click="showSystemPromptDialog = false">{{ t("cancel") }}</el-button>
</template>
<template #footer v-else>
<el-button @click="showAdd = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="addNewPrompt">{{ t("save") }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, inject, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { ChatStorage } from '../chat';
import { systemPrompts, setSystemPrompt, loadSystemPrompts, deleteSystemPrompt } from './system-prompt';
import { ElMessage } from 'element-plus';
import { debounce } from 'lodash';
const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage;
const showSystemPromptDialog = ref(false);
const showAdd = ref(false);
const hasSystemPrompt = computed(() => {
return !!tabStorage.settings.systemPrompt?.trim();
});
const newPromptName = ref('');
const newPromptContent = ref('');
const currentPromptValue = computed({
get() {
return systemPrompts.value.find(prompt => prompt.name === tabStorage.settings.systemPrompt)?.content || '';
},
set(value) {
const prompt = systemPrompts.value.find(prompt => prompt.name === tabStorage.settings.systemPrompt);
if (prompt) {
prompt.content = value;
safeSaveSystemPrompts();
}
}
});
async function addNewPrompt() {
const name = newPromptName.value.trim();
const content = newPromptContent.value.trim();
//
if (systemPrompts.value.some(prompt => prompt.name === name)) {
ElMessage.warning('预设名称已存在,请选择其他名称。');
return;
}
const res = await setSystemPrompt(name, content);
if (res.code === 200) {
ElMessage.success('预设添加成功。');
showAdd.value = false;
tabStorage.settings.systemPrompt = name;
} else {
ElMessage.error('添加预设失败。' + res.msg);
}
}
const safeSaveSystemPrompts = debounce(async () => {
if (!showAdd.value) {
const prompt = systemPrompts.value.find(prompt => prompt.name === tabStorage.settings.systemPrompt);
if (prompt) {
await setSystemPrompt(prompt.name, prompt.content);
}
}
}, 500);
async function handleCommand(command: {type: string, name: string}) {
if (command.type === 'delete') {
const res = await deleteSystemPrompt(command.name);
if (res.code === 200) {
if (tabStorage.settings.systemPrompt === command.name) {
tabStorage.settings.systemPrompt = systemPrompts.value[0]?.name || '';
}
}
}
}
onMounted(async () => {
await loadSystemPrompts();
});
</script>
<style>
.setting-button {
cursor: pointer;
padding: 8px;
border-radius: 50%;
transition: background-color 0.3s;
}
.setting-button:hover {
background-color: rgba(0, 0, 0, 0.1);
}
.setting-button.active {
color: var(--el-color-primary);
}
.tool-use .el-input__wrapper {
margin-bottom: 20px;
border-radius: .5em;
}
.choose-system-prompt {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<el-tooltip :content="t('temperature-parameter')" placement="top">
<div class="setting-button" @click="showTemperatureSlider = true">
<span class="iconfont icon-temperature"></span>
<span class="value-badge">{{ tabStorage.settings.temperature.toFixed(1) }}</span>
</div>
</el-tooltip>
<!-- 温度参数滑块 -->
<el-dialog v-model="showTemperatureSlider" :title="t('temperature-parameter')" width="400px">
<div class="slider-container">
<el-slider v-model="tabStorage.settings.temperature" :min="0" :max="2" :step="0.1" />
<div class="slider-tips">
<span> {{ t('precise') }}(0)</span>
<span>{{ t('moderate') }}(1)</span>
<span>{{ t('creative') }}(2)</span>
</div>
</div>
<template #footer>
<el-button @click="showTemperatureSlider = false">{{ t("cancel") }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { inject, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { ChatStorage } from '../chat';
const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage;
const showTemperatureSlider = ref(false);
</script>
<style>
.icon-temperature {
font-size: 18px;
}
</style>

View File

@ -0,0 +1,102 @@
<template>
<el-tooltip :content="t('tool-use')" placement="top">
<div class="setting-button" :class="{ 'active': availableToolsNum > 0 }" size="small" @click="toggleTools">
<span class="iconfont icon-tool badge-outer">
<span class="badge-inner">
{{ availableToolsNum }}
</span>
</span>
</div>
</el-tooltip>
<el-dialog v-model="showToolsDialog" title="工具管理" width="800px">
<div class="tools-dialog-container">
<el-scrollbar height="400px" class="tools-list">
<div v-for="(tool, index) in tabStorage.settings.enableTools" :key="index" class="tool-item">
<div class="tool-info">
<div class="tool-name">{{ tool.name }}</div>
<div v-if="tool.description" class="tool-description">{{ tool.description }}</div>
</div>
<el-switch v-model="tool.enabled" />
</div>
</el-scrollbar>
<el-scrollbar height="400px" class="schema-viewer">
<div v-html="activeToolsSchemaHTML"></div>
</el-scrollbar>
</div>
<template #footer>
<el-button type="primary" @click="enableAllTools">激活所有工具</el-button>
<el-button type="danger" @click="disableAllTools">禁用所有工具</el-button>
<el-button type="primary" @click="showToolsDialog = false">{{ t("cancel") }}</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, computed, inject, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { allTools, ChatStorage, getToolSchema } from '../chat';
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
import { useMessageBridge } from '@/api/message-bridge';
const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage;
const showToolsDialog = ref(false);
const availableToolsNum = computed(() => {
return tabStorage.settings.enableTools.filter(tool => tool.enabled).length;
});
// toggleTools
const toggleTools = () => {
showToolsDialog.value = true;
};
const activeToolsSchemaHTML = computed(() => {
const toolsSchema = getToolSchema(tabStorage.settings.enableTools);
const jsonString = JSON.stringify(toolsSchema, null, 2);
return markdownToHtml(
"```json\n" + jsonString + "\n```"
);
});
// -
const enableAllTools = () => {
tabStorage.settings.enableTools.forEach(tool => {
tool.enabled = true;
});
};
// -
const disableAllTools = () => {
tabStorage.settings.enableTools.forEach(tool => {
tool.enabled = false;
});
};
onMounted(async () => {
const bridge = useMessageBridge();
const res = await bridge.commandRequest('tools/list');
if (res.code === 200) {
allTools.value = res.msg.tools || [];
tabStorage.settings.enableTools = [];
for (const tool of allTools.value) {
tabStorage.settings.enableTools.push({
name: tool.name,
description: tool.description,
enabled: true
});
}
}
});
</script>
<style></style>

View File

@ -0,0 +1,26 @@
<template>
<el-tooltip :content="t('websearch')" placement="top">
<div class="setting-button" :class="{ 'active': tabStorage.settings.enableWebSearch }" size="small"
@click="toggleWebSearch">
<span class="iconfont icon-web"></span>
</div>
</el-tooltip>
</template>
<script setup lang="ts">
import { inject } from 'vue';
import { useI18n } from 'vue-i18n';
import { ChatStorage } from '../chat';
const { t } = useI18n();
const tabStorage = inject('tabStorage') as ChatStorage;
const toggleWebSearch = () => {
tabStorage.settings.enableWebSearch = !tabStorage.settings.enableWebSearch;
};
</script>
<style>
</style>

View File

@ -0,0 +1,42 @@
<template>
<el-tooltip :content="props.messages[0].content.text" placement="top">
<span class="chat-prompt-item" contenteditable="false">
<span class="iconfont icon-chat"></span>
<span class="real-text">{{ props.messages[0].content.text }}</span>
</span>
</el-tooltip>
</template>
<script setup lang="ts">
import { PromptsGetResponse } from '@/hook/type';
import { defineProps, PropType } from 'vue';
const props = defineProps({
messages: {
type: Array as PropType<PromptsGetResponse['messages']>,
required: true
}
});
</script>
<style>
.chat-prompt-item {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-flex;
border-radius: .3em;
align-items: center;
padding: 0 4px;
background-color: #373839;
border: 1px solid var(--foreground);
font-size: 12px;
margin-left: 3px;
margin-right: 3px;
}
.chat-prompt-item .iconfont {
margin-right: 4px;
}
</style>

View File

@ -0,0 +1,168 @@
<template>
<el-tooltip placement="top">
<template #content>
<div class="resource-chat-item-tooltip">
<div v-for="(item, index) of toolRenderItems" :key="index">
<div v-if="item.mimeType === 'text/plain'">
{{ item.text }}
</div>
<div v-else-if="item.mimeType === 'image/jpeg' || item.mimeType === 'image/png'">
<img :src="item.imageUrl" alt="screenshot" />
<span>{{ item.text }}</span>
</div>
<div v-else>
</div>
</div>
</div>
</template>
<span class="chat-prompt-item" contenteditable="false">
<span class="iconfont icon-file"></span>
<span class="real-text">{{ resourceText }}</span>
<el-progress v-if="!finishProcess" class="progress" style="width: 100px;" :percentage="progress"
color="var(--main-color)"></el-progress>
</span>
</el-tooltip>
</template>
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
import { ResourcesReadResponse } from '@/hook/type';
import { getImageBlobUrlByBase64 } from '@/hook/util';
import { computed, defineProps, PropType, reactive, ref, watch } from 'vue';
const props = defineProps({
contents: {
type: Array as PropType<ResourcesReadResponse['contents']>,
required: true
}
});
// mcp
// 1.
// 2. { image_url: "https://tos.com/xxx.jpeg" }
const toolRenderItems = ref<{
mimeType: string;
text: string;
[key: string]: any;
}[]>([]);
const resourceText = computed(() => {
const texts = [];
for (const item of toolRenderItems.value) {
if (item.mimeType === 'text/plain') {
texts.push(item.text);
} else if (item.mimeType === 'image/jpeg' || item.mimeType === 'image/png') {
texts.push(item.text || '');
}
}
return texts.join('');
});
const bridge = useMessageBridge();
const progress = ref(0);
const progressText = ref('OCR');
const finishProcess = ref(true);
props.contents.forEach((content) => {
console.log(content);
if (content.mimeType === 'text/plain') {
toolRenderItems.value.push({
mimeType: content.mimeType,
text: content.text
});
}
if (content.mimeType === 'image/jpeg' || content.mimeType === 'image/png') {
finishProcess.value = false;
const blobUrl = getImageBlobUrlByBase64(content.blob!, content.mimeType);
toolRenderItems.value.push({
mimeType: content.mimeType,
imageUrl: blobUrl,
text: ''
});
const renderItem = toolRenderItems.value.at(-1)!;
const makeRequest = async () => {
const res = await bridge.commandRequest('ocr/start-ocr', {
base64String: content.blob,
mimeType: content.mimeType
});
if (res.code === 200) {
const filename = res.msg.filename;
const workerId = res.msg.workerId;
const cancel = bridge.addCommandListener('ocr/worker/log', data => {
finishProcess.value = false;
const { id, progress: p = 1.0, status = 'finish' } = data;
if (id === workerId) {
progressText.value = status;
progress.value = Math.min(Math.ceil(Math.max(p * 100, 0)), 100);
}
}, { once: false });
bridge.addCommandListener('ocr/worker/done', data => {
if (data.id !== workerId) {
return;
}
progress.value = 1;
finishProcess.value = true;
toolRenderItems.value[0].text = data.text;
cancel();
}, { once: true });
}
}
makeRequest();
}
});
</script>
<style>
.resource-chat-item-tooltip {
min-height: 100px;
max-width: 420px;
display: flex;
flex-direction: column;
}
.resource-chat-item-tooltip img {
max-width: 420px;
}
.chat-resource-item {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-flex;
border-radius: .3em;
align-items: center;
padding: 0 4px;
background-color: #373839;
border: 1px solid var(--foreground);
font-size: 12px;
margin-left: 3px;
margin-right: 3px;
}
.chat-resource-item .iconfont {
margin-right: 4px;
}
</style>

View File

@ -0,0 +1,243 @@
<template>
<!-- 下侧的设置按钮 -->
<Setting :tabId="tabId" v-model="modelValue" />
<!-- 编辑区 -->
<div class="k-rich-textarea">
<div
:ref="el => editor = el"
contenteditable="true"
class="rich-editor"
:placeholder="placeholder"
@input="handleInput"
@keydown.backspace="handleBackspace"
@keydown.enter="handleKeydown"
@compositionstart="handleCompositionStart"
@compositionend="handleCompositionEnd"
></div>
</div>
</template>
<script setup lang="ts">
import { ref, PropType, defineProps, defineEmits, computed, provide } from 'vue';
import type { RichTextItem } from './chat';
import Setting from './options/setting.vue';
const props = defineProps({
tabId: {
type: Number,
required: true
},
modelValue: {
type: String,
required: true
},
placeholder: {
type: String,
default: '输入消息...'
},
customClass: {
type: String,
default: ''
}
});
const modelValue = computed({
get() {
return props.modelValue;
},
set(value: string) {
emit('update:modelValue', value);
}
})
const emit = defineEmits(['update:modelValue', 'pressEnter']);
const editor = ref<any>(null);
provide('editorContext', {
editor,
});
function handleBackspace(event: KeyboardEvent) {
// Backspace
const editorElement = editor.value;
if (!(editorElement instanceof HTMLDivElement)) {
return;
}
const selection = window.getSelection();
if (!selection || !selection.rangeCount) {
return;
}
const range = selection.getRangeAt(0);
const startContainer = range.startContainer;
// rich-item
if (startContainer.parentElement?.classList.contains('rich-item')) {
event.preventDefault();
startContainer.parentElement.remove();
}
}
function handleInput(event: Event) {
const editorElement = editor.value;
if (!(editorElement instanceof HTMLDivElement)) {
return;
}
const fragments: string[] = [];
editorElement.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
fragments.push(node.textContent || '');
} else {
const element = node as HTMLElement;
const collection = element.getElementsByClassName('real-text');
const fragmentText = extractTextFromCollection(collection);
fragments.push(fragmentText || '');
}
});
console.log(fragments);
emit('update:modelValue', fragments.join(' '));
}
function extractTextFromCollection(collection: HTMLCollection) {
const texts = [];
for (let i = 0; i < collection.length; i++) {
texts.push(collection[i].textContent); // .innerText
}
return texts.join('');
}
const isComposing = ref(false);
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey && !isComposing.value) {
event.preventDefault();
const editorElement = editor.value;
if (!(editorElement instanceof HTMLDivElement)) {
return;
}
//
editorElement.innerHTML = '';
emit('pressEnter', event);
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
const editorElement = editor.value;
if (!(editorElement instanceof HTMLDivElement)) {
return;
}
const selection = window.getSelection();
if (!selection || !selection.rangeCount) {
return;
}
const range = selection.getRangeAt(0);
const startContainer = range.startContainer;
if (event.key === 'ArrowLeft') {
//
const previousSibling = startContainer.previousSibling;
if (previousSibling && previousSibling.nodeType !== Node.TEXT_NODE) {
event.preventDefault();
range.setStartBefore(previousSibling);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
} else if (event.key === 'ArrowRight') {
//
const nextSibling = startContainer.nextSibling;
if (nextSibling && nextSibling.nodeType !== Node.TEXT_NODE) {
event.preventDefault();
range.setStartAfter(nextSibling);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
}
}
}
function handleCompositionStart() {
isComposing.value = true;
}
function handleCompositionEnd() {
isComposing.value = false;
}
</script>
<style>
.k-rich-textarea {
border: 1px solid var(--main-color);
background-color: var(--el-input-bg-color, var(--el-fill-color-blank));
background-image: none;
border-radius: .5em;
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
box-sizing: border-box;
color: var(--el-input-text-color, var(--el-text-color-regular));
padding: 10px 10px;
display: inline-block;
font-size: var(--el-font-size-base);
position: relative;
vertical-align: bottom;
width: 100%;
min-height: 50px;
transition: var(--el-transition-box-shadow);
}
.rich-editor {
min-height: 100px;
outline: none;
}
.rich-editor:empty::before {
content: attr(placeholder);
color: #C0C4CC;
}
.rich-item {
padding: 2px 4px;
border-radius: 4px;
margin: 0 2px;
}
.rich-item-prompt {
background-color: #e8f0fe;
color: #1a73e8;
}
.rich-item-resource {
background-color: #f1f3f4;
color: #202124;
}
.chat-resource-item {
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background-color: #373839;
font-size: 12px;
}
.chat-resource-item .iconfont {
margin-right: 4px;
}
</style>

View File

@ -1,62 +0,0 @@
import { ToolItem } from "@/hook/type";
import { ref } from "vue";
export interface ChatMessage {
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
tool_call_id?: string
name?: string // 工具名称,当 role 为 tool
tool_calls?: ToolCall[]
}
// 新增状态和工具数据
interface EnableToolItem {
name: string;
description: string;
enabled: boolean;
}
export interface ChatSetting {
modelIndex: number
systemPrompt: string
enableTools: EnableToolItem[]
temperature: number
enableWebSearch: boolean
contextLength: number
}
export interface ChatStorage {
messages: ChatMessage[]
settings: ChatSetting
}
export interface ToolCall {
id?: string;
index?: number;
type: string;
function: {
name: string;
arguments: string;
}
}
export const allTools = ref<ToolItem[]>([]);
export function getToolSchema(enableTools: EnableToolItem[]) {
const toolsSchema = [];
for (let i = 0; i < enableTools.length; i++) {
if (enableTools[i].enabled) {
const tool = allTools.value[i];
toolsSchema.push({
type: 'function',
function: {
name: tool.name,
description: tool.description || "",
parameters: tool.inputSchema
}
});
}
}
return toolsSchema;
}

View File

@ -1,50 +1,118 @@
/* eslint-disable */ /* eslint-disable */
import { Ref } from "vue"; import { Ref } from "vue";
import { ToolCall, ChatStorage, getToolSchema } from "./chat"; import { ToolCall, ChatStorage, getToolSchema, MessageState } from "../chat-box/chat";
import { useMessageBridge } from "@/api/message-bridge"; import { useMessageBridge } from "@/api/message-bridge";
import type { OpenAI } from 'openai'; import type { OpenAI } from 'openai';
import { callTool } from "../tool/tools"; import { callTool } from "../../tool/tools";
import { llmManager, llms } from "@/views/setting/llm"; import { llmManager, llms } from "@/views/setting/llm";
import { pinkLog, redLog } from "@/views/setting/util";
import { ElMessage } from "element-plus";
type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk; export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string }; export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
interface TaskLoopOptions { interface TaskLoopOptions {
maxEpochs: number; maxEpochs: number;
} }
interface IErrorMssage {
state: MessageState,
msg: string
}
/** /**
* @description * @description
*/ */
export class TaskLoop { export class TaskLoop {
private bridge = useMessageBridge(); private bridge = useMessageBridge();
private currentChatId = ''; private currentChatId = '';
private completionUsage: ChatCompletionChunk['usage'] | undefined;
constructor( constructor(
private readonly streamingContent: Ref<string>, private readonly streamingContent: Ref<string>,
private readonly streamingToolCalls: Ref<ToolCall[]>, private readonly streamingToolCalls: Ref<ToolCall[]>,
private onError: (msg: string) => void = (msg) => {}, private onError: (error: IErrorMssage) => void = (msg) => {},
private onChunk: (chunk: ChatCompletionChunk) => void = (chunk) => {}, private onChunk: (chunk: ChatCompletionChunk) => void = (chunk) => {},
private onDone: () => void = () => {}, private onDone: () => void = () => {},
private onEpoch: () => void = () => {}, private onEpoch: () => void = () => {},
private readonly taskOptions: TaskLoopOptions = { maxEpochs: 20 }, private readonly taskOptions: TaskLoopOptions = { maxEpochs: 20 },
) {} ) {
}
private async handleToolCalls(toolCalls: ToolCall[]) { private async handleToolCalls(toolCalls: ToolCall[]) {
// TODO: 调用多个工具并返回调用结果? // TODO: 调用多个工具并返回调用结果?
const toolCall = toolCalls[0]; const toolCall = toolCalls[0];
let toolName: string;
let toolArgs: Record<string, any>;
try {
toolName = toolCall.function.name;
toolArgs = JSON.parse(toolCall.function.arguments);
} catch (error) {
return {
content: [{
type: 'error',
text: this.parseErrorObject(error)
}],
state: MessageState.ParseJsonError
};
}
try { try {
const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments);
const toolResponse = await callTool(toolName, toolArgs); const toolResponse = await callTool(toolName, toolArgs);
if (!toolResponse.isError) {
const content = JSON.stringify(toolResponse.content); console.log(toolResponse);
return content;
if (typeof toolResponse === 'string') {
console.log(toolResponse);
return {
content: [{
type: 'error',
text: toolResponse
}],
state: MessageState.ToolCall
}
} else if (!toolResponse.isError) {
return {
content: toolResponse.content,
state: MessageState.Success
};
} else { } else {
this.onError(`工具调用失败: ${toolResponse.content}`);
return {
content: toolResponse.content,
state: MessageState.ToolCall
}
} }
} catch (error) { } catch (error) {
this.onError(`工具调用失败: ${(error as Error).message}`); this.onError({
state: MessageState.ToolCall,
msg: `工具调用失败: ${(error as Error).message}`
});
console.error(error);
return {
content: [{
type: 'error',
text: this.parseErrorObject(error)
}],
state: MessageState.ToolCall
}
}
}
private parseErrorObject(error: any): string {
if (typeof error === 'string') {
return error;
} else if (typeof error === 'object') {
return JSON.stringify(error, null, 2);
} else {
return error.toString();
} }
} }
@ -57,6 +125,7 @@ export class TaskLoop {
private handleChunkDeltaToolCalls(chunk: ChatCompletionChunk) { private handleChunkDeltaToolCalls(chunk: ChatCompletionChunk) {
const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0]; const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0];
if (toolCall) { if (toolCall) {
const currentCall = this.streamingToolCalls.value[toolCall.index]; const currentCall = this.streamingToolCalls.value[toolCall.index];
@ -89,13 +158,23 @@ export class TaskLoop {
} }
} }
private handleChunkUsage(chunk: ChatCompletionChunk) {
const usage = chunk.usage;
if (usage) {
this.completionUsage = usage;
}
}
private doConversation(chatData: ChatCompletionCreateParamsBase) { private doConversation(chatData: ChatCompletionCreateParamsBase) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => { const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => {
if (data.code !== 200) { if (data.code !== 200) {
this.onError(data.msg || '请求模型服务时发生错误'); this.onError({
reject(new Error(data.msg || '请求模型服务时发生错误')); state: MessageState.ReceiveChunkError,
msg: data.msg || '请求模型服务时发生错误'
});
resolve();
return; return;
} }
const { chunk } = data.msg as { chunk: ChatCompletionChunk }; const { chunk } = data.msg as { chunk: ChatCompletionChunk };
@ -103,6 +182,7 @@ export class TaskLoop {
// 处理增量的 content 和 tool_calls // 处理增量的 content 和 tool_calls
this.handleChunkDeltaContent(chunk); this.handleChunkDeltaContent(chunk);
this.handleChunkDeltaToolCalls(chunk); this.handleChunkDeltaToolCalls(chunk);
this.handleChunkUsage(chunk);
this.onChunk(chunk); this.onChunk(chunk);
}, { once: false }); }, { once: false });
@ -121,10 +201,19 @@ export class TaskLoop {
}); });
} }
public makeChatData(tabStorage: ChatStorage): ChatCompletionCreateParamsBase | undefined {
public makeChatData(tabStorage: ChatStorage): ChatCompletionCreateParamsBase {
const baseURL = llms[llmManager.currentModelIndex].baseUrl; const baseURL = llms[llmManager.currentModelIndex].baseUrl;
const apiKey = llms[llmManager.currentModelIndex].userToken; const apiKey = llms[llmManager.currentModelIndex].userToken || '';
if (apiKey.trim() === '') {
if (tabStorage.messages.length > 0 && tabStorage.messages[tabStorage.messages.length - 1].role === 'user') {
tabStorage.messages.pop();
ElMessage.error('请先设置 API Key');
}
return undefined;
}
const model = llms[llmManager.currentModelIndex].userModel; const model = llms[llmManager.currentModelIndex].userModel;
const temperature = tabStorage.settings.temperature; const temperature = tabStorage.settings.temperature;
const tools = getToolSchema(tabStorage.settings.enableTools); const tools = getToolSchema(tabStorage.settings.enableTools);
@ -140,6 +229,7 @@ export class TaskLoop {
// 如果超出了 tabStorage.settings.contextLength, 则删除最早的消息 // 如果超出了 tabStorage.settings.contextLength, 则删除最早的消息
const loadMessages = tabStorage.messages.slice(- tabStorage.settings.contextLength); const loadMessages = tabStorage.messages.slice(- tabStorage.settings.contextLength);
userMessages.push(...loadMessages); userMessages.push(...loadMessages);
// 增加一个id用于锁定状态 // 增加一个id用于锁定状态
const id = crypto.randomUUID(); const id = crypto.randomUUID();
@ -167,7 +257,7 @@ export class TaskLoop {
this.streamingToolCalls.value = []; this.streamingToolCalls.value = [];
} }
public registerOnError(handler: (msg: string) => void) { public registerOnError(handler: (msg: IErrorMssage) => void) {
this.onError = handler; this.onError = handler;
} }
@ -188,7 +278,15 @@ export class TaskLoop {
*/ */
public async start(tabStorage: ChatStorage, userMessage: string) { public async start(tabStorage: ChatStorage, userMessage: string) {
// 添加目前的消息 // 添加目前的消息
tabStorage.messages.push({ role: 'user', content: userMessage }); tabStorage.messages.push({
role: 'user',
content: userMessage,
extraInfo: {
created: Date.now(),
state: MessageState.Success,
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
}
});
for (let i = 0; i < this.taskOptions.maxEpochs; ++ i) { for (let i = 0; i < this.taskOptions.maxEpochs; ++ i) {
@ -197,10 +295,16 @@ export class TaskLoop {
// 初始累计清空 // 初始累计清空
this.streamingContent.value = ''; this.streamingContent.value = '';
this.streamingToolCalls.value = []; this.streamingToolCalls.value = [];
this.completionUsage = undefined;
// 构造 chatData // 构造 chatData
const chatData = this.makeChatData(tabStorage); const chatData = this.makeChatData(tabStorage);
if (!chatData) {
this.onDone();
break;
}
this.currentChatId = chatData.id!; this.currentChatId = chatData.id!;
// 发送请求 // 发送请求
@ -212,24 +316,70 @@ export class TaskLoop {
tabStorage.messages.push({ tabStorage.messages.push({
role: 'assistant', role: 'assistant',
content: this.streamingContent.value || '', content: this.streamingContent.value || '',
tool_calls: this.streamingToolCalls.value tool_calls: this.streamingToolCalls.value,
extraInfo: {
created: Date.now(),
state: MessageState.Success,
serverName: llms[llmManager.currentModelIndex].id || 'unknown'
}
}); });
pinkLog('调用工具数量:' + this.streamingToolCalls.value.length);
const toolCallResult = await this.handleToolCalls(this.streamingToolCalls.value); const toolCallResult = await this.handleToolCalls(this.streamingToolCalls.value);
if (toolCallResult) {
console.log('toolCallResult', toolCallResult);
if (toolCallResult.state === MessageState.ParseJsonError) {
// 如果是因为解析 JSON 错误,则重新开始
tabStorage.messages.pop();
redLog('解析 JSON 错误 ' + this.streamingToolCalls.value[0]?.function?.arguments);
continue;
}
if (toolCallResult.state === MessageState.Success) {
const toolCall = this.streamingToolCalls.value[0]; const toolCall = this.streamingToolCalls.value[0];
tabStorage.messages.push({ tabStorage.messages.push({
role: 'tool', role: 'tool',
tool_call_id: toolCall.id || toolCall.function.name, tool_call_id: toolCall.id || toolCall.function.name,
content: toolCallResult content: toolCallResult.content,
extraInfo: {
created: Date.now(),
state: toolCallResult.state,
serverName: llms[llmManager.currentModelIndex].id || 'unknown',
usage: this.completionUsage
}
});
}
if (toolCallResult.state === MessageState.ToolCall) {
const toolCall = this.streamingToolCalls.value[0];
tabStorage.messages.push({
role: 'tool',
tool_call_id: toolCall.id || toolCall.function.name,
content: toolCallResult.content,
extraInfo: {
created: Date.now(),
state: toolCallResult.state,
serverName: llms[llmManager.currentModelIndex].id || 'unknown',
usage: this.completionUsage
}
}); });
} }
} else if (this.streamingContent.value) { } else if (this.streamingContent.value) {
tabStorage.messages.push({ tabStorage.messages.push({
role: 'assistant', role: 'assistant',
content: this.streamingContent.value content: this.streamingContent.value,
extraInfo: {
created: Date.now(),
state: MessageState.Success,
serverName: llms[llmManager.currentModelIndex].id || 'unknown',
usage: this.completionUsage
}
}); });
break; break;

View File

@ -0,0 +1,37 @@
import { IExtraInfo } from "../chat-box/chat";
export interface UsageStatistic {
input: number;
output: number;
total: number;
cacheHitRatio: number;
}
export function makeUsageStatistic(extraInfo: IExtraInfo): UsageStatistic | undefined {
if (extraInfo.serverName === 'unknown' || extraInfo.usage === undefined || extraInfo.usage === null) {
return undefined;
}
const usage = extraInfo.usage;
switch (extraInfo.serverName) {
case 'deepseek':
return {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.prompt_tokens + usage.completion_tokens,
cacheHitRatio: Math.ceil((usage.prompt_tokens_details?.cached_tokens || 0) / usage.prompt_tokens * 1000) / 10,
}
case 'openai':
return {
// TODO: 完成其他的数值统计
input: usage?.prompt_tokens,
output: usage?.completion_tokens,
total: usage.prompt_tokens + usage.completion_tokens,
cacheHitRatio: Math.ceil(usage.prompt_tokens_details?.cached_tokens || 0 / usage.prompt_tokens * 1000) / 10,
}
}
return undefined;
}

View File

@ -1,152 +1,73 @@
<template> <template>
<div class="chat-container" :ref="el => chatContainerRef = el"> <div class="chat-container" :ref="el => chatContainerRef = el">
<el-scrollbar ref="scrollbarRef" :height="'90%'" @scroll="handleScroll"> <el-scrollbar ref="scrollbarRef" :height="'90%'" @scroll="handleScroll" v-if="renderMessages.length > 0 || isLoading">
<div class="message-list" :ref="el => messageListRef = el"> <div class="message-list" :ref="el => messageListRef = el">
<div v-for="(message, index) in renderMessages" :key="index" <div v-for="(message, index) in renderMessages" :key="index"
:class="['message-item', message.role.split('/')[0]]"> :class="['message-item', message.role.split('/')[0], message.role.split('/')[1]]"
<div class="message-avatar" v-if="message.role.split('/')[0] === 'assistant'"> >
<div class="message-avatar" v-if="message.role === 'assistant/content'">
<span class="iconfont icon-robot"></span> <span class="iconfont icon-robot"></span>
</div> </div>
<div class="message-avatar" v-else-if="message.role === 'assistant/tool_calls'">
</div>
<!-- 用户输入的部分 --> <!-- 用户输入的部分 -->
<div class="message-content" v-if="message.role === 'user'"> <div class="message-content" v-if="message.role === 'user'">
<div class="message-role"></div> <Message.User :message="message" :tab-id="props.tabId" />
<div class="message-text">
<span>{{ message.content }}</span>
</div>
</div> </div>
<!-- 助手返回的内容部分 --> <!-- 助手返回的内容部分 -->
<div class="message-content" v-else-if="message.role === 'assistant/content'"> <div class="message-content" v-else-if="message.role === 'assistant/content'">
<div class="message-role">Agent</div> <Message.Assistant :message="message" :tab-id="props.tabId" />
<div class="message-text">
<div v-if="message.content" v-html="markdownToHtml(message.content)"></div>
</div>
</div> </div>
<!-- 助手调用的工具部分 --> <!-- 助手调用的工具部分 -->
<div class="message-content" v-else-if="message.role === 'assistant/tool_calls'"> <div class="message-content" v-else-if="message.role === 'assistant/tool_calls'">
<div class="message-role"> <Message.Toolcall
Agent :message="message" :tab-id="props.tabId"
<span class="message-reminder" v-if="!message.toolResult"> @update:tool-result="(value, index) => (message.toolResult || [])[index] = value"
正在使用工具
<span class="tool-loading iconfont icon-double-loading">
</span>
</span>
</div>
<div class="message-text tool_calls">
<div v-if="message.content" v-html="markdownToHtml(message.content)"></div>
<div class="tool-calls">
<div v-for="(call, index) in message.tool_calls" :key="index" class="tool-call-item">
<div class="tool-call-header">
<span class="tool-name">{{ call.function.name }}</span>
<span class="tool-type">{{ call.type }}</span>
</div>
<div class="tool-arguments">
<div class="inner">
<div v-html="jsonResultToHtml(call.function.arguments)"></div>
</div>
</div>
</div>
</div>
<!-- 工具调用结果 -->
<div v-if="message.toolResult">
<div class="tool-call-header">
<span class="tool-name">{{ "响应" }}</span>
<span style="width: 200px;" class="tools-dialog-container">
<el-switch
v-model="message.showJson!.value"
inline-prompt
active-text="JSON"
inactive-text="Text"
style="margin-left: 10px; width: 200px;"
:inactive-action-style="'backgroundColor: var(--sidebar)'"
/> />
</span>
</div>
<div class="tool-result" v-if="isValidJSON(message.toolResult)">
<div v-if="message.showJson!.value" class="tool-result-content">
<div class="inner">
<div v-html="jsonResultToHtml(message.toolResult)"></div>
</div>
</div>
<span v-else>
<div v-for="(item, index) in JSON.parse(message.toolResult)" :key="index">
<div v-if="item.type === 'text'" class="tool-text">{{ item.text }}</div>
<div v-else class="tool-other">{{ JSON.stringify(item) }}</div>
</div>
</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
<!-- 正在加载的部分实时解析 markdown --> <!-- 正在加载的部分实时解析 markdown -->
<div v-if="isLoading" class="message-item assistant"> <div v-if="isLoading" class="message-item assistant">
<div class="message-avatar"> <Message.StreamingBox :streaming-content="streamingContent" :tab-id="props.tabId" />
<span class="iconfont icon-chat"></span>
</div>
<div class="message-content">
<div class="message-role">
Agent
<span class="message-reminder">
正在生成答案
<span class="tool-loading iconfont icon-double-loading">
</span>
</span>
</div>
<div class="message-text">
<span v-html="waitingMarkdownToHtml(streamingContent)"></span>
</div>
</div>
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
<div v-else class="chat-openmcp-icon">
<el-footer class="chat-footer" ref="footerRef"> <div>
<div class="input-area"> <!-- <span class="iconfont icon-openmcp"></span> -->
<div class="input-wrapper"> <span>{{ t('press-and-run') }}
<Setting :tabId="tabId" /> <span style="padding: 5px 15px; border-radius: .5em; background-color: var(--background);">
<span class="iconfont icon-send"></span>
<el-input v-model="userInput" type="textarea" :rows="inputHeightLines" :maxlength="2000" </span>
placeholder="输入消息..." @keydown.enter="handleKeydown" resize="none" class="chat-input" /> </span>
<el-button type="primary" @click="isLoading ? handleAbort() : handleSend()" class="send-button">
<span v-if="!isLoading" class="iconfont icon-send"></span>
<span v-else class="iconfont icon-stop"></span>
</el-button>
</div> </div>
</div> </div>
</el-footer>
<ChatBox
:ref="el => footerRef = el"
:tab-id="props.tabId"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, nextTick, watch, Ref } from 'vue'; import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, nextTick, watch, provide } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ElMessage, ScrollbarInstance } from 'element-plus'; import { ElMessage, ScrollbarInstance } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ChatMessage, ChatStorage, getToolSchema, ToolCall } from './chat'; import { ChatMessage, ChatStorage, IRenderMessage, MessageState, ToolCall } from './chat-box/chat';
import * as Message from './message';
import ChatBox from './chat-box/index.vue';
import Setting from './setting.vue';
// markdown.ts
import { markdownToHtml, copyToClipboard } from './markdown';
import { TaskLoop } from './task-loop';
defineComponent({ name: 'chat' }); defineComponent({ name: 'chat' });
const { t } = useI18n(); const { t } = useI18n();
function waitingMarkdownToHtml(content: string) {
if (content) {
return markdownToHtml(content);
}
return '<span class="typing-cursor">|</span>';
}
const props = defineProps({ const props = defineProps({
tabId: { tabId: {
type: Number, type: Number,
@ -157,32 +78,19 @@ const props = defineProps({
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ChatStorage; const tabStorage = tab.storage as ChatStorage;
const userInput = ref('');
const inputHeightLines = computed(() => {
const currentLines = userInput.value.split('\n').length;
return Math.min(12, Math.max(5, currentLines));
});
// messages // messages
if (!tabStorage.messages) { if (!tabStorage.messages) {
tabStorage.messages = [] as ChatMessage[]; tabStorage.messages = [] as ChatMessage[];
} }
interface IRenderMessage {
role: 'user' | 'assistant/content' | 'assistant/tool_calls' | 'tool';
content: string;
toolResult?: string;
tool_calls?: ToolCall[];
showJson?: Ref<boolean>;
}
const renderMessages = computed(() => { const renderMessages = computed(() => {
const messages: IRenderMessage[] = []; const messages: IRenderMessage[] = [];
for (const message of tabStorage.messages) { for (const message of tabStorage.messages) {
if (message.role === 'user') { if (message.role === 'user') {
messages.push({ messages.push({
role: 'user', role: 'user',
content: message.content content: message.content,
extraInfo: message.extraInfo
}); });
} else if (message.role === 'assistant') { } else if (message.role === 'assistant') {
if (message.tool_calls) { if (message.tool_calls) {
@ -190,12 +98,17 @@ const renderMessages = computed(() => {
role: 'assistant/tool_calls', role: 'assistant/tool_calls',
content: message.content, content: message.content,
tool_calls: message.tool_calls, tool_calls: message.tool_calls,
showJson: ref(false) showJson: ref(false),
extraInfo: {
...message.extraInfo,
state: MessageState.Unknown
}
}); });
} else { } else {
messages.push({ messages.push({
role: 'assistant/content', role: 'assistant/content',
content: message.content content: message.content,
extraInfo: message.extraInfo
}); });
} }
@ -204,6 +117,8 @@ const renderMessages = computed(() => {
const lastAssistantMessage = messages[messages.length - 1]; const lastAssistantMessage = messages[messages.length - 1];
if (lastAssistantMessage.role === 'assistant/tool_calls') { if (lastAssistantMessage.role === 'assistant/tool_calls') {
lastAssistantMessage.toolResult = message.content; lastAssistantMessage.toolResult = message.content;
lastAssistantMessage.extraInfo.state = message.extraInfo.state;
lastAssistantMessage.extraInfo.usage = lastAssistantMessage.extraInfo.usage || message.extraInfo.usage;
} }
} }
} }
@ -218,26 +133,19 @@ const streamingToolCalls = ref<ToolCall[]>([]);
const chatContainerRef = ref<any>(null); const chatContainerRef = ref<any>(null);
const messageListRef = ref<any>(null); const messageListRef = ref<any>(null);
const footerRef = ref<any>(null);
const footerRef = ref<HTMLElement>();
const scrollHeight = ref('500px'); const scrollHeight = ref('500px');
const updateScrollHeight = () => { function updateScrollHeight() {
if (chatContainerRef.value && footerRef.value) { if (chatContainerRef.value && footerRef.value) {
const containerHeight = chatContainerRef.value.clientHeight; const containerHeight = chatContainerRef.value.clientHeight;
const footerHeight = footerRef.value.clientHeight; const footerHeight = footerRef.value.clientHeight;
scrollHeight.value = `${containerHeight - footerHeight}px`; scrollHeight.value = `${containerHeight - footerHeight}px`;
} }
};
const handleKeydown = (event: KeyboardEvent) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
handleSend();
} }
// Shift+Enter
};
provide('updateScrollHeight', updateScrollHeight);
const autoScroll = ref(true); const autoScroll = ref(true);
const scrollbarRef = ref<ScrollbarInstance>(); const scrollbarRef = ref<ScrollbarInstance>();
@ -252,8 +160,13 @@ const handleScroll = ({ scrollTop, scrollHeight, clientHeight }: {
autoScroll.value = scrollTop + clientHeight >= scrollHeight - 10; autoScroll.value = scrollTop + clientHeight >= scrollHeight - 10;
}; };
provide('streamingContent', streamingContent);
provide('streamingToolCalls', streamingToolCalls);
provide('isLoading', isLoading);
provide('autoScroll', autoScroll);
// scrollToBottom // scrollToBottom
const scrollToBottom = async () => { async function scrollToBottom() {
if (!scrollbarRef.value || !messageListRef.value) return; if (!scrollbarRef.value || !messageListRef.value) return;
await nextTick(); // DOM await nextTick(); // DOM
@ -266,7 +179,9 @@ const scrollToBottom = async () => {
} catch (error) { } catch (error) {
console.error('Scroll to bottom failed:', error); console.error('Scroll to bottom failed:', error);
} }
}; }
provide('scrollToBottom', scrollToBottom);
// streamingContent // streamingContent
watch(streamingContent, () => { watch(streamingContent, () => {
@ -281,99 +196,10 @@ watch(streamingToolCalls, () => {
} }
}, { deep: true }); }, { deep: true });
let loop: TaskLoop | undefined = undefined;
const handleSend = () => {
if (!userInput.value.trim() || isLoading.value) return;
autoScroll.value = true;
isLoading.value = true;
const userMessage = userInput.value.trim();
loop = new TaskLoop(streamingContent, streamingToolCalls);
loop.registerOnError((msg) => {
ElMessage({
message: msg,
type: 'error',
duration: 3000
});
tabStorage.messages.push({
role: 'assistant',
content: `错误: ${msg}`
});
isLoading.value = false;
});
loop.registerOnChunk((chunk) => {
scrollToBottom();
});
loop.registerOnDone(() => {
isLoading.value = false;
scrollToBottom();
});
loop.registerOnEpoch(() => {
isLoading.value = true;
scrollToBottom();
});
loop.start(tabStorage, userMessage);
userInput.value = '';
};
const handleAbort = () => {
if (loop) {
loop.abort();
isLoading.value = false;
ElMessage.info('请求已中止');
}
};
onMounted(() => {
updateScrollHeight();
window.addEventListener('resize', updateScrollHeight);
scrollToBottom();
});
onUnmounted(() => {
window.removeEventListener('resize', updateScrollHeight);
});
// JSON
const isValidJSON = (str: string) => {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
};
const jsonResultToHtml = (jsonString: string) => {
const formattedJson = JSON.stringify(JSON.parse(jsonString), null, 2);
const html = markdownToHtml('```json\n' + formattedJson + '\n```');
return html;
};
//
const formatToolArguments = (args: string) => {
try {
const parsed = JSON.parse(args);
return JSON.stringify(parsed, null, 2);
} catch {
return args;
}
};
</script> </script>
<style scoped> <style>
.chat-container { .chat-container {
height: 100%; height: 100%;
display: flex; display: flex;
@ -381,6 +207,30 @@ const formatToolArguments = (args: string) => {
flex-direction: column; flex-direction: column;
} }
.chat-openmcp-icon {
width: 100%;
display: flex;
justify-content: center;
height: 100%;
opacity: 0.75;
padding-top: 70px;
}
.chat-openmcp-icon > div {
display: flex;
flex-direction: column;
align-items: left;
font-size: 28px;
}
.chat-openmcp-icon > div > span {
margin-bottom: 23px;
}
.chat-openmcp-icon .iconfont {
font-size: 22px;
}
.message-list { .message-list {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: 0 auto;
@ -395,6 +245,7 @@ const formatToolArguments = (args: string) => {
.message-avatar { .message-avatar {
margin-right: 12px; margin-right: 12px;
margin-top: 1px;
} }
.message-content { .message-content {
@ -412,14 +263,10 @@ const formatToolArguments = (args: string) => {
line-height: 1.6; line-height: 1.6;
} }
.message-text.tool_calls {
border-left: 3px solid var(--main-color);
padding-left: 10px;
}
.user .message-text { .user .message-text {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
width: 100%;
} }
.user .message-text > span { .user .message-text > span {
@ -447,134 +294,10 @@ const formatToolArguments = (args: string) => {
margin-top: 30px; margin-top: 30px;
} }
.chat-footer { .assistant.tool_calls {
padding: 16px; margin-top: 5px;
border-top: 1px solid var(--el-border-color);
flex-shrink: 0;
position: absolute;
height: fit-content;
bottom: 0;
width: 100%;
} }
.input-area {
max-width: 800px;
margin: 0 auto;
position: relative;
}
.input-wrapper {
position: relative;
}
.chat-input {
padding-right: 80px;
}
.chat-input textarea {
border-radius: .5em;
}
.send-button {
position: absolute !important;
right: 8px !important;
bottom: 8px !important;
height: auto;
padding: 8px 12px;
font-size: 20px;
border-radius: .5em;
}
:deep(.chat-settings) {
position: absolute;
left: 0;
bottom: 0px;
z-index: 1;
}
.typing-cursor {
animation: blink 1s infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
</style>
<style scoped>
/* 原有样式保持不变 */
/* 新增工具调用样式 */
.tool-calls {
margin-top: 10px;
}
.tool-call-item {
margin-bottom: 10px;
}
.tool-call-header {
display: flex;
align-items: center;
margin-bottom: 5px;
}
.tool-name {
font-weight: bold;
color: var(--el-color-primary);
margin-right: 8px;
margin-bottom: 0;
display: flex;
align-items: center;
height: 26px;
}
.tool-type {
font-size: 0.8em;
color: var(--el-text-color-secondary);
background-color: var(--el-fill-color-light);
padding: 2px 6px;
display: flex;
align-items: center;
border-radius: 4px;
}
.tool-arguments {
margin: 0;
padding: 8px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
.tool-result {
padding: 8px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
}
.tool-text {
white-space: pre-wrap;
line-height: 1.6;
}
.tool-other {
font-family: monospace;
font-size: 0.9em;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
/* 新增样式来减小行距 */
.message-text p, .message-text p,
.message-text h3, .message-text h3,
.message-text ol, .message-text ol,
@ -582,7 +305,6 @@ const formatToolArguments = (args: string) => {
margin-top: 0.5em; margin-top: 0.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
line-height: 1.4; line-height: 1.4;
/* 可以根据需要调整行高 */
} }
.message-text ol li, .message-text ol li,

View File

@ -37,6 +37,7 @@
height: inherit; height: inherit;
display: block; display: block;
overflow: auto; overflow: auto;
background-color: unset !important;
} }
.vscode-dark :not(pre)>code[class*=language-], .vscode-dark :not(pre)>code[class*=language-],
@ -251,6 +252,7 @@
height: inherit; height: inherit;
display: block; display: block;
overflow: auto; overflow: auto;
background-color: unset !important;
} }
.vscode-light :not(pre)>code[class*=language-], .vscode-light :not(pre)>code[class*=language-],

View File

@ -0,0 +1,29 @@
<template>
<div class="message-role">Agent</div>
<div class="message-text">
<div v-if="message.content" v-html="markdownToHtml(props.message.content)"></div>
</div>
<MessageMeta :message="props.message" />
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
import MessageMeta from './message-meta.vue';
const props = defineProps({
message: {
type: Object,
required: true
},
tabId: {
type: Number,
required: true
}
});
</script>
<style>
</style>

View File

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

View File

@ -0,0 +1,82 @@
<template>
<div class="message-meta" @mouseenter="showTime = true" @mouseleave="showTime = false">
<span v-if="usageStatistic" class="message-usage">
<span>
{{ t('input-token') }} {{ usageStatistic.input }}
</span>
<span>
{{ t('output-token') }} {{ usageStatistic.output }}
</span>
<span>
{{ t('total') }} {{ usageStatistic.total }}
</span>
<span>
{{ t('cache-hit-ratio') }} {{ usageStatistic.cacheHitRatio }}%
</span>
</span>
<span v-else class="message-usage">
<span>{{ t('server-not-support-statistic') }}</span>
</span>
<span v-show="showTime" class="message-time">
{{ props.message.extraInfo.serverName }} {{ t('answer-at') }}
{{ new Date(message.extraInfo.created).toLocaleString() }}
</span>
</div>
</template>
<script setup lang="ts">
import { defineComponent, defineProps, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { makeUsageStatistic } from '@/components/main-panel/chat/core/usage';
defineComponent({ name: 'message-meta' });
const { t } = useI18n();
const props = defineProps({
message: {
type: Object,
required: true
}
});
const usageStatistic = computed(() => {
return makeUsageStatistic(props.message.extraInfo);
});
const showTime = ref(false);
</script>
<style scoped>
.message-meta {
margin-top: 8px;
font-size: 0.8em;
color: var(--el-text-color-secondary);
display: flex;
}
.message-time {
opacity: 0.7;
padding: 2px 6px 2px 0;
transition: opacity 0.3s ease;
}
.message-usage {
display: flex;
align-items: center;
}
.message-usage > span {
background-color: var(--el-fill-color-light);
padding: 2px 6px;
border-radius: 4px;
margin-right: 3px;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div class="message-avatar">
<span class="iconfont icon-chat"></span>
</div>
<div class="message-content">
<div class="message-role">
Agent
<span class="message-reminder">
{{ t('generate-answer') }}
<span class="tool-loading iconfont icon-double-loading">
</span>
</span>
</div>
<div class="message-text">
<span v-html="waitingMarkdownToHtml(streamingContent)"></span>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps } from 'vue';
import { useI18n } from 'vue-i18n';
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
const { t } = useI18n();
const props = defineProps({
streamingContent: {
type: String,
required: true
},
tabId: {
type: Number,
required: true
}
});
function waitingMarkdownToHtml(content: string) {
if (content) {
return markdownToHtml(content);
}
return '<span class="typing-cursor">|</span>';
}
</script>
<style>
</style>

View File

@ -0,0 +1,232 @@
<template>
<el-scrollbar width="100%">
<div v-if="props.item.type === 'text'" class="tool-text">
{{ props.item.text }}
</div>
<div v-else-if="props.item.type === 'image'" class="tool-image">
<div class="media-item" @click="showFullImage">
<img :src="thumbnail" alt="screenshot" />
<span class="float-container">
<!-- 后处理结束后显示的部分 -->
<span class="iconfont icon-image" v-if="finishProcess"></span>
<!-- 后处理时显示的部分 -->
<el-progress v-else
class="progress"
:percentage="progress"
:stroke-width="5"
type="circle"
:width="80"
color="var(--main-color)"
>
<template #default="{ percentage }">
<div class="progress-label">
<span class="percentage-value">{{ percentage }}%</span>
<span class="percentage-label">{{ progressText }}</span>
</div>
</template>
</el-progress>
</span>
</div>
</div>
<div v-else class="tool-other">{{ JSON.stringify(props.item) }}</div>
</el-scrollbar>
</template>
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
import { ToolCallContent } from '@/hook/type';
import { getBlobUrlByFilename } from '@/hook/util';
import { defineComponent, PropType, defineProps, ref, defineEmits } from 'vue';
defineComponent({ name: 'toolcall-result-item' });
const emits = defineEmits(['update:item', 'update:ocr-done']);
const props = defineProps({
item: {
type: Object as PropType<ToolCallContent>,
required: true
}
});
const metaInfo = props.item._meta || {};
const { ocr = false, workerId = '' } = metaInfo;
//
const progress = ref(0);
const progressText = ref('OCR');
const finishProcess = ref(true);
if (ocr) {
finishProcess.value = false;
const bridge = useMessageBridge();
const cancel = bridge.addCommandListener('ocr/worker/log', data => {
finishProcess.value = false;
const { id, progress: p = 1.0, status = 'finish' } = data;
if (id === workerId) {
progressText.value = status;
progress.value = Math.min(Math.ceil(Math.max(p * 100, 0)), 100);
}
}, { once: false });
bridge.addCommandListener('ocr/worker/done', data => {
if (data.id !== workerId) {
return;
}
progress.value = 1;
finishProcess.value = true;
if (props.item._meta) {
const { _meta, ...rest } = props.item;
emits('update:item', { ...rest });
}
emits('update:ocr-done');
cancel();
}, { once: true });
}
const thumbnail = ref('');
if (props.item.data) {
console.log(props.item.data);
getBlobUrlByFilename(props.item.data).then(url => {
console.log(url);
if (url) {
thumbnail.value = url;
}
});
}
const showFullImage = () => {
const img = new Image();
img.src = thumbnail.value;
img.onload = () => {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100%';
overlay.style.height = '100%';
overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
overlay.style.zIndex = '9999';
overlay.style.display = 'flex';
overlay.style.justifyContent = 'center';
overlay.style.alignItems = 'center';
overlay.onclick = () => document.body.removeChild(overlay);
const imgContainer = document.createElement('div');
imgContainer.style.maxWidth = '90vw';
imgContainer.style.maxHeight = '90vh';
imgContainer.style.overflow = 'auto';
const fullImg = new Image();
fullImg.src = thumbnail.value;
fullImg.style.width = 'auto';
fullImg.style.height = 'auto';
fullImg.style.maxWidth = '100%';
fullImg.style.maxHeight = '100%';
imgContainer.appendChild(fullImg);
overlay.appendChild(imgContainer);
document.body.appendChild(overlay);
};
};
</script>
<style>
.tool-image {
position: relative;
}
.tool-image .progress {
margin-top: 10px;
}
.tool-image .media-item {
position: relative;
width: 100px;
height: 100px;
background-color: var(--sidebar);
border-radius: .5em;
display: flex;
justify-content: center;
align-items: center;
}
.tool-image .media-item .iconfont {
font-size: 40px;
}
.tool-image .media-item {
object-fit: cover;
overflow: hidden;
}
.tool-image .media-item>img {
position: absolute;
top: 50%;
height: 100%;
width: 100%;
object-fit: cover;
transform: translateY(-50%);
}
.tool-image .media-item .float-container {
position: absolute;
left: 0;
top: 0;
width: 100px;
height: 100px;
background-color: rgba(0, 0, 0, 0.6);
opacity: 1;
display: flex;
justify-content: center;
align-items: center;
transition: var(--animation-3s);
}
.progress-label {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.percentage-label {
max-width: 50px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.percentage-value {
font-size: 14px;
font-weight: bold;
}
.tool-image .media-item .float-container .iconfont {
color: var(--background);
}
.tool-image .media-item:hover .float-container {
opacity: 1;
}
.media-item {
cursor: pointer;
}
.media-item:hover {
opacity: 0.9;
}
</style>

View File

@ -0,0 +1,333 @@
<template>
<div class="message-role">
<span class="message-reminder" v-if="!props.message.toolResult">
Agent 正在使用工具
<span class="tool-loading iconfont icon-double-loading">
</span>
</span>
</div>
<div class="message-text tool_calls" :class="[currentMessageLevel]">
<div v-if="props.message.content" v-html="markdownToHtml(props.message.content)"></div>
<el-collapse v-model="activeNames" v-if="props.message.tool_calls">
<el-collapse-item name="tool">
<template #title>
<div class="tool-calls">
<div class="tool-call-header">
<span class="tool-name">
<span class="iconfont icon-tool"></span>
{{ props.message.tool_calls[0].function.name }}
</span>
<el-button size="small" @click="createTest(props.message.tool_calls[0])">
<span class="iconfont icon-send"></span>
</el-button>
</div>
</div>
</template>
<div>
<div class="tool-arguments">
<div class="inner">
<div v-html="jsonResultToHtml(props.message.tool_calls[0].function.arguments)"></div>
</div>
</div>
<!-- 工具调用结果 -->
<div v-if="props.message.toolResult">
<div class="tool-call-header result">
<span class="tool-name">
<span :class="`iconfont icon-${currentMessageLevel}`"></span>
{{ isValid ? '响应': '错误' }}
<el-button v-if="!isValid" size="small"
@click="gotoIssue()"
>
反馈
</el-button>
</span>
<span style="width: 200px;" class="tools-dialog-container" v-if="currentMessageLevel === 'info'">
<el-switch v-model="props.message.showJson!.value" inline-prompt active-text="JSON"
inactive-text="Text" style="margin-left: 10px; width: 200px;"
:inactive-action-style="'backgroundColor: var(--sidebar)'" />
</span>
</div>
<div class="tool-result" v-if="isValid">
<!-- 展示 JSON -->
<div v-if="props.message.showJson!.value" class="tool-result-content">
<div class="inner">
<div v-html="toHtml(props.message.toolResult)"></div>
</div>
</div>
<!-- 展示富文本 -->
<span v-else>
<div v-for="(item, index) in props.message.toolResult" :key="index"
class="response-item"
>
<ToolcallResultItem
:item="item"
@update:item="value => updateToolCallResultItem(value, index)"
@update:ocr-done="value => collposePanel()"
/>
</div>
</span>
</div>
<div v-else class="tool-result">
<div class="tool-result-content"
v-for="(error, index) of collectErrors"
:key="index"
>
{{ error }}
</div>
</div>
</div>
<MessageMeta :message="message" />
</div>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, watch, PropType, computed, defineEmits } from 'vue';
import MessageMeta from './message-meta.vue';
import { markdownToHtml } from '@/components/main-panel/chat/markdown/markdown';
import { createTest } from '@/views/setting/llm';
import { IRenderMessage, MessageState } from '../chat-box/chat';
import { ToolCallContent } from '@/hook/type';
import ToolcallResultItem from './toolcall-result-item.vue';
const props = defineProps({
message: {
type: Object as PropType<IRenderMessage>,
required: true
},
tabId: {
type: Number,
required: true
}
});
const hasOcr = computed(() => {
for (const item of props.message.toolResult || []) {
const metaInfo = item._meta || {};
const { ocr = false } = metaInfo;
if (ocr) {
return true;
}
}
return false;
});
const activeNames = ref<string[]>(props.message.toolResult ? [''] : ['tool']);
watch(
() => props.message.toolResult,
(value, _) => {
if (hasOcr.value) {
return;
}
if (value) {
collposePanel();
}
}
);
function collposePanel() {
setTimeout(() => {
activeNames.value = [''];
}, 1000);
}
/**
* @description 将工具调用结果转换成 html
* @param toolResult
*/
const toHtml = (toolResult: ToolCallContent[]) => {
const formattedJson = JSON.stringify(toolResult, null, 2);
const html = markdownToHtml('```json\n' + formattedJson + '\n```');
return html;
};
const jsonResultToHtml = (jsonResult: string) => {
try {
const formattedJson = JSON.stringify(JSON.parse(jsonResult), null, 2);
const html = markdownToHtml('```json\n' + formattedJson + '\n```');
return html;
} catch (error) {
const html = markdownToHtml('```json\n' + jsonResult + '\n```');
return html;
}
}
function gotoIssue() {
window.open('https://github.com/LSTM-Kirigaya/openmcp-client/issues', '_blank');
}
const isValid = computed(() => {
try {
const item = props.message.toolResult![0];
if (item.type === 'error') {
return false;
}
return true;
} catch {
return false;
}
});
const currentMessageLevel = computed(() => {
if (!isValid.value) {
return 'error';
}
if (props.message.extraInfo.state != MessageState.Success) {
return 'warning';
}
return 'info';
})
const collectErrors = computed(() => {
const errorMessages = [];
try {
const errorResults = props.message.toolResult!.filter(item => item.type === 'error');
console.log(errorResults);
for (const errorResult of errorResults) {
errorMessages.push(errorResult.text);
}
return errorMessages;
} catch {
return errorMessages;
}
});
const emit = defineEmits(['update:tool-result']);
function updateToolCallResultItem(value: any, index: number) {
emit('update:tool-result', value, index);
}
</script>
<style>
.message-text.tool_calls {
border: 1px solid var(--main-color);
border-radius: .5em;
padding: 3px 10px;
}
.message-text.tool_calls.warning {
border: 1px solid var(--el-color-warning);
}
.message-text.tool_calls.warning .tool-name {
color: var(--el-color-warning);
}
.message-text.tool_calls.warning .tool-result {
background-color: rgba(230, 162, 60, 0.5);
}
.message-text.tool_calls.error {
border: 1px solid var(--el-color-error);
}
.message-text.tool_calls.error .tool-name {
color: var(--el-color-error);
}
.message-text.tool_calls.error .tool-result {
background-color: rgba(245, 108, 108, 0.5);
}
.message-text .el-collapse-item__header {
display: flex;
align-items: center;
height: fit-content;
}
.message-text .el-collapse-item__content {
padding-bottom: 5px;
}
.tool-call-item {
margin-bottom: 10px;
}
.tool-call-header {
display: flex;
align-items: center;
}
.tool-call-header.result {
margin-top: 10px;
}
.tool-name {
font-weight: bold;
color: var(--el-color-primary);
margin-right: 8px;
margin-bottom: 0;
display: flex;
align-items: center;
height: 26px;
display: flex;
align-items: center;
}
.tool-name .iconfont {
margin-right: 5px;
}
.tool-type {
font-size: 0.8em;
color: var(--el-text-color-secondary);
background-color: var(--el-fill-color-light);
padding: 2px 6px;
display: flex;
align-items: center;
border-radius: 4px;
margin-right: 10px;
height: 22px;
}
.response-item {
margin-bottom: 10px;
}
.tool-arguments {
margin: 0;
padding: 8px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
.tool-result {
padding: 8px;
background-color: var(--el-fill-color-light);
border-radius: 4px;
}
.tool-text {
white-space: pre-wrap;
line-height: 1.6;
}
.tool-other {
font-family: monospace;
font-size: 0.9em;
color: var(--el-text-color-secondary);
margin-top: 4px;
}
</style>

View File

@ -0,0 +1,142 @@
<template>
<div class="message-role"></div>
<div class="message-text">
<div v-if="!isEditing" class="message-content">
<span>
{{ props.message.content.trim() }}
</span>
</div>
<KCuteTextarea v-else
v-model="userInput"
:placeholder="t('enter-message-dot')"
@press-enter="handleKeydown"
/>
<div class="message-actions" v-if="!isEditing">
<el-button @click="copy">
<span class="iconfont icon-copy"></span>
</el-button>
<el-button @click="toggleEdit">
<span class="iconfont icon-edit2"></span>
</el-button>
</div>
<div class="message-actions" v-else>
<el-button @click="toggleEdit">
{{ '取消' }}
</el-button>
<el-button @click="handleKeydown" type="primary">
{{ '发送' }}
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, ref, PropType, inject } from 'vue';
import { tabs } from '../../panel';
import { ChatStorage, IRenderMessage } from '../chat';
import KCuteTextarea from '@/components/k-cute-textarea/index.vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
message: {
type: Object as PropType<IRenderMessage>,
required: true
},
tabId: {
type: Number,
required: true
}
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ChatStorage;
const isEditing = ref(false);
const userInput = ref('');
const handleSend = inject<(newMessage: string | undefined) => void>('handleSend');
const toggleEdit = () => {
isEditing.value = !isEditing.value;
if (isEditing.value) {
userInput.value = props.message.content;
}
};
const handleKeydown = (event: KeyboardEvent) => {
const index = tabStorage.messages.findIndex(msg => msg.extraInfo === props.message.extraInfo);
if (index !== -1 && handleSend) {
// index index
tabStorage.messages.splice(index);
handleSend(userInput.value);
isEditing.value = false;
}
};
const copy = async () => {
try {
await navigator.clipboard.writeText(userInput.value);
ElMessage.success('内容已复制到剪贴板');
} catch (err) {
console.error('无法复制内容: ', err);
}
};
</script>
<style>
.message-text {
position: relative;
}
.message-text:hover .message-actions {
opacity: 1;
}
.message-actions {
opacity: 0;
transition: var(--animation-3s);
position: absolute;
bottom: -34px;
right: 0;
}
.message-actions .el-button {
border-radius: .5em;
padding: 5px 8px;
}
.message-actions .el-button:hover {
background-color: var(--main-light-color);
}
.message-actions .el-button+.el-button {
margin-left: 10px;
}
.user .message-content {
display: flex;
align-items: center;
justify-content: flex-end;
width: 100%;
}
.user .message-content > span {
max-width: calc(100% - 48px);
border-radius: .9em;
background-color: var(--main-light-color);
padding: 10px 15px;
box-sizing: border-box;
white-space: pre-wrap;
word-break: break-word;
text-align: left;
}
</style>

View File

@ -1,410 +0,0 @@
<template>
<div class="chat-settings">
<el-tooltip content="选择模型" placement="top">
<div class="setting-button" @click="showModelDialog = true">
<span class="iconfont icon-model">
{{ currentServerName }}/{{ currentModelName }}
</span>
</div>
</el-tooltip>
<el-tooltip content="系统提示词" placement="top">
<div class="setting-button" :class="{ 'active': hasSystemPrompt }" size="small"
@click="showSystemPromptDialog = true">
<span class="iconfont icon-robot"></span>
</div>
</el-tooltip>
<el-tooltip content="工具使用" placement="top">
<div class="setting-button" :class="{ 'active': toolActive }" size="small"
@click="toggleTools">
<span class="iconfont icon-tool"></span>
</div>
</el-tooltip>
<el-tooltip content="网络搜索" placement="top">
<div class="setting-button" :class="{ 'active': tabStorage.settings.enableWebSearch }" size="small"
@click="toggleWebSearch">
<span class="iconfont icon-web"></span>
</div>
</el-tooltip>
<el-tooltip content="温度参数" placement="top">
<div class="setting-button" @click="showTemperatureSlider = true">
<span class="iconfont icon-temperature"></span>
<span class="value-badge">{{ tabStorage.settings.temperature.toFixed(1) }}</span>
</div>
</el-tooltip>
<el-tooltip content="上下文长度" placement="top">
<div class="setting-button" @click="showContextLengthDialog = true">
<span class="iconfont icon-length"></span>
<span class="value-badge">{{ tabStorage.settings.contextLength }}</span>
</div>
</el-tooltip>
<!-- 模型选择对话框 -->
<el-dialog v-model="showModelDialog" title="选择模型" width="400px">
<el-radio-group v-model="selectedModelIndex" @change="onRadioGroupChange">
<el-radio v-for="(model, index) in availableModels" :key="index" :label="index">
{{ model }}
</el-radio>
</el-radio-group>
<template #footer>
<el-button @click="showModelDialog = false">取消</el-button>
<el-button type="primary" @click="confirmModelChange">确认</el-button>
</template>
</el-dialog>
<!-- System Prompt对话框 -->
<el-dialog v-model="showSystemPromptDialog" title="系统提示词" width="600px">
<el-input v-model="tabStorage.settings.systemPrompt" type="textarea" :rows="8"
placeholder="输入系统提示词(例如:你是一个专业的前端开发助手,用中文回答)" clearable />
<template #footer>
<el-button @click="showSystemPromptDialog = false">关闭</el-button>
<el-button type="primary" @click="showSystemPromptDialog = false">保存</el-button>
</template>
</el-dialog>
<!-- 温度参数滑块 -->
<el-dialog v-model="showTemperatureSlider" title="设置温度参数" width="400px">
<div class="slider-container">
<el-slider v-model="tabStorage.settings.temperature" :min="0" :max="2" :step="0.1" />
<div class="slider-tips">
<span>精确(0)</span>
<span>平衡(1)</span>
<span>创意(2)</span>
</div>
</div>
<template #footer>
<el-button @click="showTemperatureSlider = false">关闭</el-button>
</template>
</el-dialog>
<!-- 上下文长度设置 - 改为滑块形式 -->
<el-dialog v-model="showContextLengthDialog" title="设置上下文长度" width="400px">
<div class="slider-container">
<el-slider v-model="tabStorage.settings.contextLength" :min="0" :max="99" :step="1" />
<div class="slider-tips">
<span>0: 无上下文</span>
<span>10: 默认</span>
<span>99: 最大</span>
</div>
</div>
<template #footer>
<el-button @click="showContextLengthDialog = false">关闭</el-button>
</template>
</el-dialog>
<!-- 修改后的工具使用对话框 -->
<el-dialog v-model="showToolsDialog" title="工具管理" width="800px">
<div class="tools-dialog-container">
<el-scrollbar height="400px" class="tools-list">
<div v-for="(tool, index) in tabStorage.settings.enableTools" :key="index" class="tool-item">
<div class="tool-info">
<div class="tool-name">{{ tool.name }}</div>
<div v-if="tool.description" class="tool-description">{{ tool.description }}</div>
</div>
<el-switch v-model="tool.enabled"/>
</div>
</el-scrollbar>
<el-scrollbar height="400px" class="schema-viewer">
<div v-html="activeToolsSchemaHTML"></div>
</el-scrollbar>
</div>
<template #footer>
<el-button type="primary" @click="enableAllTools">激活所有工具</el-button>
<el-button type="danger" @click="disableAllTools">禁用所有工具</el-button>
<el-button type="primary" @click="showToolsDialog = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, defineProps, onMounted, onUnmounted } from 'vue';
import { llmManager, llms } from '@/views/setting/llm';
import { tabs } from '../panel';
import { allTools, ChatSetting, ChatStorage, getToolSchema } from './chat';
import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, ToolItem, ToolsListResponse } from '@/hook/type';
import { markdownToHtml } from './markdown';
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const showModelDialog = ref(false);
const showTemperatureSlider = ref(false);
const showContextLengthDialog = ref(false);
const showSystemPromptDialog = ref(false);
const currentServerName = computed(() => {
const currentLlm = llms[llmManager.currentModelIndex];
if (currentLlm) {
return currentLlm.name;
}
return '';
});
const currentModelName = computed(() => {
const currentLlm = llms[llmManager.currentModelIndex];
if (currentLlm) {
return currentLlm.models[selectedModelIndex.value];
}
return '';
});
const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ChatStorage & { settings: ChatSetting };
if (!tabStorage.settings) {
tabStorage.settings = {
modelIndex: llmManager.currentModelIndex,
enableTools: [],
enableWebSearch: false,
temperature: 0.7,
contextLength: 10,
systemPrompt: ''
} as ChatSetting;
}
const selectedModelIndex = ref(llmManager.currentModelIndex);
const availableModels = computed(() => {
return llms[llmManager.currentModelIndex].models;
});
const hasSystemPrompt = computed(() => {
return !!tabStorage.settings.systemPrompt?.trim();
});
const showToolsDialog = ref(false);
const toolActive = computed(() => {
const availableTools = tabStorage.settings.enableTools.filter(tool => tool.enabled);
return availableTools.length > 0;
});
// toggleTools
const toggleTools = () => {
showToolsDialog.value = true;
};
const toggleWebSearch = () => {
tabStorage.settings.enableWebSearch = !tabStorage.settings.enableWebSearch;
};
const confirmModelChange = () => {
llmManager.currentModelIndex = selectedModelIndex.value;
showModelDialog.value = false;
};
const onRadioGroupChange = () => {
const currentModel = llms[llmManager.currentModelIndex].models[selectedModelIndex.value];
llms[llmManager.currentModelIndex].userModel = currentModel;
};
const bridge = useMessageBridge();
let commandCancel: (() => void);
onMounted(() => {
commandCancel = bridge.addCommandListener('tools/list', (data: CasualRestAPI<ToolsListResponse>) => {
allTools.value = data.msg.tools || [];
tabStorage.settings.enableTools = [];
for (const tool of allTools.value) {
tabStorage.settings.enableTools.push({
name: tool.name,
description: tool.description,
enabled: true
});
}
}, { once: false });
bridge.postMessage({
command: 'tools/list'
});
});
onUnmounted(() => {
if (commandCancel) {
commandCancel();
}
})
// JSON Schema
const activeToolsSchemaHTML = computed(() => {
const toolsSchema = getToolSchema(tabStorage.settings.enableTools);
const jsonString = JSON.stringify(toolsSchema, null, 2);
return markdownToHtml(
"```json\n" + jsonString + "\n```"
);
});
// -
const enableAllTools = () => {
tabStorage.settings.enableTools.forEach(tool => {
tool.enabled = true;
});
};
// -
const disableAllTools = () => {
tabStorage.settings.enableTools.forEach(tool => {
tool.enabled = false;
});
};
</script>
<style>
.chat-settings {
display: flex;
gap: 2px;
padding: 8px 0;
background-color: var(--sidebar);
width: fit-content;
border-radius: 99%;
bottom: 0px;
z-index: 10;
position: absolute;
}
.setting-button {
padding: 5px 8px;
margin-right: 3px;
border-radius: .5em;
font-size: 12px;
position: relative;
user-select: none;
-webkit-user-drag: none;
display: flex;
align-items: center;
cursor: pointer;
transition: var(--animation-3s);
}
.setting-button.active {
background-color: var(--el-color-primary);
color: var(--el-text-color-primary);
transition: var(--animation-3s);
}
.setting-button.active:hover {
background-color: var(--el-color-primary);
transition: var(--animation-3s);
}
.setting-button:hover {
background-color: var(--background);
transition: var(--animation-3s);
}
.value-badge {
font-size: 10px;
padding: 1px 4px;
border-radius: 4px;
}
.slider-container {
padding: 0 10px;
}
.icon-temperature {
font-size: 18px;
}
.icon-length {
font-size: 16px;
}
.slider-tips {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
/* 新增工具相关样式 */
.tools-container {
padding: 10px;
}
.tool-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--el-border-color-light);
}
.tool-info {
flex: 1;
margin-right: 20px;
}
.tool-name {
font-weight: 500;
margin-bottom: 4px;
}
.tool-description {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.tools-dialog-container .el-switch__core {
border: 1px solid var(--main-color) !important;
}
.tools-dialog-container .el-switch .el-switch__action {
background-color: var(--main-color);
}
.tools-dialog-container .el-switch.is-checked .el-switch__action {
background-color: var(--sidebar);
}
/* 新增工具对话框样式 */
.tools-dialog-container {
display: flex;
gap: 16px;
}
.tools-list {
flex: 1;
border-right: 1px solid var(--el-border-color);
padding-right: 16px;
}
.schema-viewer {
flex: 1;
}
.schema-viewer pre {
margin: 0;
border-radius: 4px;
white-space: pre-wrap;
word-wrap: break-word;
background-color: var(--el-bg-color-overlay);
}
.schema-viewer .openmcp-code-block {
border: none;
}
.schema-viewer code {
font-family: var(--code-font-family);
font-size: 12px;
color: var(--el-text-color-primary);
}
</style>

View File

@ -6,7 +6,7 @@
<span <span
class="tab" class="tab"
v-for="(tab, index) of tabs.content" v-for="(tab, index) of tabs.content"
:key="index" :key="tab.id"
:class="{ 'active-tab': tabs.activeIndex === index }" :class="{ 'active-tab': tabs.activeIndex === index }"
@click="setActiveTab(index)" @click="setActiveTab(index)"
> >
@ -40,6 +40,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { addNewTab, tabs, closeTab } from './panel'; import { addNewTab, tabs, closeTab } from './panel';
import { panelLoaded } from '@/hook/panel';
defineComponent({ name: 'main-panel' }); defineComponent({ name: 'main-panel' });
@ -69,20 +70,19 @@ function setActiveTab(index: number) {
<style> <style>
.main-panel-container { .main-panel-container {
display: flex;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
min-width: 800px; min-width: 800px;
height: 100%; height: 100%;
margin-left: 20px; margin-left: 5px;
} }
.main-panel { .main-panel {
background-color: var(--sidebar); background-color: var(--sidebar);
border-radius: 1.2em; border-radius: 1.2em;
width: 100%; width: 100%;
height: 90%; height: calc(100% - 35px);
} }
.scroll-tabs-container { .scroll-tabs-container {
@ -91,13 +91,13 @@ function setActiveTab(index: number) {
} }
.tabs-container { .tabs-container {
height: 78px; height: 30px;
width: 90%; width: 90%;
background-color: var(--background); background-color: var(--background);
display: flex; display: flex;
align-items: center; align-items: center;
user-select: none; user-select: none;
padding: 0 10px; margin-bottom: 5px;
} }
.tabs-container .el-scrollbar { .tabs-container .el-scrollbar {
@ -106,12 +106,12 @@ function setActiveTab(index: number) {
.tabs-container .tab { .tabs-container .tab {
white-space: nowrap; white-space: nowrap;
margin: 5px; margin-right: 5px;
font-size: 13px; font-size: 12px;
width: 120px; width: 120px;
border-radius: .5em; border-radius: .5em;
background-color: var(--sidebar); background-color: var(--sidebar);
padding: 10px; padding: 3px 10px;
display: flex; display: flex;
align-items: center; align-items: center;
transition: var(--animation-3s); transition: var(--animation-3s);
@ -159,11 +159,9 @@ function setActiveTab(index: number) {
.tabs-container .add-button { .tabs-container .add-button {
cursor: pointer; cursor: pointer;
font-size: 20px; font-size: 15px;
margin-left: 5px; margin-left: 5px;
border-radius: .5em; border-radius: .5em;
height: 35px;
width: 35px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@ -1,5 +1,5 @@
import { watch, reactive } from 'vue'; import { watch, reactive } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { v4 as uuidv4 } from 'uuid';
import Resource from './resource/index.vue'; import Resource from './resource/index.vue';
import Chat from './chat/index.vue'; import Chat from './chat/index.vue';
@ -11,6 +11,7 @@ import { safeSavePanels, savePanels } from '@/hook/panel';
const { t } = I18n.global; const { t } = I18n.global;
interface Tab { interface Tab {
id: string;
name: string; name: string;
icon: string; icon: string;
type: string; type: string;
@ -50,8 +51,9 @@ watch(
{ deep: true } { deep: true }
); );
function createTab(type: string, index: number): Tab { export function createTab(type: string, index: number): Tab {
let customName: string | null = null; let customName: string | null = null;
const id = uuidv4();
return { return {
get name() { get name() {
@ -65,6 +67,7 @@ function createTab(type: string, index: number): Tab {
}, },
icon: 'icon-blank', icon: 'icon-blank',
type, type,
id,
componentIndex: -1, componentIndex: -1,
component: undefined, component: undefined,
storage: {}, storage: {},
@ -83,6 +86,8 @@ export function closeTab(index: number) {
tabs.content.splice(index, 1); tabs.content.splice(index, 1);
console.log(tabs.content);
// 调整活动标签索引 // 调整活动标签索引
if (tabs.activeIndex >= index) { if (tabs.activeIndex >= index) {
tabs.activeIndex = Math.max(0, tabs.activeIndex - 1); tabs.activeIndex = Math.max(0, tabs.activeIndex - 1);

View File

@ -1,27 +1,22 @@
<template> <template>
<el-scrollbar height="100%">
<div class="prompt-module"> <div class="prompt-module">
<div class="left"> <div class="left">
<h2> <h2>
<span class="iconfont icon-chat"></span> <span class="iconfont icon-chat"></span>
提示词模块 提示词模块
</h2> </h2>
<h3><code>prompts/list</code></h3>
<PromptTemplates <PromptTemplates :tab-id="props.tabId"></PromptTemplates>
:tab-id="props.tabId"
></PromptTemplates>
</div> </div>
<div class="right"> <div class="right">
<PromptReader <PromptReader :tab-id="props.tabId"></PromptReader>
:tab-id="props.tabId"
></PromptReader>
<PromptLogger <PromptLogger :tab-id="props.tabId"></PromptLogger>
:tab-id="props.tabId"
></PromptLogger>
</div> </div>
</div> </div>
</el-scrollbar>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -13,7 +13,7 @@
/> />
</span> </span>
</span> </span>
<el-scrollbar height="350px"> <el-scrollbar>
<div <div
class="output-content" class="output-content"
contenteditable="false" contenteditable="false"

View File

@ -3,20 +3,20 @@
<h3>{{ currentPrompt.name }}</h3> <h3>{{ currentPrompt.name }}</h3>
</div> </div>
<div class="prompt-reader-container"> <div class="prompt-reader-container">
<el-form :model="formData" :rules="formRules" ref="formRef" label-position="top"> <el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
<el-form-item v-for="param in currentPrompt?.params" :key="param.name" <el-form-item v-for="param in currentPrompt?.params" :key="param.name"
:label="param.name" :prop="param.name"> :label="param.name" :prop="param.name">
<el-input v-if="param.type === 'string'" v-model="formData[param.name]" <el-input v-if="param.type === 'string'" v-model="tabStorage.formData[param.name]"
:placeholder="param.placeholder || `请输入${param.name}`" :placeholder="param.placeholder || `请输入${param.name}`"
@keydown.enter.prevent="handleSubmit" @keydown.enter.prevent="handleSubmit"
/> />
<el-input-number v-else-if="param.type === 'number'" v-model="formData[param.name]" <el-input-number v-else-if="param.type === 'number'" v-model="tabStorage.formData[param.name]"
:placeholder="param.placeholder || `请输入${param.name}`" :placeholder="param.placeholder || `请输入${param.name}`"
@keydown.enter.prevent="handleSubmit" @keydown.enter.prevent="handleSubmit"
/> />
<el-switch v-else-if="param.type === 'boolean'" v-model="formData[param.name]" /> <el-switch v-else-if="param.type === 'boolean'" v-model="tabStorage.formData[param.name]" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -32,13 +32,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, defineProps, watch, ref, computed } from 'vue'; import { defineComponent, defineProps, defineEmits, watch, ref, computed, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { parsePromptTemplate, promptsManager, PromptStorage } from './prompts'; import { promptsManager, PromptStorage } from './prompts';
import { CasualRestAPI, PromptsGetResponse } from '@/hook/type'; import { PromptsGetResponse } from '@/hook/type';
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
defineComponent({ name: 'prompt-reader' }); defineComponent({ name: 'prompt-reader' });
@ -48,15 +49,32 @@ const props = defineProps({
tabId: { tabId: {
type: Number, type: Number,
required: true required: true
},
currentPromptName: {
type: String,
required: false
} }
}); });
const tab = tabs.content[props.tabId]; const emits = defineEmits(['prompt-get-response']);
const tabStorage = tab.storage as PromptStorage;
let tabStorage: PromptStorage;
if (props.tabId >= 0) {
tabStorage = tabs.content[props.tabId].storage as PromptStorage;
} else {
tabStorage = reactive({
currentPromptName: props.currentPromptName || '',
formData: {},
lastPromptGetResponse: undefined
});
}
if (!tabStorage.formData) {
tabStorage.formData = {};
}
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
// TODO: formData tabStorage
const formData = ref<Record<string, any>>({});
const loading = ref(false); const loading = ref(false);
const responseData = ref<PromptsGetResponse>(); const responseData = ref<PromptsGetResponse>();
@ -93,11 +111,18 @@ const formRules = computed<FormRules>(() => {
}); });
const initFormData = () => { const initFormData = () => {
formData.value = {}
currentPrompt.value?.params.forEach(param => { if (!currentPrompt.value?.params) return;
formData.value[param.name] = param.type === 'number' ? 0 :
param.type === 'boolean' ? false : '' const newSchemaDataForm: Record<string, number | boolean | string> = {};
})
currentPrompt.value.params.forEach(param => {
newSchemaDataForm[param.name] = getDefaultValue(param);
const originType = normaliseJavascriptType(typeof tabStorage.formData[param.name]);
if (tabStorage.formData[param.name]!== undefined && originType === param.type) {
newSchemaDataForm[param.name] = tabStorage.formData[param.name];
}
});
} }
const resetForm = () => { const resetForm = () => {
@ -105,24 +130,25 @@ const resetForm = () => {
responseData.value = undefined; responseData.value = undefined;
} }
function handleSubmit() { async function handleSubmit() {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
bridge.addCommandListener('prompts/get', (data: CasualRestAPI<PromptsGetResponse>) => { const { code, msg } = await bridge.commandRequest('prompts/get', {
tabStorage.lastPromptGetResponse = data.msg; promptId: currentPrompt.value.name,
}, { once: true }); args: JSON.parse(JSON.stringify(tabStorage.formData))
bridge.postMessage({
command: 'prompts/get',
data: { promptId: currentPrompt.value.name, args: JSON.parse(JSON.stringify(formData.value)) }
}); });
tabStorage.lastPromptGetResponse = msg;
emits('prompt-get-response', msg);
} }
if (props.tabId >= 0) {
watch(() => tabStorage.currentPromptName, () => { watch(() => tabStorage.currentPromptName, () => {
initFormData(); initFormData();
resetForm(); resetForm();
}, { immediate: true }); }, { immediate: true });
}
</script> </script>
<style> <style>

View File

@ -1,10 +1,18 @@
<template> <template>
<h3 class="resource-template">
<code>prompts/list</code>
<span
@click="reloadPrompts({ first: false })"
class="iconfont icon-restart"
></span>
</h3>
<div class="prompt-template-container-scrollbar"> <div class="prompt-template-container-scrollbar">
<el-scrollbar height="500px"> <el-scrollbar height="500px">
<div class="prompt-template-container"> <div class="prompt-template-container">
<div <div
class="item" class="item"
:class="{ 'active': tabStorage.currentPromptName === template.name }" :class="{ 'active': props.tabId >= 0 && tabStorage.currentPromptName === template.name }"
v-for="template of promptsManager.templates" v-for="template of promptsManager.templates"
:key="template.name" :key="template.name"
@click="handleClick(template)" @click="handleClick(template)"
@ -15,20 +23,12 @@
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
<div class="prompt-template-function-container">
<el-button
type="primary"
@click="reloadPrompts({ first: false })"
>
{{ t('refresh') }}
</el-button>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, PromptTemplate, PromptsListResponse } from '@/hook/type'; import { CasualRestAPI, PromptTemplate, PromptsListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps } from 'vue'; import { onMounted, onUnmounted, defineProps, defineEmits, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { promptsManager, PromptStorage } from './prompts'; import { promptsManager, PromptStorage } from './prompts';
import { tabs } from '../panel'; import { tabs } from '../panel';
@ -44,8 +44,20 @@ const props = defineProps({
} }
}); });
const emits = defineEmits([ 'prompt-selected' ]);
let tabStorage: PromptStorage;
if (props.tabId >= 0) {
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as PromptStorage; tabStorage = tab.storage as PromptStorage;
} else {
tabStorage = reactive({
currentPromptName: '',
formData: {},
lastPromptGetResponse: undefined
});
}
function reloadPrompts(option: { first: boolean }) { function reloadPrompts(option: { first: boolean }) {
bridge.postMessage({ bridge.postMessage({
@ -62,9 +74,11 @@ function reloadPrompts(option: { first: boolean }) {
} }
} }
function handleClick(template: PromptTemplate) { function handleClick(prompt: PromptTemplate) {
tabStorage.currentPromptName = template.name; tabStorage.currentPromptName = prompt.name;
tabStorage.lastPromptGetResponse = undefined; tabStorage.lastPromptGetResponse = undefined;
emits('prompt-selected', prompt);
} }
let commandCancel: (() => void); let commandCancel: (() => void);
@ -73,7 +87,9 @@ onMounted(() => {
commandCancel = bridge.addCommandListener('prompts/list', (data: CasualRestAPI<PromptsListResponse>) => { commandCancel = bridge.addCommandListener('prompts/list', (data: CasualRestAPI<PromptsListResponse>) => {
promptsManager.templates = data.msg.prompts || []; promptsManager.templates = data.msg.prompts || [];
if (promptsManager.templates.length > 0) { const targetPrompt = promptsManager.templates.find(template => template.name === tabStorage.currentPromptName);
if (targetPrompt === undefined) {
tabStorage.currentPromptName = promptsManager.templates[0].name; tabStorage.currentPromptName = promptsManager.templates[0].name;
tabStorage.lastPromptGetResponse = undefined; tabStorage.lastPromptGetResponse = undefined;
} }

View File

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

View File

@ -1,15 +1,19 @@
<template> <template>
<el-scrollbar height="100%">
<div class="resource-module"> <div class="resource-module">
<div class="left"> <div class="left">
<h2> <h2>
<span class="iconfont icon-file"></span> <span class="iconfont icon-file"></span>
{{ t("resources") + t("module") }} {{ t("resources") + t("module") }}
</h2> </h2>
<h3><code>resources/templates/list</code></h3>
<ResourceTemplates <ResourceListTemplates
:tab-id="props.tabId" :tab-id="props.tabId"
></ResourceTemplates> ></ResourceListTemplates>
<ResourceList
:tab-id="props.tabId"
></ResourceList>
</div> </div>
<div class="right"> <div class="right">
@ -22,12 +26,16 @@
></ResourceLogger> ></ResourceLogger>
</div> </div>
</div> </div>
</el-scrollbar>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineProps } from 'vue'; import { defineProps } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import ResourceTemplates from './resource-templates.vue';
import ResourceListTemplates from './resource-list-templates.vue';
import ResourceList from './resource-list.vue';
import ResourceReader from './resouce-reader.vue'; import ResourceReader from './resouce-reader.vue';
import ResourceLogger from './resource-logger.vue'; import ResourceLogger from './resource-logger.vue';
@ -40,8 +48,6 @@ const props = defineProps({
} }
}); });
</script> </script>
<style scoped> <style scoped>

View File

@ -3,19 +3,17 @@
<h3>{{ currentResource.template?.name }}</h3> <h3>{{ currentResource.template?.name }}</h3>
</div> </div>
<div class="resource-reader-container"> <div class="resource-reader-container">
<el-form :model="formData" :rules="formRules" ref="formRef" label-position="top"> <el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
<el-form-item v-for="param in currentResource?.params" :key="param.name" <el-form-item v-for="param in currentResource?.params" :key="param.name" :label="param.name"
:label="param.name" :prop="param.name"> :prop="param.name">
<!-- 根据不同类型渲染不同输入组件 --> <!-- 根据不同类型渲染不同输入组件 -->
<el-input v-if="param.type === 'string'" v-model="formData[param.name]" <el-input v-if="param.type === 'string'" v-model="tabStorage.formData[param.name]"
:placeholder="param.placeholder || `请输入${param.name}`" :placeholder="param.placeholder || `请输入${param.name}`" @keydown.enter.prevent="handleSubmit" />
@keydown.enter.prevent="handleSubmit" />
<el-input-number v-else-if="param.type === 'number'" v-model="formData[param.name]" <el-input-number v-else-if="param.type === 'number'" v-model="tabStorage.formData[param.name]"
:placeholder="param.placeholder || `请输入${param.name}`" :placeholder="param.placeholder || `请输入${param.name}`" @keydown.enter.prevent="handleSubmit" />
@keydown.enter.prevent="handleSubmit" />
<el-switch v-else-if="param.type === 'boolean'" v-model="formData[param.name]" /> <el-switch v-else-if="param.type === 'boolean'" v-model="tabStorage.formData[param.name]" />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@ -31,13 +29,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, defineProps, watch, ref, computed } from 'vue'; import { defineComponent, defineProps, watch, ref, computed, reactive, defineEmits } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { parseResourceTemplate, resourcesManager, ResourceStorage } from './resources'; import { parseResourceTemplate, resourcesManager, ResourceStorage } from './resources';
import { CasualRestAPI, ResourcesReadResponse } from '@/hook/type'; import { CasualRestAPI, ResourcesReadResponse } from '@/hook/type';
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
defineComponent({ name: 'resource-reader' }); defineComponent({ name: 'resource-reader' });
@ -47,15 +46,35 @@ const props = defineProps({
tabId: { tabId: {
type: Number, type: Number,
required: true required: true
},
currentResourceName: {
type: String,
required: false
} }
}); });
const emits = defineEmits(['resource-get-response']);
let tabStorage: ResourceStorage;
if (props.tabId >= 0) {
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ResourceStorage; tabStorage = tab.storage as ResourceStorage;
} else {
tabStorage = reactive({
currentType: 'resource',
currentResourceName: props.currentResourceName || '',
formData: {},
lastResourceReadResponse: undefined
});
}
if (!tabStorage.formData) {
tabStorage.formData = {};
}
// //
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const formData = ref<Record<string, any>>({});
const loading = ref(false); const loading = ref(false);
const responseData = ref<ResourcesReadResponse>(); const responseData = ref<ResourcesReadResponse>();
@ -93,13 +112,19 @@ const formRules = computed<FormRules>(() => {
return rules; return rules;
}); });
// //
const initFormData = () => { const initFormData = () => {
formData.value = {} if (!currentResource.value?.params) return;
currentResource.value?.params.forEach(param => {
formData.value[param.name] = param.type === 'number' ? 0 : const newSchemaDataForm: Record<string, number | boolean | string> = {};
param.type === 'boolean' ? false : ''
currentResource.value.params.forEach(param => {
newSchemaDataForm[param.name] = getDefaultValue(param);
const originType = normaliseJavascriptType(typeof tabStorage.formData[param.name]);
if (tabStorage.formData[param.name] !== undefined && originType === param.type) {
newSchemaDataForm[param.name] = tabStorage.formData[param.name];
}
}) })
} }
@ -109,28 +134,38 @@ const resetForm = () => {
responseData.value = undefined; responseData.value = undefined;
} }
// function getUri() {
function handleSubmit() { if (tabStorage.currentType === 'template') {
const fillFn = currentResource.value.fill; const fillFn = currentResource.value.fill;
const uri = fillFn(formData.value); const uri = fillFn(tabStorage.formData);
return uri;
const bridge = useMessageBridge();
bridge.addCommandListener('resources/read', (data: CasualRestAPI<ResourcesReadResponse>) => {
tabStorage.lastResourceReadResponse = data.msg;
}, { once: true });
bridge.postMessage({
command: 'resources/read',
data: { resourceUri: uri }
});
} }
// const currentResourceName = props.tabId >= 0 ? tabStorage.currentResourceName : props.currentResourceName;
const targetResource = resourcesManager.resources.find(resources => resources.name === currentResourceName);
return targetResource?.uri;
}
//
async function handleSubmit() {
const uri = getUri();
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest('resources/read', { resourceUri: uri });
tabStorage.lastResourceReadResponse = msg;
emits('resource-get-response', msg);
}
if (props.tabId >= 0) {
watch(() => tabStorage.currentResourceName, () => { watch(() => tabStorage.currentResourceName, () => {
initFormData(); initFormData();
resetForm(); resetForm();
}, { immediate: true }); }, { immediate: true });
}
</script> </script>

View File

@ -1,10 +1,18 @@
<template> <template>
<h3 class="resource-template">
<code>resources/templates/list</code>
<span
class="iconfont icon-restart"
@click="reloadResources({ first: false })"
></span>
</h3>
<div class="resource-template-container-scrollbar"> <div class="resource-template-container-scrollbar">
<el-scrollbar height="500px"> <el-scrollbar height="500px" v-if="resourcesManager.templates.length > 0">
<div class="resource-template-container"> <div class="resource-template-container">
<div <div
class="item" class="item"
:class="{ 'active': tabStorage.currentResourceName === template.name }" :class="{ 'active': props.tabId >= 0 && tabStorage.currentType === 'template' && tabStorage.currentResourceName === template.name }"
v-for="template of resourcesManager.templates" v-for="template of resourcesManager.templates"
:key="template.name" :key="template.name"
@click="handleClick(template)" @click="handleClick(template)"
@ -14,21 +22,16 @@
</div> </div>
</div> </div>
</el-scrollbar> </el-scrollbar>
<div v-else style="padding: 10px;">
empty
</div> </div>
<div class="resource-template-function-container">
<el-button
type="primary"
@click="reloadResources({ first: false })"
>
{{ t('refresh') }}
</el-button>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type'; import { CasualRestAPI, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps } from 'vue'; import { onMounted, onUnmounted, defineProps, ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { resourcesManager, ResourceStorage } from './resources'; import { resourcesManager, ResourceStorage } from './resources';
import { tabs } from '../panel'; import { tabs } from '../panel';
@ -44,8 +47,19 @@ const props = defineProps({
} }
}); });
let tabStorage: ResourceStorage;
if (props.tabId >= 0) {
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ResourceStorage; tabStorage = tab.storage as ResourceStorage;
} else {
tabStorage = reactive({
currentType:'template',
currentResourceName: '',
formData: {},
lastResourceReadResponse: undefined
});
}
function reloadResources(option: { first: boolean }) { function reloadResources(option: { first: boolean }) {
bridge.postMessage({ bridge.postMessage({
@ -63,8 +77,8 @@ function reloadResources(option: { first: boolean }) {
} }
function handleClick(template: ResourceTemplate) { function handleClick(template: ResourceTemplate) {
tabStorage.currentType = 'template';
tabStorage.currentResourceName = template.name; tabStorage.currentResourceName = template.name;
// TODO:
tabStorage.lastResourceReadResponse = undefined; tabStorage.lastResourceReadResponse = undefined;
} }
@ -74,11 +88,13 @@ onMounted(() => {
commandCancel = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => { commandCancel = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => {
resourcesManager.templates = data.msg.resourceTemplates || []; resourcesManager.templates = data.msg.resourceTemplates || [];
if (resourcesManager.templates.length > 0) { if (tabStorage.currentType === 'template') {
tabStorage.currentResourceName = resourcesManager.templates[0].name; const targetResource = resourcesManager.templates.find(template => template.name === tabStorage.currentResourceName);
// TODO: if (targetResource === undefined) {
tabStorage.currentResourceName = resourcesManager.templates[0]?.name;
tabStorage.lastResourceReadResponse = undefined; tabStorage.lastResourceReadResponse = undefined;
} }
}
}, { once: false }); }, { once: false });
reloadResources({ first: true }); reloadResources({ first: true });
@ -89,10 +105,23 @@ onUnmounted(() => {
commandCancel(); commandCancel();
} }
}) })
</script> </script>
<style> <style>
h3.resource-template {
display: flex;
align-items: center;
}
h3.resource-template .iconfont.icon-restart {
margin-left: 10px;
cursor: pointer;
}
h3.resource-template .iconfont.icon-restart:hover {
color: var(--main-color);
}
.resource-template-container-scrollbar { .resource-template-container-scrollbar {
background-color: var(--background); background-color: var(--background);
margin-bottom: 10px; margin-bottom: 10px;

View File

@ -0,0 +1,188 @@
<template>
<h3 class="resource-template">
<code>resources/list</code>
<span
class="iconfont icon-restart"
@click="reloadResources({ first: false })"
></span>
</h3>
<div class="resource-template-container-scrollbar">
<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 resourcesManager.resources"
:key="resource.uri"
@click="handleClick(resource)"
>
<span>{{ resource.name }}</span>
<span>{{ resource.mimeType }}</span>
</div>
</div>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { useMessageBridge } from '@/api/message-bridge';
import { CasualRestAPI, Resources, ResourcesListResponse } from '@/hook/type';
import { onMounted, onUnmounted, defineProps, defineEmits, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { resourcesManager, ResourceStorage } from './resources';
import { tabs } from '../panel';
import { ElMessage } from 'element-plus';
const bridge = useMessageBridge();
const { t } = useI18n();
const props = defineProps({
tabId: {
type: Number,
required: true
}
});
const emits = defineEmits([ 'resource-selected' ]);
let tabStorage: ResourceStorage;
if (props.tabId >= 0) {
const tab = tabs.content[props.tabId];
tabStorage = tab.storage as ResourceStorage;
} else {
tabStorage = reactive({
currentType:'resource',
currentResourceName: '',
formData: {},
lastResourceReadResponse: undefined
});
}
function reloadResources(option: { first: boolean }) {
bridge.postMessage({
command: 'resources/list'
});
if (!option.first) {
ElMessage({
message: t('finish-refresh'),
type: 'success',
duration: 3000,
showClose: true,
});
}
}
function handleClick(resource: Resources) {
tabStorage.currentType = 'resource';
tabStorage.currentResourceName = resource.name;
tabStorage.lastResourceReadResponse = undefined;
emits('resource-selected', resource);
}
let commandCancel: (() => void);
onMounted(() => {
commandCancel = bridge.addCommandListener('resources/list', (data: CasualRestAPI<ResourcesListResponse>) => {
resourcesManager.resources = data.msg.resources || [];
if (tabStorage.currentType === 'resource') {
const targetResource = resourcesManager.resources.find(resources => resources.name === tabStorage.currentResourceName);
if (targetResource === undefined) {
tabStorage.currentResourceName = resourcesManager.templates[0]?.name;
tabStorage.lastResourceReadResponse = undefined;
}
}
}, { once: false });
reloadResources({ first: true });
});
onUnmounted(() => {
if (commandCancel){
commandCancel();
}
})
</script>
<style>
h3.resource-template {
display: flex;
align-items: center;
}
h3.resource-template .iconfont.icon-restart {
margin-left: 10px;
cursor: pointer;
}
h3.resource-template .iconfont.icon-restart:hover {
color: var(--main-color);
}
.resource-template-container-scrollbar {
background-color: var(--background);
margin-bottom: 10px;
border-radius: .5em;
}
.resource-template-container {
height: fit-content;
display: flex;
flex-direction: column;
padding: 10px;
}
.resource-template-function-container {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.resource-template-function-container button {
width: 175px;
}
.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.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-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>

View File

@ -13,15 +13,28 @@
/> />
</span> </span>
</span> </span>
<el-scrollbar height="350px"> <el-scrollbar>
<div <div
class="output-content" class="output-content"
contenteditable="false" contenteditable="false"
> >
<template v-if="!showRawJson"> <template v-if="!showRawJson">
<span v-for="(content, index) of tabStorage.lastResourceReadResponse?.contents || []" :key="index"> <span v-for="(content, index) of tabStorage.lastResourceReadResponse?.contents || []" :key="index">
<span v-if="content.mimeType === 'image/png'">
<img
class="resource-list-image"
:src="getImageBlobUrlByBase64(content.blob || '', content.mimeType)"
:alt="content.text"
style="max-width: 100%; max-height: 300px;"
/>
</span>
<span v-if="content.mimeType === 'image/jpeg'">
</span>
<span v-else>
{{ content.text }} {{ content.text }}
</span> </span>
</span>
</template> </template>
<template v-else> <template v-else>
{{ formattedJson }} {{ formattedJson }}
@ -36,6 +49,7 @@ import { defineComponent, defineProps, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ResourceStorage } from './resources'; import { ResourceStorage } from './resources';
import { getImageBlobUrlByBase64 } from '@/hook/util';
defineComponent({ name: 'resource-logger' }); defineComponent({ name: 'resource-logger' });
const { t } = useI18n(); const { t } = useI18n();
@ -90,7 +104,7 @@ const formattedJson = computed(() => {
.resource-logger .output-content { .resource-logger .output-content {
border-radius: .5em; border-radius: .5em;
padding: 15px; padding: 15px;
min-height: 300px; min-height: 600px;
height: fit-content; height: fit-content;
font-family: var(--code-font-family); font-family: var(--code-font-family);
white-space: pre-wrap; white-space: pre-wrap;
@ -101,4 +115,8 @@ const formattedJson = computed(() => {
line-height: 1.5; line-height: 1.5;
background-color: var(--sidebar); background-color: var(--sidebar);
} }
.resource-list-image {
cursor: unset;
}
</style> </style>

View File

@ -1,18 +1,22 @@
import { ResourcesReadResponse, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type'; import { ResourcesReadResponse, ResourceTemplate, Resources } from '@/hook/type';
import { reactive } from 'vue'; import { reactive } from 'vue';
export const resourcesManager = reactive<{ export const resourcesManager = reactive<{
current: ResourceTemplate | undefined current: ResourceTemplate | undefined
templates: ResourceTemplate[] templates: ResourceTemplate[],
resources: Resources[]
}>({ }>({
current: undefined, current: undefined,
templates: [] templates: [],
resources: []
}); });
export interface ResourceStorage { export interface ResourceStorage {
currentType: 'resource' | 'template';
currentResourceName: string; currentResourceName: string;
lastResourceReadResponse?: ResourcesReadResponse; lastResourceReadResponse?: ResourcesReadResponse;
formData: Record<string, any>;
} }
/** /**
@ -22,7 +26,7 @@ export interface ResourceStorage {
*/ */
export function parseResourceTemplate(template: string): { export function parseResourceTemplate(template: string): {
params: string[], params: string[],
fill: (params: Record<string, string>) => string fill: (params: Record<string, number | boolean | string>) => string
} { } {
// 1. 提取所有参数名 // 1. 提取所有参数名
const paramRegex = /\{([^}]+)\}/g; const paramRegex = /\{([^}]+)\}/g;
@ -36,7 +40,7 @@ export function parseResourceTemplate(template: string): {
const paramList = Array.from(params); const paramList = Array.from(params);
// 2. 创建填充函数 // 2. 创建填充函数
const fill = (values: Record<string, string>): string => { const fill = (values: Record<string, number | boolean | string>): string => {
let result = template; let result = template;
// 验证所有必填参数 // 验证所有必填参数
@ -48,7 +52,7 @@ export function parseResourceTemplate(template: string): {
// 替换所有参数 // 替换所有参数
for (const param of paramList) { for (const param of paramList) {
result = result.replace(new RegExp(`\\{${param}\\}`, 'g'), values[param]); result = result.replace(new RegExp(`\\{${param}\\}`, 'g'), values[param].toString());
} }
return result; return result;

View File

@ -1,27 +1,22 @@
<template> <template>
<el-scrollbar height="100%">
<div class="tool-module"> <div class="tool-module">
<div class="left"> <div class="left">
<h2> <h2>
<span class="iconfont icon-tool"></span> <span class="iconfont icon-tool"></span>
工具模块 工具模块
</h2> </h2>
<h3><code>tools/list</code></h3> <ToolList :tab-id="props.tabId"></ToolList>
<ToolList
:tab-id="props.tabId"
></ToolList>
</div> </div>
<div class="right"> <div class="right">
<ToolExecutor <ToolExecutor :tab-id="props.tabId"></ToolExecutor>
:tab-id="props.tabId"
></ToolExecutor>
<ToolLogger <ToolLogger :tab-id="props.tabId"></ToolLogger>
:tab-id="props.tabId"
></ToolLogger>
</div> </div>
</div> </div>
</el-scrollbar>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@ -3,9 +3,8 @@
<h3>{{ currentTool?.name }}</h3> <h3>{{ currentTool?.name }}</h3>
</div> </div>
<div class="tool-executor-container"> <div class="tool-executor-container">
<el-form :model="formData" :rules="formRules" ref="formRef" label-position="top"> <el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
<template v-if="currentTool?.inputSchema?.properties"> <template v-if="currentTool?.inputSchema?.properties">
<el-scrollbar height="150px">
<el-form-item <el-form-item
v-for="[name, property] in Object.entries(currentTool.inputSchema.properties)" v-for="[name, property] in Object.entries(currentTool.inputSchema.properties)"
:key="name" :key="name"
@ -15,25 +14,33 @@
> >
<el-input <el-input
v-if="property.type === 'string'" v-if="property.type === 'string'"
v-model="formData[name]" v-model="tabStorage.formData[name]"
:placeholder="t('enter') + ' ' + (property.title || name)" type="text"
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
@keydown.enter.prevent="handleExecute" @keydown.enter.prevent="handleExecute"
/> />
<el-input-number <el-input-number
v-else-if="property.type === 'number' || property.type === 'integer'" v-else-if="property.type === 'number' || property.type === 'integer'"
v-model="formData[name]" v-model="tabStorage.formData[name]"
controls-position="right" controls-position="right"
:placeholder="t('enter') + ' ' + (property.title || name)" :placeholder="property.description || t('enter') + ' ' + (property.title || name)"
@keydown.enter.prevent="handleExecute" @keydown.enter.prevent="handleExecute"
/> />
<el-switch <el-switch
v-else-if="property.type === 'boolean'" v-else-if="property.type === 'boolean'"
v-model="formData[name]" active-text="true"
inactive-text="false"
v-model="tabStorage.formData[name]"
/>
<k-input-object
v-else-if="property.type === 'object'"
v-model="tabStorage.formData[name]"
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
/> />
</el-form-item> </el-form-item>
</el-scrollbar>
</template> </template>
<el-form-item> <el-form-item>
@ -54,8 +61,9 @@ import { useI18n } from 'vue-i18n';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { callTool, toolsManager, ToolStorage } from './tools'; import { callTool, toolsManager, ToolStorage } from './tools';
import { CasualRestAPI, ToolCallResponse } from '@/hook/type'; import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
import { useMessageBridge } from '@/api/message-bridge';
import KInputObject from '@/components/k-input-object/index.vue';
defineComponent({ name: 'tool-executor' }); defineComponent({ name: 'tool-executor' });
@ -71,14 +79,21 @@ const props = defineProps({
const tab = tabs.content[props.tabId]; const tab = tabs.content[props.tabId];
const tabStorage = tab.storage as ToolStorage; const tabStorage = tab.storage as ToolStorage;
if (!tabStorage.formData) {
tabStorage.formData = {};
}
console.log(tabStorage.formData);
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const formData = ref<Record<string, any>>({});
const loading = ref(false); const loading = ref(false);
const currentTool = computed(() => { const currentTool = computed(() => {
return toolsManager.tools.find(tool => tool.name === tabStorage.currentToolName); return toolsManager.tools.find(tool => tool.name === tabStorage.currentToolName);
}); });
const formRules = computed<FormRules>(() => { const formRules = computed<FormRules>(() => {
const rules: FormRules = {}; const rules: FormRules = {};
if (!currentTool.value?.inputSchema?.properties) return rules; if (!currentTool.value?.inputSchema?.properties) return rules;
@ -98,26 +113,44 @@ const formRules = computed<FormRules>(() => {
return rules; return rules;
}); });
const initFormData = () => { const initFormData = () => {
formData.value = {}; // inputSchema
// 1. key value schema
// 2. key value
if (!currentTool.value?.inputSchema?.properties) return; if (!currentTool.value?.inputSchema?.properties) return;
const newSchemaDataForm: Record<string, number | boolean | string | object> = {};
console.log(currentTool.value.inputSchema.properties);
Object.entries(currentTool.value.inputSchema.properties).forEach(([name, property]) => { Object.entries(currentTool.value.inputSchema.properties).forEach(([name, property]) => {
formData.value[name] = (property.type === 'number' || property.type === 'integer') ? 0 : newSchemaDataForm[name] = getDefaultValue(property);
property.type === 'boolean' ? false : ''; const originType = normaliseJavascriptType(typeof tabStorage.formData[name]);
if (tabStorage.formData[name] !== undefined && originType === property.type) {
newSchemaDataForm[name] = tabStorage.formData[name];
}
}); });
tabStorage.formData = newSchemaDataForm;
}; };
const resetForm = () => { const resetForm = () => {
formRef.value?.resetFields(); formRef.value?.resetFields();
tabStorage.lastToolCallResponse = undefined;
}; };
async function handleExecute() { async function handleExecute() {
if (!currentTool.value) return; if (!currentTool.value) return;
loading.value = true;
const toolResponse = await callTool(tabStorage.currentToolName, formData.value); try {
tabStorage.lastToolCallResponse = undefined;
const toolResponse = await callTool(tabStorage.currentToolName, tabStorage.formData);
tabStorage.lastToolCallResponse = toolResponse; tabStorage.lastToolCallResponse = toolResponse;
} finally {
loading.value = false;
}
} }
watch(() => tabStorage.currentToolName, () => { watch(() => tabStorage.currentToolName, () => {
@ -133,4 +166,18 @@ watch(() => tabStorage.currentToolName, () => {
border-radius: .5em; border-radius: .5em;
margin-bottom: 15px; margin-bottom: 15px;
} }
.tool-executor-container .el-switch .el-switch__action {
background-color: var(--main-color);
}
.tool-executor-container .el-switch.is-checked .el-switch__action {
background-color: var(--sidebar);
}
.tool-executor-container .el-switch__core {
border: 1px solid var(--main-color) !important;
}
</style> </style>

View File

@ -1,4 +1,12 @@
<template> <template>
<h3 class="resource-template">
<code>tools/list</code>
<span
class="iconfont icon-restart"
@click="reloadTools({ first: false })"
></span>
</h3>
<div class="tool-list-container-scrollbar"> <div class="tool-list-container-scrollbar">
<el-scrollbar height="500px"> <el-scrollbar height="500px">
<div class="tool-list-container"> <div class="tool-list-container">
@ -15,13 +23,9 @@
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
<div class="tool-list-function-container">
<el-button <div>
type="primary" <!-- resources/list -->
@click="reloadTools({ first: false })"
>
{{ t('refresh') }}
</el-button>
</div> </div>
</template> </template>
@ -73,7 +77,11 @@ onMounted(() => {
commandCancel = bridge.addCommandListener('tools/list', (data: CasualRestAPI<ToolsListResponse>) => { commandCancel = bridge.addCommandListener('tools/list', (data: CasualRestAPI<ToolsListResponse>) => {
toolsManager.tools = data.msg.tools || []; toolsManager.tools = data.msg.tools || [];
if (toolsManager.tools.length > 0) { const targetTool = toolsManager.tools.find((tool) => {
return tool.name === tabStorage.currentToolName;
});
if (targetTool === undefined) {
tabStorage.currentToolName = toolsManager.tools[0].name; tabStorage.currentToolName = toolsManager.tools[0].name;
tabStorage.lastToolCallResponse = undefined; tabStorage.lastToolCallResponse = undefined;
} }

View File

@ -3,35 +3,34 @@
<span> <span>
<span>{{ t('response') }}</span> <span>{{ t('response') }}</span>
<span style="width: 200px;"> <span style="width: 200px;">
<el-switch <el-switch v-model="showRawJson" inline-prompt active-text="JSON" inactive-text="Text"
v-model="showRawJson"
inline-prompt
active-text="JSON"
inactive-text="Text"
style="margin-left: 10px; width: 200px;" style="margin-left: 10px; width: 200px;"
:inactive-action-style="'backgroundColor: var(--sidebar)'" :inactive-action-style="'backgroundColor: var(--sidebar)'" />
/>
</span> </span>
</span> </span>
<el-scrollbar height="300px"> <el-scrollbar height="500px">
<div <div class="output-content" contenteditable="false">
class="output-content"
contenteditable="false" <!-- TODO: 更加稳定现在通过下面这个来判断上一次执行结果是否成功 -->
> <div v-if="typeof tabStorage.lastToolCallResponse === 'string'" class="error-tool-call">
<span>
{{ tabStorage.lastToolCallResponse }}
</span>
</div>
<div v-else>
<!-- 展示原本的信息 -->
<template v-if="!showRawJson"> <template v-if="!showRawJson">
<template v-if="tabStorage.lastToolCallResponse?.isError">
<span style="color: var(--el-color-error)">
{{ tabStorage.lastToolCallResponse.content.map(c => c.text).join('\n') }}
</span>
</template>
<template v-else>
{{tabStorage.lastToolCallResponse?.content.map(c => c.text).join('\n')}} {{tabStorage.lastToolCallResponse?.content.map(c => c.text).join('\n')}}
</template> </template>
</template>
<!-- 展示 json -->
<template v-else> <template v-else>
{{ formattedJson }} {{ formattedJson }}
</template> </template>
</div> </div>
</div>
</el-scrollbar> </el-scrollbar>
</div> </div>
</template> </template>
@ -41,6 +40,7 @@ import { defineComponent, defineProps, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ToolStorage } from './tools'; import { ToolStorage } from './tools';
import { useMessageBridge } from '@/api/message-bridge';
defineComponent({ name: 'tool-logger' }); defineComponent({ name: 'tool-logger' });
const { t } = useI18n(); const { t } = useI18n();
@ -59,11 +59,15 @@ const showRawJson = ref(false);
const formattedJson = computed(() => { const formattedJson = computed(() => {
try { try {
if (typeof tabStorage.lastToolCallResponse === 'string') {
return tabStorage.lastToolCallResponse;
}
return JSON.stringify(tabStorage.lastToolCallResponse, null, 2); return JSON.stringify(tabStorage.lastToolCallResponse, null, 2);
} catch { } catch {
return 'Invalid JSON'; return 'Invalid JSON';
} }
}); });
</script> </script>
<style> <style>
@ -95,7 +99,7 @@ const formattedJson = computed(() => {
.tool-logger .output-content { .tool-logger .output-content {
border-radius: .5em; border-radius: .5em;
padding: 15px; padding: 15px;
min-height: 300px; min-height: 450px;
height: fit-content; height: fit-content;
font-family: var(--code-font-family); font-family: var(--code-font-family);
white-space: pre-wrap; white-space: pre-wrap;
@ -106,4 +110,10 @@ const formattedJson = computed(() => {
line-height: 1.5; line-height: 1.5;
background-color: var(--sidebar); background-color: var(--sidebar);
} }
.error-tool-call {
background-color: rgba(245, 108, 108, 0.5);
padding: 5px 9px;
border-radius: .5em;
}
</style> </style>

View File

@ -1,4 +1,5 @@
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { mcpSetting } from '@/hook/mcp';
import { ToolsListResponse, ToolCallResponse, CasualRestAPI } from '@/hook/type'; import { ToolsListResponse, ToolCallResponse, CasualRestAPI } from '@/hook/type';
import { pinkLog } from '@/views/setting/util'; import { pinkLog } from '@/views/setting/util';
import { reactive } from 'vue'; import { reactive } from 'vue';
@ -11,7 +12,8 @@ export const toolsManager = reactive<{
export interface ToolStorage { export interface ToolStorage {
currentToolName: string; currentToolName: string;
lastToolCallResponse?: ToolCallResponse; lastToolCallResponse?: ToolCallResponse | string;
formData: Record<string, any>;
} }
const bridge = useMessageBridge(); const bridge = useMessageBridge();
@ -22,7 +24,7 @@ export function callTool(toolName: string, toolArgs: Record<string, any>) {
console.log(data.msg); console.log(data.msg);
if (data.code !== 200) { if (data.code !== 200) {
reject(new Error(data.msg + '')); resolve(data.msg);
} else { } else {
resolve(data.msg); resolve(data.msg);
} }
@ -35,7 +37,10 @@ export function callTool(toolName: string, toolArgs: Record<string, any>) {
command: 'tools/call', command: 'tools/call',
data: { data: {
toolName, toolName,
toolArgs: JSON.parse(JSON.stringify(toolArgs)) toolArgs: JSON.parse(JSON.stringify(toolArgs)),
callToolOption: {
timeout: mcpSetting.timeout * 1000
}
} }
}); });
}); });

View File

@ -1,6 +1,8 @@
<template> <template>
<div class="connected-status-container" <div class="connected-status-container"
id="connected-status-container"
@click.stop="toggleConnectionPanel()" @click.stop="toggleConnectionPanel()"
:class="{ 'connected': connectionResult.success }"
> >
<span class="mcp-server-info"> <span class="mcp-server-info">
<el-tooltip <el-tooltip
@ -13,12 +15,14 @@
</el-tooltip> </el-tooltip>
</span> </span>
<span class="connect-status"> <span class="connect-status">
<span <span v-if="connectionResult.success">
class="status-circle" <span class="iconfont icon-connect"></span>
:class="statusColorStyle" <span class="iconfont icon-dui"></span>
> </span>
<span v-else>
<span class="iconfont icon-connect"></span>
<span class="iconfont icon-cuo"></span>
</span> </span>
<span class="status-string">{{ statusString }}</span>
</span> </span>
</div> </div>
@ -34,32 +38,34 @@ defineComponent({ name: 'connected' });
const { t } = useI18n(); const { t } = useI18n();
const statusString = computed(() => {
if (connectionResult.success) {
return t('connected');
} else {
return t('disconnected');
}
});
const statusColorStyle = computed(() => {
if (connectionResult.success) {
return 'connected-color';
} else {
return 'disconnected-color';
}
});
const fullDisplayServerName = computed(() => { const fullDisplayServerName = computed(() => {
return connectionResult.serverInfo.name + '/' + connectionResult.serverInfo.version; return connectionResult.serverInfo.name + '/' + connectionResult.serverInfo.version;
}); });
const displayServerName = computed(() => { const displayServerName = computed(() => {
if (connectionResult.serverInfo.name.length > 20) { const name = connectionResult.serverInfo.name;
return connectionResult.serverInfo.name.substring(0, 20); if (name.length <= 3) return name;
} else {
return connectionResult.serverInfo.name; //
const chineseMatch = name.match(/[\u4e00-\u9fa5]/g);
if (chineseMatch && chineseMatch.length >= 2) {
return chineseMatch.slice(0, 3).join('');
} }
//
const words = name
.replace(/([a-z])([A-Z])/g, '$1 $2') //
.split(/[\s\-_]+/) // 线
.filter(word => word.length > 0);
if (words.length === 1 && words[0].length > 3) {
return words[0].substring(0, 3).toUpperCase();
}
return words
.map(word => word[0].toUpperCase())
.slice(0, 3)
.join('');
}); });
function toggleConnectionPanel() { function toggleConnectionPanel() {
@ -69,9 +75,13 @@ function toggleConnectionPanel() {
</script> </script>
<style> <style>
.connected .status-circle {
background-color: var(--el-color-success) !important;
}
.connected-color { .connected .connect-status {
background-color: #21DA49; border: 1px solid var(--el-color-success) !important;
color: var(--el-color-success) !important;
} }
.disconnected-color { .disconnected-color {
@ -81,8 +91,8 @@ function toggleConnectionPanel() {
.status-circle { .status-circle {
height: 12px; height: 12px;
width: 12px; width: 12px;
margin-right: 8px;
border-radius: 99%; border-radius: 99%;
background-color: var(--main-color);
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1); box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
} }
@ -104,7 +114,13 @@ function toggleConnectionPanel() {
.connected-status-container .connect-status { .connected-status-container .connect-status {
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: 20px; justify-content: space-between;
margin-top: 10px;
border-radius: .5em;
padding: 5px 10px;
width: 30px;
border: 1px solid var(--main-color);
color: var(--main-color);
} }
.connected-status-container:hover { .connected-status-container:hover {
@ -129,10 +145,15 @@ function toggleConnectionPanel() {
.mcp-server-info .name { .mcp-server-info .name {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
max-width: 60px; width: 30px;
white-space: wrap; display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background-color: #f39a6d; background-color: #f39a6d;
padding: 5px; padding: 5px 12px;
border-radius: .5em; border-radius: .5em;
color: #1e1e1e; color: #1e1e1e;
} }

View File

@ -1,7 +1,11 @@
<template> <template>
<div class="mcp-title"> <div class="mcp-title">
<div class="openmcp-logo" style="width: 48px; height: 48px; margin-top: 10px; margin-bottom: 15px; margin-right: 10px;"></div> <div class="simple-logo"
<!-- <div>OpenMCP</div> --> @click="clickLogo"
>
<span class="iconfont icon-openmcp"></span>
<span style="font-size: 12px;">openmcp</span>
</div>
</div> </div>
</template> </template>
@ -9,15 +13,43 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
defineComponent({ name: 'mcp-title' }); defineComponent({ name: 'mcp-title' });
function clickLogo() {
window.open('https://kirigaya.cn/blog/article?seq=311', '_blank');
}
</script> </script>
<style> <style>
.mcp-title { .mcp-title {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 5px 10px;
} }
.mcp-title>div { .mcp-title>div {
font-size: 24px; font-size: 24px;
} }
.simple-logo {
height: 73px;
display: flex;
align-items: center;
flex-direction: column;
user-select: none;
-webkit-user-drag: none;
transition: var(--animation-3s);
cursor: pointer;
}
.simple-logo:hover {
color: var(--main-color);
}
.simple-logo .iconfont {
font-size: 38px;
}
</style> </style>

View File

@ -1,6 +1,8 @@
<template> <template>
<div class="sidebar-item-container"> <div class="sidebar-item-container">
<div v-for="(item, index) of sidebarItems" :key="index"> <div v-for="(item, index) of sidebarItems" :key="index"
:id="`sidebar-${item.ident}`"
>
<el-tooltip :content="t(item.ident)" placement="right"> <el-tooltip :content="t(item.ident)" placement="right">
<div class="sidebar-option-item" :class="{ 'active': isActive(item.ident) }" <div class="sidebar-option-item" :class="{ 'active': isActive(item.ident) }"
@click="gotoOption(item.ident)"> @click="gotoOption(item.ident)">

View File

@ -15,7 +15,6 @@ export function setDefaultCss() {
document.body.style.setProperty('--el-border-color-light', 'var(--sidebar)'); document.body.style.setProperty('--el-border-color-light', 'var(--sidebar)');
document.body.style.setProperty('--el-border-color-lighter', 'var(--sidebar)'); document.body.style.setProperty('--el-border-color-lighter', 'var(--sidebar)');
document.body.style.setProperty('--el-bg-color-overlay', 'var(--sidebar)'); document.body.style.setProperty('--el-bg-color-overlay', 'var(--sidebar)');
document.body.style.setProperty('--el-color-info-light-9', 'var(--main-color)');
document.body.style.setProperty('--el-color-info', 'var(--foreground)'); document.body.style.setProperty('--el-color-info', 'var(--foreground)');
document.body.style.setProperty('--el-color-info-light-8', 'var(--main-color)'); document.body.style.setProperty('--el-color-info-light-8', 'var(--main-color)');
document.body.style.setProperty('--el-fill-color-light', 'var(--sidebar-item-selected)'); document.body.style.setProperty('--el-fill-color-light', 'var(--sidebar-item-selected)');
@ -25,6 +24,7 @@ export function setDefaultCss() {
document.body.style.setProperty('--el-color-primary-light-5', 'var(--button-disabled)'); document.body.style.setProperty('--el-color-primary-light-5', 'var(--button-disabled)');
document.body.style.setProperty('--el-bg-color', 'var(--background)'); document.body.style.setProperty('--el-bg-color', 'var(--background)');
document.body.style.setProperty('--el-text-color-primary', 'var(--foreground)'); document.body.style.setProperty('--el-text-color-primary', 'var(--foreground)');
document.body.style.setProperty('--el-button-hover-text-color', 'var(--background)');
// document.body.style.setProperty('--el-color-white', 'var(--background)'); // document.body.style.setProperty('--el-color-white', 'var(--background)');

36
renderer/src/hook/mcp.ts Normal file
View File

@ -0,0 +1,36 @@
import { reactive } from "vue";
interface TypeAble {
type: string;
}
export function getDefaultValue(property: TypeAble): any {
if (property.type === 'number' || property.type === 'integer') {
return 0;
} else if (property.type === 'boolean') {
return false;
} else if (property.type === 'object') {
return {};
} else {
return '';
}
}
export function normaliseJavascriptType(type: string) {
switch (type) {
case 'integer':
return 'number';
case 'number':
return 'number';
case 'boolean':
return 'boolean';
case 'string':
return 'string';
default:
return 'string';
}
}
export const mcpSetting = reactive({
timeout: 60,
});

View File

@ -1,9 +1,8 @@
import { useMessageBridge } from "@/api/message-bridge"; import { useMessageBridge } from "@/api/message-bridge";
import { llmManager, llms } from "@/views/setting/llm";
import { pinkLog } from "@/views/setting/util"; import { pinkLog } from "@/views/setting/util";
import I18n from '@/i18n/index';
import { debugModes, tabs } from "@/components/main-panel/panel"; import { debugModes, tabs } from "@/components/main-panel/panel";
import { markRaw, ref } from "vue"; import { markRaw, ref, nextTick } from "vue";
import { v4 as uuidv4 } from 'uuid';
interface SaveTabItem { interface SaveTabItem {
name: string; name: string;
@ -21,6 +20,8 @@ interface SaveTab {
export const panelLoaded = ref(false); export const panelLoaded = ref(false);
export function loadPanels() { export function loadPanels() {
return new Promise((resolve, reject) => {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
bridge.addCommandListener('panel/load', data => { bridge.addCommandListener('panel/load', data => {
@ -35,6 +36,8 @@ export function loadPanels() {
if (persistTab.tabs.length === 0) { if (persistTab.tabs.length === 0) {
// 空的,直接返回不需要管 // 空的,直接返回不需要管
panelLoaded.value = true;
resolve(void 0);
return; return;
} }
@ -42,12 +45,16 @@ export function loadPanels() {
tabs.content = []; tabs.content = [];
for (const tab of persistTab.tabs || []) { for (const tab of persistTab.tabs || []) {
const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined;
tabs.content.push({ tabs.content.push({
id: uuidv4(),
name: tab.name, name: tab.name,
icon: tab.icon, icon: tab.icon,
type: tab.type, type: tab.type,
componentIndex: tab.componentIndex, componentIndex: tab.componentIndex,
component: markRaw(debugModes[tab.componentIndex]), component: component,
storage: tab.storage storage: tab.storage
}); });
} }
@ -56,11 +63,13 @@ export function loadPanels() {
} }
panelLoaded.value = true; panelLoaded.value = true;
resolve(void 0);
}, { once: true }); }, { once: true });
bridge.postMessage({ bridge.postMessage({
command: 'panel/load' command: 'panel/load'
}); });
});
} }
let debounceHandler: NodeJS.Timeout; let debounceHandler: NodeJS.Timeout;
@ -69,10 +78,15 @@ export function safeSavePanels() {
clearTimeout(debounceHandler); clearTimeout(debounceHandler);
debounceHandler = setTimeout(() => { debounceHandler = setTimeout(() => {
savePanels(); savePanels();
}, 1000); }, 100);
} }
export function savePanels(saveHandler?: () => void) { export function savePanels(saveHandler?: () => void) {
// // 没有完成 panel 加载就不保存
// if (!panelLoaded.value) {
// return;
// }
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const saveTabs: SaveTab = { const saveTabs: SaveTab = {

Some files were not shown because too many files have changed in this diff Show More