Compare commits
98 Commits
a107dc04ff
...
0f947c5b09
Author | SHA1 | Date | |
---|---|---|---|
0f947c5b09 | |||
74c18dcd2b | |||
18db01af6b | |||
96d36c0706 | |||
![]() |
3ecacfc6e8 | ||
![]() |
5c0462751a | ||
cefa6b2af5 | |||
ea44620dee | |||
5a2a699a51 | |||
cf16cff7cf | |||
8d549d17e2 | |||
![]() |
c716cbc6ca | ||
![]() |
b056618e11 | ||
7aad1e6729 | |||
f835d90f39 | |||
8f8106a0a3 | |||
1c92b6fddd | |||
73a5b05a5d | |||
b7015d7532 | |||
e206b502b5 | |||
56145bfdf9 | |||
dca2a9c820 | |||
96d029f906 | |||
c1b06313d7 | |||
34936d944d | |||
354380cf23 | |||
0bea084c35 | |||
2b1c0c30dd | |||
11ff54e2f1 | |||
63ed5d7256 | |||
9c98a636f9 | |||
7d2afb053e | |||
9ca03ef0fe | |||
1c696fa585 | |||
07ceb7cb84 | |||
85c26a3cbf | |||
27e94efa26 | |||
a535690bc6 | |||
4f9900a64c | |||
dd0d6016fa | |||
31fa5ead4f | |||
8ef6ddd1ed | |||
a55759d92f | |||
45ba33119c | |||
46db790304 | |||
a328929296 | |||
beaf4f5ba1 | |||
cf3a3a57ce | |||
dddb786aa4 | |||
91cff239ab | |||
5fc7a9e468 | |||
ca64ce040f | |||
f925da7d7d | |||
ad857e6544 | |||
f14687b1d8 | |||
dd9c117df7 | |||
8887da8ba9 | |||
f484688a4b | |||
ddb4dfb565 | |||
![]() |
1e201db87a | ||
6c982f1800 | |||
![]() |
3b95a57bd9 | ||
4473421708 | |||
5bac8f8726 | |||
b2b80c1a3f | |||
31a25f27bc | |||
3b1afa70bd | |||
859d506ea9 | |||
3976670295 | |||
03d79d0222 | |||
fcf3b8cb9f | |||
493580ba3b | |||
799c37fc76 | |||
7414a905d4 | |||
299bb30df7 | |||
8105bfde85 | |||
36744f2a71 | |||
39919e7273 | |||
75191f1144 | |||
ce31ee739d | |||
471ad41c8e | |||
69b907901f | |||
5c9f46d2f1 | |||
3e0622f7e7 | |||
3ffcbdd9a8 | |||
698bbf0f84 | |||
b7a4ab8706 | |||
b1a083dcd4 | |||
96772ad422 | |||
75ed81b5ef | |||
b33eab402d | |||
3de7ef68ba | |||
4e8caf4c53 | |||
200a53fafc | |||
03eb664a1a | |||
a0968902f4 | |||
e306388f14 | |||
37b162155c |
15
.editorconfig
Normal 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
@ -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
@ -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: "例如: 相关截图/日志/错误信息"
|
54
.github/ISSUE_TEMPLATE/feature-request.yaml
vendored
Normal 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
@ -4,4 +4,13 @@ node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
.env
|
||||
resources
|
||||
openmcp-sdk
|
||||
.DS_Store
|
||||
.exe
|
||||
|
||||
.idea
|
||||
resources/ocr/*.js
|
||||
resources/ocr/*.wasm
|
||||
resources/renderer
|
||||
resources/service
|
||||
*.traineddata
|
@ -1,5 +0,0 @@
|
||||
import { defineConfig } from '@vscode/test-cli';
|
||||
|
||||
export default defineConfig({
|
||||
files: 'out/test/**/*.test.js',
|
||||
});
|
@ -17,5 +17,10 @@ service/**
|
||||
test/**
|
||||
servers/**
|
||||
scripts/**
|
||||
software/**
|
||||
*.sh
|
||||
*.ps1
|
||||
|
||||
.editorconfig
|
||||
.gitattributes
|
||||
*.vsix
|
41
CHANGELOG.md
@ -1,5 +1,46 @@
|
||||
# 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
|
||||
|
||||
- 完成 openmcp 的基础 inspector 功能
|
||||
|
149
README.md
@ -4,34 +4,120 @@
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
## OpenMCP
|
||||
|
||||
一款用于 MCP 服务端调试的一体化 vscode 插件。
|
||||
一款用于 MCP 服务端调试的一体化 vscode/trae/cursor 插件。
|
||||
|
||||

|
||||
<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 客户端基础功能,开发测试一体化。
|
||||
|
||||

|
||||
|
||||
进行资源协议、工具、Prompt 的 MCP 服务器测试。
|
||||
|
||||

|
||||
|
||||
测试完成的工具可以放入 「交互测试」 模块之间进行大模型交互测试。
|
||||
|
||||

|
||||
|
||||
完整的项目级管理面板,更加方便的进行项目和全局的 mcp 项目管理。
|
||||
|
||||

|
||||
|
||||
支持多种大模型
|
||||
|
||||

|
||||
|
||||
|
||||
## 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
|
||||
|
||||
@ -50,23 +136,22 @@ B <--mcp--> m(MCP Server)
|
||||
配置项目
|
||||
|
||||
```bash
|
||||
source configure.sh
|
||||
## linux
|
||||
./configure.sh
|
||||
## windows
|
||||
./configure.ps1
|
||||
```
|
||||
|
||||
启动 dev server
|
||||
|
||||
```bash
|
||||
cd renderer
|
||||
npm run serve
|
||||
```
|
||||
|
||||
启动 service
|
||||
|
||||
```bash
|
||||
cd service
|
||||
npm run serve
|
||||
## linux
|
||||
./dev.sh
|
||||
## windows
|
||||
./dev.ps1
|
||||
```
|
||||
|
||||
> 端口占用: 8080 (renderer) + 8081 (service)
|
||||
|
||||
### Extention Dev
|
||||
|
||||
@ -86,17 +171,3 @@ B <--mcp--> m(MCP Server)
|
||||
```
|
||||
|
||||
and just press f5, いただきます
|
||||
|
||||
## Flowchart
|
||||
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
A[用户输入问题] --> B[选择工具]
|
||||
B --> C[大模型处理]
|
||||
C --> D{是否有tool use?}
|
||||
D -- 否 --> E[返回 content]
|
||||
D -- 是 --> F[执行工具]
|
||||
F --> G[返回工具执行结果]
|
||||
G --> C
|
||||
```
|
@ -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 = @(
|
||||
Start-Job -ScriptBlock {
|
||||
Set-Location $using:PWD\renderer
|
||||
# 获取当前工作目录的绝对路径
|
||||
$currentDir = (Get-Location).Path
|
||||
|
||||
# 并行构建 renderer 和 service
|
||||
$rendererJob = Start-Job -ScriptBlock {
|
||||
param($workDir)
|
||||
Set-Location -Path "$workDir\renderer"
|
||||
npm run build
|
||||
Move-Item -Force -Path .\dist -Destination ..\resources\renderer
|
||||
}
|
||||
Start-Job -ScriptBlock {
|
||||
Set-Location $using:PWD\service
|
||||
Move-Item -Path "./dist" -Destination "$workDir\openmcp-sdk\renderer" -Force
|
||||
} -ArgumentList $currentDir
|
||||
|
||||
$serviceJob = Start-Job -ScriptBlock {
|
||||
param($workDir)
|
||||
Set-Location -Path "$workDir\service"
|
||||
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
|
||||
Receive-Job -Job $jobs
|
||||
Remove-Job -Job $jobs
|
||||
# 等待任务完成
|
||||
$rendererJob | Wait-Job | Receive-Job
|
||||
$serviceJob | Wait-Job | Receive-Job
|
||||
|
||||
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"
|
||||
|
@ -1,7 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
mkdir -p ./resources
|
||||
(cd ./renderer && npm run build && mv ./dist ../resources/renderer) &
|
||||
(cd ./service && npm run build && mv ./dist ../resources/service) &
|
||||
mkdir -p ./openmcp-sdk
|
||||
rm -rf ./openmcp-sdk/
|
||||
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
|
||||
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
@ -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
@ -1,3 +1,5 @@
|
||||
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 run prepare:ocr
|
7
dev.ps1
Normal 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
@ -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
@ -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
@ -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
After Width: | Height: | Size: 543 KiB |
BIN
icons/openmcp.management.png
Normal file
After Width: | Height: | Size: 496 KiB |
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 54 KiB |
BIN
icons/openmcp.resource.png
Normal file
After Width: | Height: | Size: 492 KiB |
BIN
icons/openmcp.support.llm.png
Normal file
After Width: | Height: | Size: 435 KiB |
BIN
icons/openmcp.welcome.png
Normal file
After Width: | Height: | Size: 402 KiB |
@ -1,22 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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>
|
||||
<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>
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 178 KiB |
505
package-lock.json
generated
@ -1,18 +1,21 @@
|
||||
{
|
||||
"name": "openmcp",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "openmcp",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.6",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@seald-io/nedb": "^4.1.1",
|
||||
"axios": "^1.7.7",
|
||||
"bson": "^6.8.0",
|
||||
"openai": "^4.93.0",
|
||||
"pako": "^2.1.0",
|
||||
"tesseract.js": "^6.0.1",
|
||||
"uuid": "^11.1.0",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -20,9 +23,10 @@
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/vscode": "^1.72.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.4.2",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack": "^5.99.5",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"engines": {
|
||||
@ -97,9 +101,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz",
|
||||
"integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==",
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz",
|
||||
"integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^1.0.5",
|
||||
@ -117,6 +121,22 @@
|
||||
"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": {
|
||||
"version": "9.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-9.6.1.tgz",
|
||||
@ -524,6 +544,21 @@
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"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": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||
@ -535,6 +570,12 @@
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
|
||||
@ -623,6 +664,24 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"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_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": {
|
||||
"version": "2.8.5",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@ -889,6 +989,17 @@
|
||||
"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": {
|
||||
"version": "5.18.1",
|
||||
"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": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||
@ -1349,6 +1475,19 @@
|
||||
"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": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
@ -1382,6 +1521,18 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
@ -1458,6 +1609,18 @@
|
||||
"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": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/import-local/-/import-local-3.2.0.tgz",
|
||||
@ -1501,6 +1664,34 @@
|
||||
"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": {
|
||||
"version": "2.16.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz",
|
||||
@ -1543,6 +1775,45 @@
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@ -1608,6 +1879,15 @@
|
||||
"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": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/loader-runner/-/loader-runner-4.3.0.tgz",
|
||||
@ -1617,6 +1897,15 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@ -1765,6 +2054,16 @@
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@ -1846,6 +2145,15 @@
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
|
||||
@ -1969,6 +2277,15 @@
|
||||
"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": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@ -2048,6 +2365,12 @@
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@ -2134,6 +2457,23 @@
|
||||
],
|
||||
"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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@ -2238,6 +2578,23 @@
|
||||
"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": {
|
||||
"version": "1.2.0",
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@ -2611,6 +3037,32 @@
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@ -2620,6 +3072,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wasm-feature-detect": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
|
||||
"integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.2.tgz",
|
||||
@ -2650,9 +3108,10 @@
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
@ -2796,6 +3255,27 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"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": {
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||
|
173
package.json
@ -2,7 +2,7 @@
|
||||
"name": "openmcp",
|
||||
"displayName": "OpenMCP",
|
||||
"description": "An all in one MCP Client/TestTool",
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.7",
|
||||
"publisher": "kirigaya",
|
||||
"author": {
|
||||
"name": "kirigaya",
|
||||
@ -28,9 +28,81 @@
|
||||
"title": "展示 OpenMCP",
|
||||
"category": "openmcp",
|
||||
"icon": {
|
||||
"light": "./icons/protocol.svg",
|
||||
"dark": "./icons/protocol.svg"
|
||||
"light": "./icons/light/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": {
|
||||
@ -40,6 +112,72 @@
|
||||
"group": "navigation",
|
||||
"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": {
|
||||
@ -54,10 +192,22 @@
|
||||
"views": {
|
||||
"openmcp-sidebar": [
|
||||
{
|
||||
"id": "webview-sidebar.view",
|
||||
"id": "openmcp.sidebar.workspace-connection",
|
||||
"icon": "./icons/protocol.svg",
|
||||
"name": "chatbot",
|
||||
"type": "webview"
|
||||
"name": "MCP 连接 (工作区)",
|
||||
"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 ./",
|
||||
"pretest": "npm run compile && npm run lint",
|
||||
"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": {
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@seald-io/nedb": "^4.1.1",
|
||||
"axios": "^1.7.7",
|
||||
"bson": "^6.8.0",
|
||||
"openai": "^4.93.0",
|
||||
"pako": "^2.1.0",
|
||||
"tesseract.js": "^6.0.1",
|
||||
"uuid": "^11.1.0",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -83,9 +237,10 @@
|
||||
"@types/pako": "^2.0.3",
|
||||
"@types/showdown": "^2.0.0",
|
||||
"@types/vscode": "^1.72.0",
|
||||
"copy-webpack-plugin": "^13.0.0",
|
||||
"ts-loader": "^9.5.1",
|
||||
"typescript": "^5.4.2",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack": "^5.99.5",
|
||||
"webpack-cli": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
36
renderer/package-lock.json
generated
@ -9,16 +9,19 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"element-plus": "^2.9.7",
|
||||
"element-plus": "^2.9.9",
|
||||
"katex": "^0.16.21",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-katex": "^2.0.3",
|
||||
"openai": "^4.93.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^3.2.13",
|
||||
"vue-i18n": "^11.1.0",
|
||||
"vue-router": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
@ -6116,9 +6119,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.9.7",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.7.tgz",
|
||||
"integrity": "sha512-6vjZh5SXBncLhUwJGTVKS5oDljfgGMh6J4zVTeAZK3YdMUN76FgpvHkwwFXocpJpMbii6rDYU3sgie64FyPerQ==",
|
||||
"version": "2.9.9",
|
||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.9.tgz",
|
||||
"integrity": "sha512-gN553+xr7ETkhJhH26YG0fERmd2BSCcQKslbtR8fats0Mc0yCtZOXr00bmoPOt5xGzhuRN1TWc9+f1pCaiA0/Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^3.4.1",
|
||||
@ -8689,7 +8692,7 @@
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"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==",
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -11820,6 +11823,16 @@
|
||||
"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": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
|
||||
@ -12970,13 +12983,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"dev": true,
|
||||
"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/bin/uuid"
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache": {
|
||||
|
@ -9,16 +9,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.8.3",
|
||||
"element-plus": "^2.9.7",
|
||||
"element-plus": "^2.9.9",
|
||||
"katex": "^0.16.21",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-katex": "^2.0.3",
|
||||
"openai": "^4.93.0",
|
||||
"uuid": "^11.1.0",
|
||||
"vue": "^3.2.13",
|
||||
"vue-i18n": "^11.1.0",
|
||||
"vue-router": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
|
@ -22,7 +22,7 @@
|
||||
--vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6);
|
||||
--vscode-progressBar-background: #0e70c0;
|
||||
--vscode-editor-background: #ffffff;
|
||||
--vscode-editor-foreground: #000000;
|
||||
--vscode-editor-foreground: #3d3d3d;
|
||||
--vscode-editorStickyScroll-background: #ffffff;
|
||||
--vscode-editorStickyScrollHover-background: #f0f0f0;
|
||||
--vscode-editorStickyScroll-shadow: #dddddd;
|
||||
|
@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4870215 */
|
||||
src: url('iconfont.woff2?t=1744476757936') format('woff2'),
|
||||
url('iconfont.woff?t=1744476757936') format('woff'),
|
||||
url('iconfont.ttf?t=1744476757936') format('truetype');
|
||||
src: url('iconfont.woff2?t=1746529081655') format('woff2'),
|
||||
url('iconfont.woff?t=1746529081655') format('woff'),
|
||||
url('iconfont.ttf?t=1746529081655') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@ -13,6 +13,78 @@
|
||||
-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 {
|
||||
content: "\ecda";
|
||||
}
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 54 KiB |
@ -5,6 +5,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<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="stylesheet" href="default-dark.css">
|
||||
<link rel="stylesheet" href="vscode.css">
|
||||
|
@ -65,7 +65,6 @@ body::-webkit-scrollbar {
|
||||
}
|
||||
|
||||
.el-textarea__inner {
|
||||
border-radius: .9em !important;
|
||||
padding: 10px !important;
|
||||
box-shadow: 0 0 0 1px var(--main-color) !important;
|
||||
}
|
||||
@ -124,7 +123,7 @@ a {
|
||||
}
|
||||
|
||||
.openmcp-code-block pre code {
|
||||
background-color: none;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.openmcp-code-block pre code::-webkit-scrollbar {
|
||||
@ -133,8 +132,7 @@ a {
|
||||
}
|
||||
|
||||
.tool-arguments .openmcp-code-block pre code {
|
||||
background: var(--el-fill-color-light);
|
||||
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.openmcp-code-block pre code::-webkit-scrollbar-track {
|
||||
@ -206,39 +204,17 @@ a {
|
||||
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-repeat: no-repeat;
|
||||
background-position: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.deepseek-icon {
|
||||
background-image: url('./images/deepseek.com.ico');
|
||||
.el-button:hover {
|
||||
color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
.openai-icon {
|
||||
background-image: url('./images/openai.com.ico');
|
||||
}
|
||||
|
||||
.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');
|
||||
.el-dropdown-menu__item:hover {
|
||||
background-color: var(--background) !important;
|
||||
}
|
@ -2,78 +2,49 @@
|
||||
<div class="main">
|
||||
<Sidebar></Sidebar>
|
||||
<MainPanel></MainPanel>
|
||||
|
||||
<Tour v-if="!userHasReadGuide"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { Connection } from './components/sidebar/sidebar';
|
||||
|
||||
import Sidebar from '@/components/sidebar/index.vue';
|
||||
import MainPanel from '@/components/main-panel/index.vue';
|
||||
import { setDefaultCss } from './hook/css';
|
||||
import { pinkLog } from './views/setting/util';
|
||||
import { acquireVsCodeApi, useMessageBridge } from './api/message-bridge';
|
||||
import { connectionArgs, connectionMethods, connectionResult, doConnect, getServerVersion, launchConnect } from './views/connect/connection';
|
||||
import { loadSetting } from './hook/setting';
|
||||
import { greenLog, pinkLog } from './views/setting/util';
|
||||
import { useMessageBridge } from './api/message-bridge';
|
||||
import { doConnect, loadEnvVar } from './views/connect/connection';
|
||||
import { getTour, loadSetting } from './hook/setting';
|
||||
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();
|
||||
|
||||
// 监听所有消息
|
||||
bridge.addCommandListener('hello', data => {
|
||||
pinkLog(`${data.name} 上线`);
|
||||
pinkLog(`version: ${data.version}`);
|
||||
greenLog(`${data.name}`);
|
||||
greenLog(`version: ${data.version}`);
|
||||
}, { once: true });
|
||||
|
||||
// 监听 connect
|
||||
bridge.addCommandListener('connect', async data => {
|
||||
const { code, msg } = data;
|
||||
connectionResult.success = (code === 200);
|
||||
connectionResult.logString = msg;
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const res = await getServerVersion() as { name: string, version: string };
|
||||
connectionResult.serverInfo.name = res.name || '';
|
||||
connectionResult.serverInfo.version = res.version || '';
|
||||
onMounted(async () => {
|
||||
const loading = ElLoading.service({
|
||||
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
|
||||
setDefaultCss();
|
||||
|
||||
@ -83,19 +54,57 @@ onMounted(() => {
|
||||
|
||||
pinkLog('OpenMCP Client 启动');
|
||||
|
||||
if (acquireVsCodeApi === undefined) {
|
||||
initDebug();
|
||||
} else {
|
||||
initProduce();
|
||||
const platform = getPlatform();
|
||||
|
||||
// 跳转到首页
|
||||
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>
|
||||
|
||||
<style>
|
||||
.main {
|
||||
height: calc(100vh - 50px);
|
||||
height: calc(100vh - 5px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.message-text img {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
|
||||
.icon-chat:before {
|
||||
font-weight: 1000;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { pinkLog } from '@/views/setting/util';
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import { pinkLog, redLog } from '@/views/setting/util';
|
||||
import { acquireVsCodeApi, electronApi, getPlatform } from './platform';
|
||||
import { ref } from 'vue';
|
||||
|
||||
export interface VSCodeMessage {
|
||||
command: string;
|
||||
@ -7,11 +8,14 @@ export interface VSCodeMessage {
|
||||
callbackId?: string;
|
||||
}
|
||||
|
||||
export interface RestFulResponse {
|
||||
code: number;
|
||||
msg: any;
|
||||
}
|
||||
|
||||
export type MessageHandler = (message: VSCodeMessage) => void;
|
||||
export type CommandHandler = (data: any) => void;
|
||||
|
||||
export const acquireVsCodeApi = (window as any)['acquireVsCodeApi'];
|
||||
|
||||
interface AddCommandListenerOption {
|
||||
once: boolean // 只调用一次就销毁
|
||||
}
|
||||
@ -19,27 +23,36 @@ interface AddCommandListenerOption {
|
||||
class MessageBridge {
|
||||
private ws: WebSocket | null = null;
|
||||
private handlers = new Map<string, Set<CommandHandler>>();
|
||||
public isConnected = ref(false);
|
||||
private isConnected: Promise<boolean> | null = null;
|
||||
|
||||
constructor(private wsUrl: string = 'ws://localhost:8080') {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
// 环境检测优先级:
|
||||
// 1. VS Code WebView 环境
|
||||
// 2. 浏览器 WebSocket 环境
|
||||
if (typeof acquireVsCodeApi !== 'undefined') {
|
||||
this.setupVSCodeListener();
|
||||
pinkLog('当前模式:release');
|
||||
} else {
|
||||
|
||||
const platform = getPlatform();
|
||||
|
||||
switch (platform) {
|
||||
case 'vscode':
|
||||
this.setupVsCodeListener();
|
||||
pinkLog('当前模式: vscode');
|
||||
break;
|
||||
|
||||
case 'electron':
|
||||
this.setupElectronListener();
|
||||
pinkLog('当前模式: electron');
|
||||
break;
|
||||
|
||||
case 'web':
|
||||
this.setupWebSocket();
|
||||
pinkLog('当前模式:debug');
|
||||
pinkLog('当前模式: web');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// VS Code 环境监听
|
||||
private setupVSCodeListener() {
|
||||
private setupVsCodeListener() {
|
||||
const vscode = acquireVsCodeApi();
|
||||
|
||||
window.addEventListener('message', (event: MessageEvent<VSCodeMessage>) => {
|
||||
@ -47,37 +60,58 @@ class MessageBridge {
|
||||
});
|
||||
|
||||
this.postMessage = (message) => vscode.postMessage(message);
|
||||
this.isConnected.value = true;
|
||||
}
|
||||
|
||||
// WebSocket 环境连接
|
||||
private setupWebSocket() {
|
||||
this.ws = new WebSocket(this.wsUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.isConnected.value = true;
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data) as VSCodeMessage;
|
||||
this.dispatchMessage(message);
|
||||
} catch (err) {
|
||||
console.error('Message parse error:', err);
|
||||
console.log(event);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.isConnected.value = false;
|
||||
redLog('WebSocket connection closed');
|
||||
};
|
||||
|
||||
this.postMessage = (message) => {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
console.log(message);
|
||||
|
||||
console.log('send', 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)) {
|
||||
this.handlers.set(command, new Set<CommandHandler>());
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const commandHandlers = this.handlers.get(command)!;
|
||||
|
||||
const wrapperCommandHandler = option.once ? (data: any) => {
|
||||
@ -133,6 +168,25 @@ class MessageBridge {
|
||||
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() {
|
||||
this.ws?.close();
|
||||
this.handlers.clear();
|
||||
@ -149,6 +203,7 @@ export function useMessageBridge() {
|
||||
return {
|
||||
postMessage: bridge.postMessage.bind(bridge),
|
||||
addCommandListener: bridge.addCommandListener.bind(bridge),
|
||||
isConnected: bridge.isConnected
|
||||
commandRequest: bridge.commandRequest.bind(bridge),
|
||||
awaitForWebsockt: bridge.awaitForWebsockt.bind(bridge)
|
||||
};
|
||||
}
|
15
renderer/src/api/platform.ts
Normal 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';
|
||||
}
|
||||
}
|
25
renderer/src/components/guide/tour-title.vue
Normal 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>
|
3
renderer/src/components/guide/tour.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { ref } from "vue";
|
||||
|
||||
export const userHasReadGuide = ref(true);
|
291
renderer/src/components/guide/tour.vue
Normal 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 协议中的 resources、prompts、tools 对应。
|
||||
|
||||
而「交互测试」则允许你直接将写好的 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 接口规范的大模型,比如 deepseek,openai,kimi 等等。
|
||||
本地部署的 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>
|
79
renderer/src/components/k-cute-textarea/index.vue
Normal 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>
|
182
renderer/src/components/k-input-object/index.vue
Normal 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>
|
129
renderer/src/components/main-panel/chat/chat-box/chat.ts
Normal 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;
|
||||
}
|
204
renderer/src/components/main-panel/chat/chat-box/index.vue
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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"> {{ 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
}
|
@ -1,50 +1,118 @@
|
||||
/* eslint-disable */
|
||||
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 type { OpenAI } from 'openai';
|
||||
import { callTool } from "../tool/tools";
|
||||
import { callTool } from "../../tool/tools";
|
||||
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;
|
||||
type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
|
||||
export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
||||
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
|
||||
interface TaskLoopOptions {
|
||||
maxEpochs: number;
|
||||
}
|
||||
|
||||
interface IErrorMssage {
|
||||
state: MessageState,
|
||||
msg: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 对任务循环进行的抽象封装
|
||||
*/
|
||||
export class TaskLoop {
|
||||
private bridge = useMessageBridge();
|
||||
private currentChatId = '';
|
||||
private completionUsage: ChatCompletionChunk['usage'] | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly streamingContent: Ref<string>,
|
||||
private readonly streamingToolCalls: Ref<ToolCall[]>,
|
||||
private onError: (msg: string) => void = (msg) => {},
|
||||
private onError: (error: IErrorMssage) => void = (msg) => {},
|
||||
private onChunk: (chunk: ChatCompletionChunk) => void = (chunk) => {},
|
||||
private onDone: () => void = () => {},
|
||||
private onEpoch: () => void = () => {},
|
||||
private readonly taskOptions: TaskLoopOptions = { maxEpochs: 20 },
|
||||
) {}
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
private async handleToolCalls(toolCalls: ToolCall[]) {
|
||||
// TODO: 调用多个工具并返回调用结果?
|
||||
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 {
|
||||
const toolName = toolCall.function.name;
|
||||
const toolArgs = JSON.parse(toolCall.function.arguments);
|
||||
const toolResponse = await callTool(toolName, toolArgs);
|
||||
if (!toolResponse.isError) {
|
||||
const content = JSON.stringify(toolResponse.content);
|
||||
return content;
|
||||
|
||||
console.log(toolResponse);
|
||||
|
||||
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 {
|
||||
this.onError(`工具调用失败: ${toolResponse.content}`);
|
||||
|
||||
return {
|
||||
content: toolResponse.content,
|
||||
state: MessageState.ToolCall
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.onError(`工具调用失败: ${(error as Error).message}`);
|
||||
this.onError({
|
||||
state: MessageState.ToolCall,
|
||||
msg: `工具调用失败: ${(error as Error).message}`
|
||||
});
|
||||
console.error(error);
|
||||
|
||||
return {
|
||||
content: [{
|
||||
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) {
|
||||
const toolCall = chunk.choices[0]?.delta?.tool_calls?.[0];
|
||||
|
||||
if (toolCall) {
|
||||
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) {
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const chunkHandler = this.bridge.addCommandListener('llm/chat/completions/chunk', data => {
|
||||
if (data.code !== 200) {
|
||||
this.onError(data.msg || '请求模型服务时发生错误');
|
||||
reject(new Error(data.msg || '请求模型服务时发生错误'));
|
||||
this.onError({
|
||||
state: MessageState.ReceiveChunkError,
|
||||
msg: data.msg || '请求模型服务时发生错误'
|
||||
});
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const { chunk } = data.msg as { chunk: ChatCompletionChunk };
|
||||
@ -103,6 +182,7 @@ export class TaskLoop {
|
||||
// 处理增量的 content 和 tool_calls
|
||||
this.handleChunkDeltaContent(chunk);
|
||||
this.handleChunkDeltaToolCalls(chunk);
|
||||
this.handleChunkUsage(chunk);
|
||||
|
||||
this.onChunk(chunk);
|
||||
}, { once: false });
|
||||
@ -121,10 +201,19 @@ export class TaskLoop {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public makeChatData(tabStorage: ChatStorage): ChatCompletionCreateParamsBase {
|
||||
public makeChatData(tabStorage: ChatStorage): ChatCompletionCreateParamsBase | undefined {
|
||||
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 temperature = tabStorage.settings.temperature;
|
||||
const tools = getToolSchema(tabStorage.settings.enableTools);
|
||||
@ -140,6 +229,7 @@ export class TaskLoop {
|
||||
// 如果超出了 tabStorage.settings.contextLength, 则删除最早的消息
|
||||
const loadMessages = tabStorage.messages.slice(- tabStorage.settings.contextLength);
|
||||
userMessages.push(...loadMessages);
|
||||
|
||||
// 增加一个id用于锁定状态
|
||||
const id = crypto.randomUUID();
|
||||
|
||||
@ -167,7 +257,7 @@ export class TaskLoop {
|
||||
this.streamingToolCalls.value = [];
|
||||
}
|
||||
|
||||
public registerOnError(handler: (msg: string) => void) {
|
||||
public registerOnError(handler: (msg: IErrorMssage) => void) {
|
||||
this.onError = handler;
|
||||
}
|
||||
|
||||
@ -188,7 +278,15 @@ export class TaskLoop {
|
||||
*/
|
||||
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) {
|
||||
|
||||
@ -197,10 +295,16 @@ export class TaskLoop {
|
||||
// 初始累计清空
|
||||
this.streamingContent.value = '';
|
||||
this.streamingToolCalls.value = [];
|
||||
this.completionUsage = undefined;
|
||||
|
||||
// 构造 chatData
|
||||
const chatData = this.makeChatData(tabStorage);
|
||||
|
||||
if (!chatData) {
|
||||
this.onDone();
|
||||
break;
|
||||
}
|
||||
|
||||
this.currentChatId = chatData.id!;
|
||||
|
||||
// 发送请求
|
||||
@ -212,24 +316,70 @@ export class TaskLoop {
|
||||
tabStorage.messages.push({
|
||||
role: 'assistant',
|
||||
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);
|
||||
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];
|
||||
|
||||
tabStorage.messages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: toolCall.id || toolCall.function.name,
|
||||
content: toolCallResult
|
||||
content: toolCallResult.content,
|
||||
extraInfo: {
|
||||
created: Date.now(),
|
||||
state: toolCallResult.state,
|
||||
serverName: llms[llmManager.currentModelIndex].id || 'unknown',
|
||||
usage: this.completionUsage
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
tabStorage.messages.push({
|
||||
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;
|
||||
|
37
renderer/src/components/main-panel/chat/core/usage.ts
Normal 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;
|
||||
}
|
@ -1,152 +1,73 @@
|
||||
<template>
|
||||
<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 v-for="(message, index) in renderMessages" :key="index"
|
||||
:class="['message-item', message.role.split('/')[0]]">
|
||||
<div class="message-avatar" v-if="message.role.split('/')[0] === 'assistant'">
|
||||
:class="['message-item', message.role.split('/')[0], message.role.split('/')[1]]"
|
||||
>
|
||||
<div class="message-avatar" v-if="message.role === 'assistant/content'">
|
||||
<span class="iconfont icon-robot"></span>
|
||||
</div>
|
||||
<div class="message-avatar" v-else-if="message.role === 'assistant/tool_calls'">
|
||||
</div>
|
||||
|
||||
<!-- 用户输入的部分 -->
|
||||
<div class="message-content" v-if="message.role === 'user'">
|
||||
<div class="message-role"></div>
|
||||
<div class="message-text">
|
||||
<span>{{ message.content }}</span>
|
||||
</div>
|
||||
<Message.User :message="message" :tab-id="props.tabId" />
|
||||
</div>
|
||||
|
||||
<!-- 助手返回的内容部分 -->
|
||||
<div class="message-content" v-else-if="message.role === 'assistant/content'">
|
||||
<div class="message-role">Agent</div>
|
||||
<div class="message-text">
|
||||
<div v-if="message.content" v-html="markdownToHtml(message.content)"></div>
|
||||
</div>
|
||||
<Message.Assistant :message="message" :tab-id="props.tabId" />
|
||||
</div>
|
||||
|
||||
<!-- 助手调用的工具部分 -->
|
||||
<div class="message-content" v-else-if="message.role === 'assistant/tool_calls'">
|
||||
<div class="message-role">
|
||||
Agent
|
||||
<span class="message-reminder" v-if="!message.toolResult">
|
||||
正在使用工具
|
||||
<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)'"
|
||||
<Message.Toolcall
|
||||
:message="message" :tab-id="props.tabId"
|
||||
@update:tool-result="(value, index) => (message.toolResult || [])[index] = value"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<!-- 正在加载的部分实时解析 markdown -->
|
||||
<div v-if="isLoading" class="message-item assistant">
|
||||
<div class="message-avatar">
|
||||
<span class="iconfont icon-chat"></span>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-role">
|
||||
Agent
|
||||
<span class="message-reminder">
|
||||
正在生成答案
|
||||
<span class="tool-loading iconfont icon-double-loading">
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="message-text">
|
||||
<span v-html="waitingMarkdownToHtml(streamingContent)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<Message.StreamingBox :streaming-content="streamingContent" :tab-id="props.tabId" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<el-footer class="chat-footer" ref="footerRef">
|
||||
<div class="input-area">
|
||||
<div class="input-wrapper">
|
||||
<Setting :tabId="tabId" />
|
||||
|
||||
<el-input v-model="userInput" type="textarea" :rows="inputHeightLines" :maxlength="2000"
|
||||
placeholder="输入消息..." @keydown.enter="handleKeydown" resize="none" class="chat-input" />
|
||||
|
||||
<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 v-else class="chat-openmcp-icon">
|
||||
<div>
|
||||
<!-- <span class="iconfont icon-openmcp"></span> -->
|
||||
<span>{{ t('press-and-run') }}
|
||||
<span style="padding: 5px 15px; border-radius: .5em; background-color: var(--background);">
|
||||
<span class="iconfont icon-send"></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-footer>
|
||||
|
||||
<ChatBox
|
||||
:ref="el => footerRef = el"
|
||||
:tab-id="props.tabId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, nextTick, watch, Ref } from 'vue';
|
||||
import { ref, onMounted, defineComponent, defineProps, onUnmounted, computed, nextTick, watch, provide } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ElMessage, ScrollbarInstance } from 'element-plus';
|
||||
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' });
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
function waitingMarkdownToHtml(content: string) {
|
||||
if (content) {
|
||||
return markdownToHtml(content);
|
||||
}
|
||||
return '<span class="typing-cursor">|</span>';
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
tabId: {
|
||||
type: Number,
|
||||
@ -157,32 +78,19 @@ const props = defineProps({
|
||||
const tab = tabs.content[props.tabId];
|
||||
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
|
||||
if (!tabStorage.messages) {
|
||||
tabStorage.messages = [] as ChatMessage[];
|
||||
}
|
||||
|
||||
interface IRenderMessage {
|
||||
role: 'user' | 'assistant/content' | 'assistant/tool_calls' | 'tool';
|
||||
content: string;
|
||||
toolResult?: string;
|
||||
tool_calls?: ToolCall[];
|
||||
showJson?: Ref<boolean>;
|
||||
}
|
||||
|
||||
const renderMessages = computed(() => {
|
||||
const messages: IRenderMessage[] = [];
|
||||
for (const message of tabStorage.messages) {
|
||||
if (message.role === 'user') {
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: message.content
|
||||
content: message.content,
|
||||
extraInfo: message.extraInfo
|
||||
});
|
||||
} else if (message.role === 'assistant') {
|
||||
if (message.tool_calls) {
|
||||
@ -190,12 +98,17 @@ const renderMessages = computed(() => {
|
||||
role: 'assistant/tool_calls',
|
||||
content: message.content,
|
||||
tool_calls: message.tool_calls,
|
||||
showJson: ref(false)
|
||||
showJson: ref(false),
|
||||
extraInfo: {
|
||||
...message.extraInfo,
|
||||
state: MessageState.Unknown
|
||||
}
|
||||
});
|
||||
} else {
|
||||
messages.push({
|
||||
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];
|
||||
if (lastAssistantMessage.role === 'assistant/tool_calls') {
|
||||
lastAssistantMessage.toolResult = message.content;
|
||||
lastAssistantMessage.extraInfo.state = message.extraInfo.state;
|
||||
lastAssistantMessage.extraInfo.usage = lastAssistantMessage.extraInfo.usage || message.extraInfo.usage;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -218,26 +133,19 @@ const streamingToolCalls = ref<ToolCall[]>([]);
|
||||
|
||||
const chatContainerRef = ref<any>(null);
|
||||
const messageListRef = ref<any>(null);
|
||||
const footerRef = ref<any>(null);
|
||||
|
||||
const footerRef = ref<HTMLElement>();
|
||||
const scrollHeight = ref('500px');
|
||||
|
||||
const updateScrollHeight = () => {
|
||||
function updateScrollHeight() {
|
||||
if (chatContainerRef.value && footerRef.value) {
|
||||
const containerHeight = chatContainerRef.value.clientHeight;
|
||||
const footerHeight = footerRef.value.clientHeight;
|
||||
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 scrollbarRef = ref<ScrollbarInstance>();
|
||||
@ -252,8 +160,13 @@ const handleScroll = ({ scrollTop, scrollHeight, clientHeight }: {
|
||||
autoScroll.value = scrollTop + clientHeight >= scrollHeight - 10;
|
||||
};
|
||||
|
||||
provide('streamingContent', streamingContent);
|
||||
provide('streamingToolCalls', streamingToolCalls);
|
||||
provide('isLoading', isLoading);
|
||||
provide('autoScroll', autoScroll);
|
||||
|
||||
// 修改 scrollToBottom 方法
|
||||
const scrollToBottom = async () => {
|
||||
async function scrollToBottom() {
|
||||
if (!scrollbarRef.value || !messageListRef.value) return;
|
||||
|
||||
await nextTick(); // 等待 DOM 更新
|
||||
@ -266,7 +179,9 @@ const scrollToBottom = async () => {
|
||||
} catch (error) {
|
||||
console.error('Scroll to bottom failed:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
provide('scrollToBottom', scrollToBottom);
|
||||
|
||||
// 添加对 streamingContent 的监听
|
||||
watch(streamingContent, () => {
|
||||
@ -281,99 +196,10 @@ watch(streamingToolCalls, () => {
|
||||
}
|
||||
}, { 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>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
.chat-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@ -381,6 +207,30 @@ const formatToolArguments = (args: string) => {
|
||||
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 {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
@ -395,6 +245,7 @@ const formatToolArguments = (args: string) => {
|
||||
|
||||
.message-avatar {
|
||||
margin-right: 12px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
@ -412,17 +263,13 @@ const formatToolArguments = (args: string) => {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.message-text.tool_calls {
|
||||
border-left: 3px solid var(--main-color);
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.user .message-text {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user .message-text>span {
|
||||
.user .message-text > span {
|
||||
border-radius: .9em;
|
||||
background-color: var(--main-light-color);
|
||||
padding: 10px 15px;
|
||||
@ -447,134 +294,10 @@ const formatToolArguments = (args: string) => {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.chat-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--el-border-color);
|
||||
flex-shrink: 0;
|
||||
position: absolute;
|
||||
height: fit-content;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
.assistant.tool_calls {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.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 h3,
|
||||
.message-text ol,
|
||||
@ -582,7 +305,6 @@ const formatToolArguments = (args: string) => {
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.4;
|
||||
/* 可以根据需要调整行高 */
|
||||
}
|
||||
|
||||
.message-text ol li,
|
||||
|
@ -37,6 +37,7 @@
|
||||
height: inherit;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.vscode-dark :not(pre)>code[class*=language-],
|
||||
@ -251,6 +252,7 @@
|
||||
height: inherit;
|
||||
display: block;
|
||||
overflow: auto;
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
.vscode-light :not(pre)>code[class*=language-],
|
@ -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>
|
5
renderer/src/components/main-panel/chat/message/index.ts
Normal 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 };
|
@ -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>
|
@ -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>
|
@ -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>
|
333
renderer/src/components/main-panel/chat/message/toolcall.vue
Normal 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>
|
142
renderer/src/components/main-panel/chat/message/user.vue
Normal 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>
|
@ -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>
|
@ -6,7 +6,7 @@
|
||||
<span
|
||||
class="tab"
|
||||
v-for="(tab, index) of tabs.content"
|
||||
:key="index"
|
||||
:key="tab.id"
|
||||
:class="{ 'active-tab': tabs.activeIndex === index }"
|
||||
@click="setActiveTab(index)"
|
||||
>
|
||||
@ -40,6 +40,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { addNewTab, tabs, closeTab } from './panel';
|
||||
import { panelLoaded } from '@/hook/panel';
|
||||
|
||||
defineComponent({ name: 'main-panel' });
|
||||
|
||||
@ -69,20 +70,19 @@ function setActiveTab(index: number) {
|
||||
|
||||
<style>
|
||||
.main-panel-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
height: 100%;
|
||||
margin-left: 20px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
background-color: var(--sidebar);
|
||||
border-radius: 1.2em;
|
||||
width: 100%;
|
||||
height: 90%;
|
||||
height: calc(100% - 35px);
|
||||
}
|
||||
|
||||
.scroll-tabs-container {
|
||||
@ -91,13 +91,13 @@ function setActiveTab(index: number) {
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
height: 78px;
|
||||
height: 30px;
|
||||
width: 90%;
|
||||
background-color: var(--background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
padding: 0 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.tabs-container .el-scrollbar {
|
||||
@ -106,12 +106,12 @@ function setActiveTab(index: number) {
|
||||
|
||||
.tabs-container .tab {
|
||||
white-space: nowrap;
|
||||
margin: 5px;
|
||||
font-size: 13px;
|
||||
margin-right: 5px;
|
||||
font-size: 12px;
|
||||
width: 120px;
|
||||
border-radius: .5em;
|
||||
background-color: var(--sidebar);
|
||||
padding: 10px;
|
||||
padding: 3px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: var(--animation-3s);
|
||||
@ -159,11 +159,9 @@ function setActiveTab(index: number) {
|
||||
|
||||
.tabs-container .add-button {
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
font-size: 15px;
|
||||
margin-left: 5px;
|
||||
border-radius: .5em;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { watch, reactive } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import Resource from './resource/index.vue';
|
||||
import Chat from './chat/index.vue';
|
||||
@ -11,6 +11,7 @@ import { safeSavePanels, savePanels } from '@/hook/panel';
|
||||
const { t } = I18n.global;
|
||||
|
||||
interface Tab {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
type: string;
|
||||
@ -50,8 +51,9 @@ watch(
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
function createTab(type: string, index: number): Tab {
|
||||
export function createTab(type: string, index: number): Tab {
|
||||
let customName: string | null = null;
|
||||
const id = uuidv4();
|
||||
|
||||
return {
|
||||
get name() {
|
||||
@ -65,6 +67,7 @@ function createTab(type: string, index: number): Tab {
|
||||
},
|
||||
icon: 'icon-blank',
|
||||
type,
|
||||
id,
|
||||
componentIndex: -1,
|
||||
component: undefined,
|
||||
storage: {},
|
||||
@ -83,6 +86,8 @@ export function closeTab(index: number) {
|
||||
|
||||
tabs.content.splice(index, 1);
|
||||
|
||||
console.log(tabs.content);
|
||||
|
||||
// 调整活动标签索引
|
||||
if (tabs.activeIndex >= index) {
|
||||
tabs.activeIndex = Math.max(0, tabs.activeIndex - 1);
|
||||
|
@ -1,27 +1,22 @@
|
||||
<template>
|
||||
<el-scrollbar height="100%">
|
||||
<div class="prompt-module">
|
||||
<div class="left">
|
||||
<h2>
|
||||
<span class="iconfont icon-chat"></span>
|
||||
提示词模块
|
||||
</h2>
|
||||
<h3><code>prompts/list</code></h3>
|
||||
|
||||
<PromptTemplates
|
||||
:tab-id="props.tabId"
|
||||
></PromptTemplates>
|
||||
<PromptTemplates :tab-id="props.tabId"></PromptTemplates>
|
||||
|
||||
</div>
|
||||
<div class="right">
|
||||
<PromptReader
|
||||
:tab-id="props.tabId"
|
||||
></PromptReader>
|
||||
<PromptReader :tab-id="props.tabId"></PromptReader>
|
||||
|
||||
<PromptLogger
|
||||
:tab-id="props.tabId"
|
||||
></PromptLogger>
|
||||
<PromptLogger :tab-id="props.tabId"></PromptLogger>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -13,7 +13,7 @@
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<el-scrollbar height="350px">
|
||||
<el-scrollbar>
|
||||
<div
|
||||
class="output-content"
|
||||
contenteditable="false"
|
||||
|
@ -3,20 +3,20 @@
|
||||
<h3>{{ currentPrompt.name }}</h3>
|
||||
</div>
|
||||
<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"
|
||||
: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}`"
|
||||
@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}`"
|
||||
@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>
|
||||
@ -32,13 +32,14 @@
|
||||
</template>
|
||||
|
||||
<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 type { FormInstance, FormRules } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
import { parsePromptTemplate, promptsManager, PromptStorage } from './prompts';
|
||||
import { CasualRestAPI, PromptsGetResponse } from '@/hook/type';
|
||||
import { promptsManager, PromptStorage } from './prompts';
|
||||
import { PromptsGetResponse } from '@/hook/type';
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||
|
||||
defineComponent({ name: 'prompt-reader' });
|
||||
|
||||
@ -48,15 +49,32 @@ const props = defineProps({
|
||||
tabId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
currentPromptName: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
});
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as PromptStorage;
|
||||
const emits = defineEmits(['prompt-get-response']);
|
||||
|
||||
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>();
|
||||
// TODO: 将 formData 装入 tabStorage
|
||||
const formData = ref<Record<string, any>>({});
|
||||
const loading = ref(false);
|
||||
const responseData = ref<PromptsGetResponse>();
|
||||
|
||||
@ -93,11 +111,18 @@ const formRules = computed<FormRules>(() => {
|
||||
});
|
||||
|
||||
const initFormData = () => {
|
||||
formData.value = {}
|
||||
currentPrompt.value?.params.forEach(param => {
|
||||
formData.value[param.name] = param.type === 'number' ? 0 :
|
||||
param.type === 'boolean' ? false : ''
|
||||
})
|
||||
|
||||
if (!currentPrompt.value?.params) return;
|
||||
|
||||
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 = () => {
|
||||
@ -105,24 +130,25 @@ const resetForm = () => {
|
||||
responseData.value = undefined;
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
async function handleSubmit() {
|
||||
const bridge = useMessageBridge();
|
||||
|
||||
bridge.addCommandListener('prompts/get', (data: CasualRestAPI<PromptsGetResponse>) => {
|
||||
tabStorage.lastPromptGetResponse = data.msg;
|
||||
}, { once: true });
|
||||
|
||||
bridge.postMessage({
|
||||
command: 'prompts/get',
|
||||
data: { promptId: currentPrompt.value.name, args: JSON.parse(JSON.stringify(formData.value)) }
|
||||
const { code, msg } = await bridge.commandRequest('prompts/get', {
|
||||
promptId: currentPrompt.value.name,
|
||||
args: JSON.parse(JSON.stringify(tabStorage.formData))
|
||||
});
|
||||
|
||||
tabStorage.lastPromptGetResponse = msg;
|
||||
|
||||
emits('prompt-get-response', msg);
|
||||
}
|
||||
|
||||
watch(() => tabStorage.currentPromptName, () => {
|
||||
if (props.tabId >= 0) {
|
||||
watch(() => tabStorage.currentPromptName, () => {
|
||||
initFormData();
|
||||
resetForm();
|
||||
}, { immediate: true });
|
||||
|
||||
}, { immediate: true });
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
@ -1,10 +1,18 @@
|
||||
<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">
|
||||
<el-scrollbar height="500px">
|
||||
<div class="prompt-template-container">
|
||||
<div
|
||||
class="item"
|
||||
:class="{ 'active': tabStorage.currentPromptName === template.name }"
|
||||
:class="{ 'active': props.tabId >= 0 && tabStorage.currentPromptName === template.name }"
|
||||
v-for="template of promptsManager.templates"
|
||||
:key="template.name"
|
||||
@click="handleClick(template)"
|
||||
@ -15,20 +23,12 @@
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="prompt-template-function-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="reloadPrompts({ first: false })"
|
||||
>
|
||||
{{ t('refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
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 { promptsManager, PromptStorage } from './prompts';
|
||||
import { tabs } from '../panel';
|
||||
@ -44,8 +44,20 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as PromptStorage;
|
||||
const emits = defineEmits([ 'prompt-selected' ]);
|
||||
|
||||
let tabStorage: PromptStorage;
|
||||
|
||||
if (props.tabId >= 0) {
|
||||
const tab = tabs.content[props.tabId];
|
||||
tabStorage = tab.storage as PromptStorage;
|
||||
} else {
|
||||
tabStorage = reactive({
|
||||
currentPromptName: '',
|
||||
formData: {},
|
||||
lastPromptGetResponse: undefined
|
||||
});
|
||||
}
|
||||
|
||||
function reloadPrompts(option: { first: boolean }) {
|
||||
bridge.postMessage({
|
||||
@ -62,9 +74,11 @@ function reloadPrompts(option: { first: boolean }) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(template: PromptTemplate) {
|
||||
tabStorage.currentPromptName = template.name;
|
||||
function handleClick(prompt: PromptTemplate) {
|
||||
tabStorage.currentPromptName = prompt.name;
|
||||
tabStorage.lastPromptGetResponse = undefined;
|
||||
|
||||
emits('prompt-selected', prompt);
|
||||
}
|
||||
|
||||
let commandCancel: (() => void);
|
||||
@ -73,7 +87,9 @@ onMounted(() => {
|
||||
commandCancel = bridge.addCommandListener('prompts/list', (data: CasualRestAPI<PromptsListResponse>) => {
|
||||
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.lastPromptGetResponse = undefined;
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ export const promptsManager = reactive<{
|
||||
export interface PromptStorage {
|
||||
currentPromptName: string;
|
||||
lastPromptGetResponse?: PromptsGetResponse;
|
||||
formData: Record<string, any>;
|
||||
}
|
||||
|
||||
export function parsePromptTemplate(template: string): {
|
||||
|
@ -1,15 +1,19 @@
|
||||
<template>
|
||||
<el-scrollbar height="100%">
|
||||
<div class="resource-module">
|
||||
<div class="left">
|
||||
<h2>
|
||||
<span class="iconfont icon-file"></span>
|
||||
{{ t("resources") + t("module") }}
|
||||
</h2>
|
||||
<h3><code>resources/templates/list</code></h3>
|
||||
|
||||
<ResourceTemplates
|
||||
<ResourceListTemplates
|
||||
:tab-id="props.tabId"
|
||||
></ResourceTemplates>
|
||||
></ResourceListTemplates>
|
||||
|
||||
<ResourceList
|
||||
:tab-id="props.tabId"
|
||||
></ResourceList>
|
||||
|
||||
</div>
|
||||
<div class="right">
|
||||
@ -21,13 +25,17 @@
|
||||
:tab-id="props.tabId"
|
||||
></ResourceLogger>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineProps } from 'vue';
|
||||
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 ResourceLogger from './resource-logger.vue';
|
||||
|
||||
@ -40,8 +48,6 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -3,19 +3,17 @@
|
||||
<h3>{{ currentResource.template?.name }}</h3>
|
||||
</div>
|
||||
<div class="resource-reader-container">
|
||||
<el-form :model="formData" :rules="formRules" ref="formRef" label-position="top">
|
||||
<el-form-item v-for="param in currentResource?.params" :key="param.name"
|
||||
:label="param.name" :prop="param.name">
|
||||
<el-form :model="tabStorage.formData" :rules="formRules" ref="formRef" label-position="top">
|
||||
<el-form-item v-for="param in currentResource?.params" :key="param.name" :label="param.name"
|
||||
:prop="param.name">
|
||||
<!-- 根据不同类型渲染不同输入组件 -->
|
||||
<el-input v-if="param.type === 'string'" v-model="formData[param.name]"
|
||||
:placeholder="param.placeholder || `请输入${param.name}`"
|
||||
@keydown.enter.prevent="handleSubmit" />
|
||||
<el-input v-if="param.type === 'string'" v-model="tabStorage.formData[param.name]"
|
||||
:placeholder="param.placeholder || `请输入${param.name}`" @keydown.enter.prevent="handleSubmit" />
|
||||
|
||||
<el-input-number v-else-if="param.type === 'number'" v-model="formData[param.name]"
|
||||
:placeholder="param.placeholder || `请输入${param.name}`"
|
||||
@keydown.enter.prevent="handleSubmit" />
|
||||
<el-input-number v-else-if="param.type === 'number'" v-model="tabStorage.formData[param.name]"
|
||||
:placeholder="param.placeholder || `请输入${param.name}`" @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>
|
||||
@ -31,13 +29,14 @@
|
||||
</template>
|
||||
|
||||
<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 type { FormInstance, FormRules } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
import { parseResourceTemplate, resourcesManager, ResourceStorage } from './resources';
|
||||
import { CasualRestAPI, ResourcesReadResponse } from '@/hook/type';
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||
|
||||
defineComponent({ name: 'resource-reader' });
|
||||
|
||||
@ -47,15 +46,35 @@ const props = defineProps({
|
||||
tabId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
currentResourceName: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
});
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ResourceStorage;
|
||||
const emits = defineEmits(['resource-get-response']);
|
||||
|
||||
let tabStorage: ResourceStorage;
|
||||
|
||||
if (props.tabId >= 0) {
|
||||
const tab = tabs.content[props.tabId];
|
||||
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 formData = ref<Record<string, any>>({});
|
||||
const loading = ref(false);
|
||||
const responseData = ref<ResourcesReadResponse>();
|
||||
|
||||
@ -67,7 +86,7 @@ const currentResource = computed(() => {
|
||||
const viewParams = params.map(param => ({
|
||||
name: param,
|
||||
type: 'string',
|
||||
placeholder: t('enter') +' ' + param,
|
||||
placeholder: t('enter') + ' ' + param,
|
||||
required: true
|
||||
}));
|
||||
|
||||
@ -93,13 +112,19 @@ const formRules = computed<FormRules>(() => {
|
||||
return rules;
|
||||
});
|
||||
|
||||
|
||||
// 初始化表单数据
|
||||
const initFormData = () => {
|
||||
formData.value = {}
|
||||
currentResource.value?.params.forEach(param => {
|
||||
formData.value[param.name] = param.type === 'number' ? 0 :
|
||||
param.type === 'boolean' ? false : ''
|
||||
if (!currentResource.value?.params) return;
|
||||
|
||||
const newSchemaDataForm: Record<string, number | boolean | string> = {};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function handleSubmit() {
|
||||
function getUri() {
|
||||
if (tabStorage.currentType === 'template') {
|
||||
const fillFn = currentResource.value.fill;
|
||||
const uri = fillFn(formData.value);
|
||||
const uri = fillFn(tabStorage.formData);
|
||||
return uri;
|
||||
}
|
||||
|
||||
const bridge = useMessageBridge();
|
||||
const currentResourceName = props.tabId >= 0 ? tabStorage.currentResourceName : props.currentResourceName;
|
||||
|
||||
bridge.addCommandListener('resources/read', (data: CasualRestAPI<ResourcesReadResponse>) => {
|
||||
tabStorage.lastResourceReadResponse = data.msg;
|
||||
}, { once: true });
|
||||
const targetResource = resourcesManager.resources.find(resources => resources.name === currentResourceName);
|
||||
|
||||
bridge.postMessage({
|
||||
command: 'resources/read',
|
||||
data: { resourceUri: uri }
|
||||
});
|
||||
return targetResource?.uri;
|
||||
}
|
||||
|
||||
// 监听资源变化重置表单
|
||||
watch(() => tabStorage.currentResourceName, () => {
|
||||
// 提交表单
|
||||
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, () => {
|
||||
initFormData();
|
||||
resetForm();
|
||||
}, { immediate: true });
|
||||
}, { immediate: true });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -1,10 +1,18 @@
|
||||
<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">
|
||||
<el-scrollbar height="500px">
|
||||
<el-scrollbar height="500px" v-if="resourcesManager.templates.length > 0">
|
||||
<div class="resource-template-container">
|
||||
<div
|
||||
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"
|
||||
:key="template.name"
|
||||
@click="handleClick(template)"
|
||||
@ -14,21 +22,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<div v-else style="padding: 10px;">
|
||||
empty
|
||||
</div>
|
||||
<div class="resource-template-function-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="reloadResources({ first: false })"
|
||||
>
|
||||
{{ t('refresh') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
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 { resourcesManager, ResourceStorage } from './resources';
|
||||
import { tabs } from '../panel';
|
||||
@ -44,8 +47,19 @@ const props = defineProps({
|
||||
}
|
||||
});
|
||||
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ResourceStorage;
|
||||
let tabStorage: ResourceStorage;
|
||||
|
||||
if (props.tabId >= 0) {
|
||||
const tab = tabs.content[props.tabId];
|
||||
tabStorage = tab.storage as ResourceStorage;
|
||||
} else {
|
||||
tabStorage = reactive({
|
||||
currentType:'template',
|
||||
currentResourceName: '',
|
||||
formData: {},
|
||||
lastResourceReadResponse: undefined
|
||||
});
|
||||
}
|
||||
|
||||
function reloadResources(option: { first: boolean }) {
|
||||
bridge.postMessage({
|
||||
@ -63,8 +77,8 @@ function reloadResources(option: { first: boolean }) {
|
||||
}
|
||||
|
||||
function handleClick(template: ResourceTemplate) {
|
||||
tabStorage.currentType = 'template';
|
||||
tabStorage.currentResourceName = template.name;
|
||||
// TODO: 恢复这部分响应?
|
||||
tabStorage.lastResourceReadResponse = undefined;
|
||||
}
|
||||
|
||||
@ -74,11 +88,13 @@ onMounted(() => {
|
||||
commandCancel = bridge.addCommandListener('resources/templates/list', (data: CasualRestAPI<ResourceTemplatesListResponse>) => {
|
||||
resourcesManager.templates = data.msg.resourceTemplates || [];
|
||||
|
||||
if (resourcesManager.templates.length > 0) {
|
||||
tabStorage.currentResourceName = resourcesManager.templates[0].name;
|
||||
// TODO: 恢复这部分响应?
|
||||
if (tabStorage.currentType === 'template') {
|
||||
const targetResource = resourcesManager.templates.find(template => template.name === tabStorage.currentResourceName);
|
||||
if (targetResource === undefined) {
|
||||
tabStorage.currentResourceName = resourcesManager.templates[0]?.name;
|
||||
tabStorage.lastResourceReadResponse = undefined;
|
||||
}
|
||||
}
|
||||
}, { once: false });
|
||||
|
||||
reloadResources({ first: true });
|
||||
@ -89,10 +105,23 @@ onUnmounted(() => {
|
||||
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;
|
188
renderer/src/components/main-panel/resource/resource-list.vue
Normal 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>
|
@ -13,15 +13,28 @@
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<el-scrollbar height="350px">
|
||||
<el-scrollbar>
|
||||
<div
|
||||
class="output-content"
|
||||
contenteditable="false"
|
||||
>
|
||||
<template v-if="!showRawJson">
|
||||
<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 }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ formattedJson }}
|
||||
@ -36,6 +49,7 @@ import { defineComponent, defineProps, computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { tabs } from '../panel';
|
||||
import { ResourceStorage } from './resources';
|
||||
import { getImageBlobUrlByBase64 } from '@/hook/util';
|
||||
|
||||
defineComponent({ name: 'resource-logger' });
|
||||
const { t } = useI18n();
|
||||
@ -90,7 +104,7 @@ const formattedJson = computed(() => {
|
||||
.resource-logger .output-content {
|
||||
border-radius: .5em;
|
||||
padding: 15px;
|
||||
min-height: 300px;
|
||||
min-height: 600px;
|
||||
height: fit-content;
|
||||
font-family: var(--code-font-family);
|
||||
white-space: pre-wrap;
|
||||
@ -101,4 +115,8 @@ const formattedJson = computed(() => {
|
||||
line-height: 1.5;
|
||||
background-color: var(--sidebar);
|
||||
}
|
||||
|
||||
.resource-list-image {
|
||||
cursor: unset;
|
||||
}
|
||||
</style>
|
@ -1,18 +1,22 @@
|
||||
import { ResourcesReadResponse, ResourceTemplate, ResourceTemplatesListResponse } from '@/hook/type';
|
||||
import { ResourcesReadResponse, ResourceTemplate, Resources } from '@/hook/type';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
|
||||
export const resourcesManager = reactive<{
|
||||
current: ResourceTemplate | undefined
|
||||
templates: ResourceTemplate[]
|
||||
templates: ResourceTemplate[],
|
||||
resources: Resources[]
|
||||
}>({
|
||||
current: undefined,
|
||||
templates: []
|
||||
templates: [],
|
||||
resources: []
|
||||
});
|
||||
|
||||
export interface ResourceStorage {
|
||||
currentType: 'resource' | 'template';
|
||||
currentResourceName: string;
|
||||
lastResourceReadResponse?: ResourcesReadResponse;
|
||||
formData: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,7 +26,7 @@ export interface ResourceStorage {
|
||||
*/
|
||||
export function parseResourceTemplate(template: string): {
|
||||
params: string[],
|
||||
fill: (params: Record<string, string>) => string
|
||||
fill: (params: Record<string, number | boolean | string>) => string
|
||||
} {
|
||||
// 1. 提取所有参数名
|
||||
const paramRegex = /\{([^}]+)\}/g;
|
||||
@ -36,7 +40,7 @@ export function parseResourceTemplate(template: string): {
|
||||
const paramList = Array.from(params);
|
||||
|
||||
// 2. 创建填充函数
|
||||
const fill = (values: Record<string, string>): string => {
|
||||
const fill = (values: Record<string, number | boolean | string>): string => {
|
||||
let result = template;
|
||||
|
||||
// 验证所有必填参数
|
||||
@ -48,7 +52,7 @@ export function parseResourceTemplate(template: string): {
|
||||
|
||||
// 替换所有参数
|
||||
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;
|
||||
|
@ -1,27 +1,22 @@
|
||||
<template>
|
||||
<el-scrollbar height="100%">
|
||||
<div class="tool-module">
|
||||
<div class="left">
|
||||
<h2>
|
||||
<span class="iconfont icon-tool"></span>
|
||||
工具模块
|
||||
</h2>
|
||||
<h3><code>tools/list</code></h3>
|
||||
|
||||
<ToolList
|
||||
:tab-id="props.tabId"
|
||||
></ToolList>
|
||||
<ToolList :tab-id="props.tabId"></ToolList>
|
||||
|
||||
</div>
|
||||
<div class="right">
|
||||
<ToolExecutor
|
||||
:tab-id="props.tabId"
|
||||
></ToolExecutor>
|
||||
<ToolExecutor :tab-id="props.tabId"></ToolExecutor>
|
||||
|
||||
<ToolLogger
|
||||
:tab-id="props.tabId"
|
||||
></ToolLogger>
|
||||
<ToolLogger :tab-id="props.tabId"></ToolLogger>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -3,9 +3,8 @@
|
||||
<h3>{{ currentTool?.name }}</h3>
|
||||
</div>
|
||||
<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">
|
||||
<el-scrollbar height="150px">
|
||||
<el-form-item
|
||||
v-for="[name, property] in Object.entries(currentTool.inputSchema.properties)"
|
||||
:key="name"
|
||||
@ -15,25 +14,33 @@
|
||||
>
|
||||
<el-input
|
||||
v-if="property.type === 'string'"
|
||||
v-model="formData[name]"
|
||||
:placeholder="t('enter') + ' ' + (property.title || name)"
|
||||
v-model="tabStorage.formData[name]"
|
||||
type="text"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
|
||||
@keydown.enter.prevent="handleExecute"
|
||||
/>
|
||||
|
||||
<el-input-number
|
||||
v-else-if="property.type === 'number' || property.type === 'integer'"
|
||||
v-model="formData[name]"
|
||||
v-model="tabStorage.formData[name]"
|
||||
controls-position="right"
|
||||
:placeholder="t('enter') + ' ' + (property.title || name)"
|
||||
:placeholder="property.description || t('enter') + ' ' + (property.title || name)"
|
||||
@keydown.enter.prevent="handleExecute"
|
||||
/>
|
||||
|
||||
<el-switch
|
||||
v-else-if="property.type === 'boolean'"
|
||||
v-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-scrollbar>
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
@ -54,8 +61,9 @@ import { useI18n } from 'vue-i18n';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
import { callTool, toolsManager, ToolStorage } from './tools';
|
||||
import { CasualRestAPI, ToolCallResponse } from '@/hook/type';
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||
|
||||
import KInputObject from '@/components/k-input-object/index.vue';
|
||||
|
||||
defineComponent({ name: 'tool-executor' });
|
||||
|
||||
@ -71,14 +79,21 @@ const props = defineProps({
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ToolStorage;
|
||||
|
||||
if (!tabStorage.formData) {
|
||||
tabStorage.formData = {};
|
||||
}
|
||||
|
||||
console.log(tabStorage.formData);
|
||||
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const formData = ref<Record<string, any>>({});
|
||||
const loading = ref(false);
|
||||
|
||||
const currentTool = computed(() => {
|
||||
return toolsManager.tools.find(tool => tool.name === tabStorage.currentToolName);
|
||||
});
|
||||
|
||||
|
||||
const formRules = computed<FormRules>(() => {
|
||||
const rules: FormRules = {};
|
||||
if (!currentTool.value?.inputSchema?.properties) return rules;
|
||||
@ -98,26 +113,44 @@ const formRules = computed<FormRules>(() => {
|
||||
return rules;
|
||||
});
|
||||
|
||||
|
||||
const initFormData = () => {
|
||||
formData.value = {};
|
||||
// 初始化,根据输入的 inputSchema 校验
|
||||
// 1. 当前是否存在缺失的 key value,如果有,则根据 schema 给与默认值
|
||||
// 2. 如果有多余的 key value,则删除
|
||||
|
||||
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]) => {
|
||||
formData.value[name] = (property.type === 'number' || property.type === 'integer') ? 0 :
|
||||
property.type === 'boolean' ? false : '';
|
||||
newSchemaDataForm[name] = getDefaultValue(property);
|
||||
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 = () => {
|
||||
formRef.value?.resetFields();
|
||||
tabStorage.lastToolCallResponse = undefined;
|
||||
};
|
||||
|
||||
async function handleExecute() {
|
||||
if (!currentTool.value) return;
|
||||
|
||||
const toolResponse = await callTool(tabStorage.currentToolName, formData.value);
|
||||
loading.value = true;
|
||||
try {
|
||||
tabStorage.lastToolCallResponse = undefined;
|
||||
const toolResponse = await callTool(tabStorage.currentToolName, tabStorage.formData);
|
||||
tabStorage.lastToolCallResponse = toolResponse;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => tabStorage.currentToolName, () => {
|
||||
@ -133,4 +166,18 @@ watch(() => tabStorage.currentToolName, () => {
|
||||
border-radius: .5em;
|
||||
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>
|
@ -1,4 +1,12 @@
|
||||
<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">
|
||||
<el-scrollbar height="500px">
|
||||
<div class="tool-list-container">
|
||||
@ -15,13 +23,9 @@
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="tool-list-function-container">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="reloadTools({ first: false })"
|
||||
>
|
||||
{{ t('refresh') }}
|
||||
</el-button>
|
||||
|
||||
<div>
|
||||
<!-- resources/list -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -73,7 +77,11 @@ onMounted(() => {
|
||||
commandCancel = bridge.addCommandListener('tools/list', (data: CasualRestAPI<ToolsListResponse>) => {
|
||||
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.lastToolCallResponse = undefined;
|
||||
}
|
||||
|
@ -3,35 +3,34 @@
|
||||
<span>
|
||||
<span>{{ t('response') }}</span>
|
||||
<span style="width: 200px;">
|
||||
<el-switch
|
||||
v-model="showRawJson"
|
||||
inline-prompt
|
||||
active-text="JSON"
|
||||
inactive-text="Text"
|
||||
<el-switch v-model="showRawJson" inline-prompt active-text="JSON" inactive-text="Text"
|
||||
style="margin-left: 10px; width: 200px;"
|
||||
:inactive-action-style="'backgroundColor: var(--sidebar)'"
|
||||
/>
|
||||
:inactive-action-style="'backgroundColor: var(--sidebar)'" />
|
||||
</span>
|
||||
</span>
|
||||
<el-scrollbar height="300px">
|
||||
<div
|
||||
class="output-content"
|
||||
contenteditable="false"
|
||||
>
|
||||
<el-scrollbar height="500px">
|
||||
<div class="output-content" contenteditable="false">
|
||||
|
||||
<!-- TODO: 更加稳定,现在通过下面这个来判断上一次执行结果是否成功 -->
|
||||
<div v-if="typeof tabStorage.lastToolCallResponse === 'string'" class="error-tool-call">
|
||||
<span>
|
||||
{{ tabStorage.lastToolCallResponse }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- 展示原本的信息 -->
|
||||
<template v-if="!showRawJson">
|
||||
<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') }}
|
||||
</template>
|
||||
{{tabStorage.lastToolCallResponse?.content.map(c => c.text).join('\n')}}
|
||||
</template>
|
||||
|
||||
<!-- 展示 json -->
|
||||
<template v-else>
|
||||
{{ formattedJson }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
@ -41,6 +40,7 @@ import { defineComponent, defineProps, computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { tabs } from '../panel';
|
||||
import { ToolStorage } from './tools';
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
|
||||
defineComponent({ name: 'tool-logger' });
|
||||
const { t } = useI18n();
|
||||
@ -59,11 +59,15 @@ const showRawJson = ref(false);
|
||||
|
||||
const formattedJson = computed(() => {
|
||||
try {
|
||||
if (typeof tabStorage.lastToolCallResponse === 'string') {
|
||||
return tabStorage.lastToolCallResponse;
|
||||
}
|
||||
return JSON.stringify(tabStorage.lastToolCallResponse, null, 2);
|
||||
} catch {
|
||||
return 'Invalid JSON';
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@ -86,7 +90,7 @@ const formattedJson = computed(() => {
|
||||
background-color: var(--sidebar);
|
||||
}
|
||||
|
||||
.tool-logger > span:first-child {
|
||||
.tool-logger>span:first-child {
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -95,7 +99,7 @@ const formattedJson = computed(() => {
|
||||
.tool-logger .output-content {
|
||||
border-radius: .5em;
|
||||
padding: 15px;
|
||||
min-height: 300px;
|
||||
min-height: 450px;
|
||||
height: fit-content;
|
||||
font-family: var(--code-font-family);
|
||||
white-space: pre-wrap;
|
||||
@ -106,4 +110,10 @@ const formattedJson = computed(() => {
|
||||
line-height: 1.5;
|
||||
background-color: var(--sidebar);
|
||||
}
|
||||
|
||||
.error-tool-call {
|
||||
background-color: rgba(245, 108, 108, 0.5);
|
||||
padding: 5px 9px;
|
||||
border-radius: .5em;
|
||||
}
|
||||
</style>
|
@ -1,4 +1,5 @@
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
import { mcpSetting } from '@/hook/mcp';
|
||||
import { ToolsListResponse, ToolCallResponse, CasualRestAPI } from '@/hook/type';
|
||||
import { pinkLog } from '@/views/setting/util';
|
||||
import { reactive } from 'vue';
|
||||
@ -11,7 +12,8 @@ export const toolsManager = reactive<{
|
||||
|
||||
export interface ToolStorage {
|
||||
currentToolName: string;
|
||||
lastToolCallResponse?: ToolCallResponse;
|
||||
lastToolCallResponse?: ToolCallResponse | string;
|
||||
formData: Record<string, any>;
|
||||
}
|
||||
|
||||
const bridge = useMessageBridge();
|
||||
@ -22,7 +24,7 @@ export function callTool(toolName: string, toolArgs: Record<string, any>) {
|
||||
console.log(data.msg);
|
||||
|
||||
if (data.code !== 200) {
|
||||
reject(new Error(data.msg + ''));
|
||||
resolve(data.msg);
|
||||
} else {
|
||||
resolve(data.msg);
|
||||
}
|
||||
@ -35,7 +37,10 @@ export function callTool(toolName: string, toolArgs: Record<string, any>) {
|
||||
command: 'tools/call',
|
||||
data: {
|
||||
toolName,
|
||||
toolArgs: JSON.parse(JSON.stringify(toolArgs))
|
||||
toolArgs: JSON.parse(JSON.stringify(toolArgs)),
|
||||
callToolOption: {
|
||||
timeout: mcpSetting.timeout * 1000
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="connected-status-container"
|
||||
id="connected-status-container"
|
||||
@click.stop="toggleConnectionPanel()"
|
||||
:class="{ 'connected': connectionResult.success }"
|
||||
>
|
||||
<span class="mcp-server-info">
|
||||
<el-tooltip
|
||||
@ -13,12 +15,14 @@
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<span class="connect-status">
|
||||
<span
|
||||
class="status-circle"
|
||||
:class="statusColorStyle"
|
||||
>
|
||||
<span v-if="connectionResult.success">
|
||||
<span class="iconfont icon-connect"></span>
|
||||
<span class="iconfont icon-dui"></span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span class="iconfont icon-connect"></span>
|
||||
<span class="iconfont icon-cuo"></span>
|
||||
</span>
|
||||
<span class="status-string">{{ statusString }}</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
@ -34,32 +38,34 @@ defineComponent({ name: 'connected' });
|
||||
|
||||
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(() => {
|
||||
return connectionResult.serverInfo.name + '/' + connectionResult.serverInfo.version;
|
||||
});
|
||||
|
||||
const displayServerName = computed(() => {
|
||||
if (connectionResult.serverInfo.name.length > 20) {
|
||||
return connectionResult.serverInfo.name.substring(0, 20);
|
||||
} else {
|
||||
return connectionResult.serverInfo.name;
|
||||
const name = connectionResult.serverInfo.name;
|
||||
if (name.length <= 3) return 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() {
|
||||
@ -69,9 +75,13 @@ function toggleConnectionPanel() {
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.connected .status-circle {
|
||||
background-color: var(--el-color-success) !important;
|
||||
}
|
||||
|
||||
.connected-color {
|
||||
background-color: #21DA49;
|
||||
.connected .connect-status {
|
||||
border: 1px solid var(--el-color-success) !important;
|
||||
color: var(--el-color-success) !important;
|
||||
}
|
||||
|
||||
.disconnected-color {
|
||||
@ -81,8 +91,8 @@ function toggleConnectionPanel() {
|
||||
.status-circle {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
margin-right: 8px;
|
||||
border-radius: 99%;
|
||||
background-color: var(--main-color);
|
||||
box-shadow: 0 0 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@ -104,7 +114,13 @@ function toggleConnectionPanel() {
|
||||
.connected-status-container .connect-status {
|
||||
display: flex;
|
||||
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 {
|
||||
@ -129,10 +145,15 @@ function toggleConnectionPanel() {
|
||||
.mcp-server-info .name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
max-width: 60px;
|
||||
white-space: wrap;
|
||||
width: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background-color: #f39a6d;
|
||||
padding: 5px;
|
||||
padding: 5px 12px;
|
||||
border-radius: .5em;
|
||||
color: #1e1e1e;
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="mcp-title">
|
||||
<div class="openmcp-logo" style="width: 48px; height: 48px; margin-top: 10px; margin-bottom: 15px; margin-right: 10px;"></div>
|
||||
<!-- <div>OpenMCP</div> -->
|
||||
<div class="simple-logo"
|
||||
@click="clickLogo"
|
||||
>
|
||||
<span class="iconfont icon-openmcp"></span>
|
||||
<span style="font-size: 12px;">openmcp</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -9,15 +13,43 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
defineComponent({ name: 'mcp-title' });
|
||||
|
||||
|
||||
function clickLogo() {
|
||||
window.open('https://kirigaya.cn/blog/article?seq=311', '_blank');
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.mcp-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.mcp-title > div {
|
||||
.mcp-title>div {
|
||||
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>
|
@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<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">
|
||||
<div class="sidebar-option-item" :class="{ 'active': isActive(item.ident) }"
|
||||
@click="gotoOption(item.ident)">
|
||||
|
@ -15,7 +15,6 @@ export function setDefaultCss() {
|
||||
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-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-light-8', 'var(--main-color)');
|
||||
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-bg-color', 'var(--background)');
|
||||
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)');
|
||||
|
||||
|
36
renderer/src/hook/mcp.ts
Normal 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,
|
||||
});
|
@ -1,9 +1,8 @@
|
||||
import { useMessageBridge } from "@/api/message-bridge";
|
||||
import { llmManager, llms } from "@/views/setting/llm";
|
||||
import { pinkLog } from "@/views/setting/util";
|
||||
import I18n from '@/i18n/index';
|
||||
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 {
|
||||
name: string;
|
||||
@ -21,6 +20,8 @@ interface SaveTab {
|
||||
export const panelLoaded = ref(false);
|
||||
|
||||
export function loadPanels() {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const bridge = useMessageBridge();
|
||||
|
||||
bridge.addCommandListener('panel/load', data => {
|
||||
@ -35,6 +36,8 @@ export function loadPanels() {
|
||||
|
||||
if (persistTab.tabs.length === 0) {
|
||||
// 空的,直接返回不需要管
|
||||
panelLoaded.value = true;
|
||||
resolve(void 0);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -42,12 +45,16 @@ export function loadPanels() {
|
||||
tabs.content = [];
|
||||
|
||||
for (const tab of persistTab.tabs || []) {
|
||||
|
||||
const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined;
|
||||
|
||||
tabs.content.push({
|
||||
id: uuidv4(),
|
||||
name: tab.name,
|
||||
icon: tab.icon,
|
||||
type: tab.type,
|
||||
componentIndex: tab.componentIndex,
|
||||
component: markRaw(debugModes[tab.componentIndex]),
|
||||
component: component,
|
||||
storage: tab.storage
|
||||
});
|
||||
}
|
||||
@ -56,11 +63,13 @@ export function loadPanels() {
|
||||
}
|
||||
|
||||
panelLoaded.value = true;
|
||||
resolve(void 0);
|
||||
}, { once: true });
|
||||
|
||||
bridge.postMessage({
|
||||
command: 'panel/load'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let debounceHandler: NodeJS.Timeout;
|
||||
@ -69,10 +78,15 @@ export function safeSavePanels() {
|
||||
clearTimeout(debounceHandler);
|
||||
debounceHandler = setTimeout(() => {
|
||||
savePanels();
|
||||
}, 1000);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
export function savePanels(saveHandler?: () => void) {
|
||||
// // 没有完成 panel 加载就不保存
|
||||
// if (!panelLoaded.value) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
const bridge = useMessageBridge();
|
||||
|
||||
const saveTabs: SaveTab = {
|
||||
|