写点什么

腾讯云 COS MCP Server + CodeBuddy ,让你的 idea 不止停留在想象中...

  • 2025-05-18
    北京
  • 本文字数:17031 字

    阅读完需:约 56 分钟

引言

最近在一次上班过程中听到了产品经理的抱怨,后来一时兴起就给产品经理写了一篇基于腾讯云 CodeBuddy 和 EdgeOne Pages MCP Server 帮助产品经理快速落地原型图的示例,给产品经理看后,产品经理表示很满意,在实现上没什么技术上的门槛,效果上远比其自身苦哈哈画两天原型图的效果要好很多,最重要是这个还很快,两句话搞定原型图。

过了两天,产品经理又来找我,说是想让我知道他制作一个图片管理系统,页面可以先简单点,有基础的图片上传功能、展示功能、删除功能就行,上传的图片可以保存在腾讯云 COS ,需要先做一个示例页面可以展示给客户效果,后期再补充登录等功能。那么有了需求,我们就开干吧。

本文使用的 COS MCP Server 已上架腾讯云 MCP 广场:https://cloud.tencent.com/developer/mcp?channel=ugc

名词介绍

本篇文章主要涉及 MCP、COS MCP Server、CodeBuddy、EdgeOne Pages MCP Server 等相关名词介绍,关于 CodeBuddy 的介绍,前面我们已经介绍很详细了,简单理解就是腾讯云 AI 代码助手。那么下面来一一介绍其他名词。

MCP

先来介绍一下什么是 MCP?MCP 是模型上下文协议(Model Context Protocol)的简称,是一个开源协议,由 Anthropic(Claude 开发公司)开发,旨在让大型语言模型(LLM)能够以标准化的方式连接到外部数据源和工具。有了 MCP 标准协议,就像给 AI 大模型装了一个 “万能接口”,让 AI 模型能够与不同的数据源和工具进行无缝交互。它就像 USB-C 接口一样,提供了一种标准化的方法,将 AI 模型连接到各种数据源和工具。同时,MCP 可以在不同的应用 / 服务之间保持上下文,增强整体自主执行任务的能力

COS MCP Server

一句话介绍,就是基于 MCP 协议的腾讯云 COS MCP Server,无需编码即可让大模型快速接入腾讯云存储 (COS) 和数据万象 (CI) 能力。可以通过使用其他 MCP 能力获取的文本/图片/视频/音频等数据,然后直接上传到 COS 云端存储,或者自动化将视频/图片/音频/文本等数据在云端处理,并转存到 COS 云端存储

Pages MCP Server

一句话介绍就是,一个用于将 HTML 内容部署到 EdgeOne Pages 并获取公开可访问 URL 的 MCP 服务。就是说我们可以通过 EdgeOne Pages MCP Server 将我们生成的页面或者指定的页面快速部署到 EdgeOne Pages 并生成公开访问链接,用户就可以通过公开访问链接,方便高效,就像这样

MCP Server 配置

在介绍了上面涉及到的 MCP 的名词之后,下面我们需要在我们的开发工具 VSCode 中先配置好我们的 COS MCP Server 以及 Pages MCP Server。下面我们来依次配置这两个 MCP Server。

COS MCP Server

在配置 COS MCP Server 之前,我们可以按照 MCP 广场腾讯云 COS MCP Server 的文档说明进行操作,先要获取腾讯云 COS 的密钥,SecretId / SecretKey,用于身份认证。

SecretId / SecretKey

访问 腾讯云密钥管理,在打开的访问管理控制台点击左侧菜单【API 密钥管理】-【新建密钥】,在弹出的创建 SecretKey 弹窗中,点击【复制】按钮复制并保存 SecretId / SecretKey ,然后勾选协议点击【确定】完成 SecretId / SecretKey 的创建

Bucket

后面需要创建腾讯云 COS 存储桶 Bucket,用于存放数据,相当于您的个人存储空间。访问 存储桶列表,在存储桶列表页面中点击【创建存储桶】, 输入 存储桶名称,选择所属地域后勾选协议,点击【下一步】,后面的配置默认就可以,完成存储桶的创建

存储桶创建完成后,点击存储桶名称进入存储桶 概览 页面,记录存储桶的基本信息,后面配置要用到

COS MCP Server 配置

然后回到我们的 VSCode 的 CodeBuddy 对话页面,点击 MCP

选择【MCP 市场】,找到我们需要的腾讯云 COS MCP Server,点击【安装】

在腾讯云 COS MCP Server 配置页面输入 Bucket、Region、DatasetName(非必填参数,数据智能检索操作需要此参数)、SecretId 、SecretKey 后点击【保存】

点击保存后完成 腾讯云 COS MCP Server 的配置,在腾讯云 COS MCP Server 下面可以看到具体支持的工具调用,我们可以直接点击腾讯云 COS MCP Server 右侧的 三角按钮

可以看到在我们的 Craft 对话框中会自动执行一条对话【TencentCloudCOSMCPServer MCP Server 已经安装完成,通过使用工具之一来演示服务器的功能】来验证腾讯云 COS MCP Server 的一些工具功能

并在验证结束的最后对本次已经验证的功能进行了一个简单的总结,方便我们快速知道对于腾讯云 COS MCP Server 一些常用工具的状态

Pages MCP Server

API Token

在配置 Pages MCP Server 之前,首先需要获取 API Token,如果 Pages MCP Server 没有配置 API Token 的话,是无法正常调用工具的。我们首先需要登录 EdgeOne Pages控制台 : 点击左侧菜单【Pages】切换到 【API Token】页面后,点击【创建 API Token】

在弹出的 API Token 设置页面输入 Token 描述以及选择过期时间后,点击【提交】后

在 API Token 列表页点击复制 API Token 并保存,然后按照上面给出的 MCP 配置文件路径更改 Craft_mcp_settings.json 的配置,增加 Pages MCP Server 配置内容后,整个配置文件完整内容


{  "mcpServers": {    "TencentCloudCOSMCPServer": {      "command": "npx",      "args": [        "cos-mcp"      ],      "env": {        "Bucket": "bucket-1302073945",        "DatasetName": "",        "Region": "ap-beijing",        "SecretId": "AKIDCH8pbmrkP3ZsxnPnz243tgS8ex4wZx85",        "SecretKey": "lIckeKp7B0av2vazA6mMjsnV7WhdKn0y"      }    },    "edgeone-pages-mcp-server": {      "command": "npx",      "args": ["edgeone-pages-mcp"],       "env": {             "EDGEONE_PAGES_API_TOKEN": "您的API令牌"       }    }  }}
复制代码

替换我们创建的 API Token 为上面的 API 令牌后,保存配置内容可以看到我们的两个 MCP Server 已经可以正常调用了

快速生成图片管理页面

上面的资源准备以及 MCP Server 都配置完成之后,就可以输入我们的需求了,这里我们需要生成一个图片管理页面,并且支持上传图片、查看图片、删除图片的功能,那么我们在 AI 对话框中输入

随后 Craft 会对我们的任务进行一个任务分析,确定技术方案,文件结构规划,实现步骤等内容,同时会按照实现步骤进行文件创建,

在对话最后 Craft 会主动与我们确认我们是否已经配置好腾讯云 COS 服务,我们给出【已配置好腾讯云 COS MCP Server】

在收到我们的回复之后,Craft 根据我们的回复开始调整具体 js 的功能方案,使用 MCP 工具与 COS 交互,并自动对 script.js 文件进行修改

随后自动完成了图片管理系统的创建,同时给出浏览器访问地址,以及图片管理系统的功能说明,注意事项等,同时对当前图片管理系统给出了后续改进建议,考虑的很长远

我们选择【全部接受】文件后,访问 Craft 给出的图片管理系统地址,下面我们就可以复制 Craft AI 对话框提供的访问地址进行访问了,但是访问报错了

这里其实也很容易猜到,这是因为我们当前页面并没有本地服务,因此通过本地访问是访问不到的。可以通过过去的方式,直接访问本地文件的绝对路径或者是通过我们的 Pages MCP Server 直接发布页面

页面发布

这里我们可以直接在 AI 对话框中输入通过 【使用 edgeone-pages-mcp-server 发布页面】然后查看效果

Craft 在收到我们的对话内容之后会自动分析目标并调用我们的 edgeone-pages-mcp-server 自动发布页面

发布成功后自动生成访问 url,我们可以直接复制 url 到浏览器访问查看效果

页面优化

这里我们可以看到我们的页面有点过于简单了,那么我们可以提出我们的详细需求,对页面进行美化,

再优化了页面之后我尝试上传图片,但是提示上传失败

我在对话框中输入我们的问题【上传图片提示 上传失败: Failed to fetch】CodeBuddy 在对错误原因进行分析后,会自动调用 MCP 服务测试服务连接问题

在确定了 COS MCP Server 服务连接没有问题之后会继续进行测试,测试上传的工具方法是否正常,本地自动创建测试文件自动调用上传方法进行测试,通过多轮次的自动测试后上传成功


在修改完成后接受了文件,并再次使用 edgeone-pages-mcp-server 自动发布页面 并获取页面访问链接,我们看到的结果其实页面还是没有连接上远程 COS MCP Server


script.js:182 加载图片错误: Error: 请求失败: 405 - <?xml version='1.0' encoding='utf-8' ?><Error>	<Code>MethodNotAllowed</Code>	<Message>The specified method is not allowed against this resource.</Message>	<Resource>/</Resource>	<RequestId>NjgyOTU3MzdfODhjZjExMGJfNTJlNF8yNGQzNmUy</RequestId>	<TraceId>OGVmYzZiMmQzYjA2OWNhODk0NTRkMTBiOWVmMDAxODc1NGE1MWY0MzY2NTg1MzM1OTY3MDliYzY2YTQ0ZThhMDJkOGMwMzhjMzdhNzA3OTQ1YzhiODI3YTdiMGY3Nzhk</TraceId></Error>

at loadImages (script.js:155:23)
复制代码

但是在 Craft 对话框中尝试获取腾讯云 COS 信息是可以自动调用腾讯云 COS MCP Server,在页面直接调用则不行

切换 COS-MCP

考虑到这种情况的话,那么我们不能直接安装 Craft MCP 广场的腾讯云 COS MCP Server,页面无法直接访问通过并调用,可以尝试腾讯云 COS MCP Server 的 SSE 模式,在 Craft 对话框执行命令


# 安装npm install -g cos-mcp@latest
复制代码


安装完成 cos-mcp 后运行开启 SSE 模式


# 运行开启 SSE 模式cos-mcp --Region=yourRegion --Bucket=yourBucket --SecretId=yourSecretId --SecretKey=yourSecretKey --DatasetName=yourDatasetname --port=3001 --connectType=sse
复制代码


安装完成后,在 MCP 配置文件中替换为新的腾讯云 COS MCP Server 的 SSE 模式的路径

下面继续更新我们页面的地址为新的 cos-mcp 的访问路径,在访问页面过程中,需要打开 F12 检查控制台,可以时时关注到页面的错误信息,就像下面的情况,我们可以不断的将页面的错误信息获取并通知 Craft 处理



我们可以不用懂为什么错误,直接将错误信息复制到 Craft ,Craft 会自动根据错误信息定位具体的 js 文件位置并解决,整个解决问题的过程比较漫长,不是说一个问题漫长,而是会出现各种各样的问题。总体的解决方案就是将错误信息复制到 Craft 进行排查处理,当看到错误信息出现

的时候,说明我们页面中请求地址还未被完全替换为新的 SSE 模式的地址,那么输入【替换请求中 MCP Server 为 SSE 模式地址】那么 Craft 在收到任务后会自动解析问题并进行解决

这里需要注意,当请求页面在本地时,可能存在跨域问题,需要将本地页面部署在本地,通过命令行


# 1. 安装live-servernpm install -g live-server
# 2. 启动本地服务器cd D:/2024code/image-manegerlive-server --port=8080
复制代码

然后本地启动后通过 http://127.0.0.1:8080/ 的方式访问地址,然后配置跨域访问相关内容,可以借助于 Craft 自动配置实现。最后的效果


相关源码

这里我们提供一下 Craft 生成的相关页面的源码,以及 css 、js 文件的源码

index.html

<!DOCTYPE html><html lang="zh-CN"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>图片管理系统</title>    <link rel="stylesheet" href="style.css"></head><body>    <div class="container">        <header class="app-header">            <h1>图片管理系统</h1>            <div class="upload-section">                <input type="file" id="fileInput" accept="image/*" multiple>                <button id="uploadBtn">上传图片</button>                <div id="uploadStatus"></div>            </div>        </header>                <main class="main-content">            <div class="gallery-section">                <h2>我的图片</h2>                <div id="imageGallery" class="image-gallery">                    <!-- 图片将通过JavaScript动态加载 -->                </div>            </div>        </main>                <!-- 图片详情模态框 -->        <div id="imageModal" class="modal">            <div class="modal-content">                <span class="close-modal">&times;</span>                <h3>图片详情</h3>                <div class="image-details">                    <div class="image-preview">                        <img id="modalImage" src="" alt="图片预览">                    </div>                    <div class="image-info">                        <p><strong>文件名:</strong> <span id="imageName"></span></p>                        <p><strong>大小:</strong> <span id="imageSize"></span></p>                        <p><strong>上传时间:</strong> <span id="imageDate"></span></p>                        <p><strong>URL:</strong> <span id="imageUrl"></span></p>                        <div class="image-actions">                            <button id="copyUrlBtn">复制链接</button>                            <button id="deleteImageBtn" class="delete-btn">删除图片</button>                        </div>                    </div>                </div>            </div>        </div>    </div>
<script src="script.js"></script> <div class="loader"> <div class="loader-spinner"></div> </div> </body></html>
复制代码

style.css

body {    font-family: 'Segoe UI', 'Helvetica Neue', sans-serif;    line-height: 1.6;    margin: 0;    padding: 0;    background: linear-gradient(135deg, #f5f7fa 0%, #e4e8f0 100%);    color: #2d3748;    min-height: 100vh;}
.loader { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(255,255,255,0.8); z-index: 1000; justify-content: center; align-items: center;}
.loader.active { display: flex;}
.loader-spinner { width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid #4299e1; border-radius: 50%; animation: spin 1s linear infinite;}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); }}
.container { max-width: 1200px; margin: 0 auto; padding: 20px; animation: fadeIn 0.5s ease-out;}
.app-header { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin-bottom: 30px;}
.app-header h1 { margin-bottom: 20px;}
.upload-section { display: flex; align-items: center; gap: 15px; padding: 15px; background: #f8fafc; border-radius: 8px;}
.upload-section input[type="file"] { flex: 1; padding: 8px; border: 2px dashed #cbd5e1; border-radius: 6px; background: white;}
.main-content { background: white; padding: 20px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);}
@keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); }}
h1, h2 { color: #2d3748; text-shadow: 0 1px 2px rgba(0,0,0,0.1);}
h1 { font-size: 2.5rem; margin-bottom: 1.5rem; position: relative; display: inline-block;}
h1::after { content: ''; position: absolute; bottom: -10px; left: 0; width: 60px; height: 4px; background: #4299e1; border-radius: 2px;}
.upload-section { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 30px;}
#fileInput { display: block; margin: 15px 0;}
button { background-color: #4299e1; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; font-weight: 500; letter-spacing: 0.5px; transition: all 0.3s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.1);}
button:hover { background-color: #3182ce; transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0,0,0,0.15);}
button:active { transform: translateY(0); box-shadow: 0 2px 3px rgba(0,0,0,0.1);}
#uploadStatus { margin-top: 10px; color: #27ae60; font-weight: bold;}
.image-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; margin-top: 20px; min-height: 300px;}
.empty-message { grid-column: 1 / -1; text-align: center; color: #64748b; padding: 40px; font-size: 18px; background: #f8fafc; border-radius: 8px;}
.image-item { position: relative; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);}
.image-item:hover { transform: translateY(-5px); box-shadow: 0 10px 20px rgba(0,0,0,0.1);}
.image-item img { width: 100%; height: 200px; object-fit: cover; display: block; transition: transform 0.3s ease;}
.image-item:hover img { transform: scale(1.03);}
.image-item .delete-btn { position: absolute; top: 10px; right: 10px; background-color: rgba(231, 76, 60, 0.9); color: white; border: none; width: 30px; height: 30px; border-radius: 50%; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; opacity: 0; transform: scale(0.8); transition: all 0.2s ease;}
.image-item:hover .delete-btn { opacity: 1; transform: scale(1);}
.image-item .delete-btn:hover { background-color: #c0392b; transform: scale(1.1) !important;}
/* 模态框样式 */.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 1000; opacity: 0; transition: opacity 0.3s ease;}
.modal.active { display: flex; opacity: 1; justify-content: center; align-items: center;}
.modal-content { background: white; padding: 30px; border-radius: 12px; width: 90%; max-width: 800px; max-height: 90vh; overflow-y: auto; position: relative; transform: translateY(-20px); transition: transform 0.3s ease;}
.modal.active .modal-content { transform: translateY(0);}
.close-modal { position: absolute; right: 20px; top: 20px; font-size: 24px; cursor: pointer; color: #64748b; transition: color 0.2s;}
.close-modal:hover { color: #334155;}
.image-details { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-top: 20px;}
.image-preview { width: 100%; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1);}
.image-preview img { width: 100%; height: auto; display: block;}
.image-info { padding: 20px; background: #f8fafc; border-radius: 8px;}
.image-info p { margin: 10px 0; color: #334155;}
.image-info strong { color: #1e293b; min-width: 100px; display: inline-block;}
.image-actions { margin-top: 20px; display: flex; gap: 10px;}
#copyUrlBtn { background-color: #4299e1;}
#deleteImageBtn { background-color: #ef4444;}
.image-item { cursor: pointer;}
@media (max-width: 768px) { .image-details { grid-template-columns: 1fr; } .upload-section { flex-direction: column; align-items: stretch; } .modal-content { padding: 20px; }}
复制代码

script.js

// 图片管理系统核心功能// SSE连接实现function connectSSE() {    return new Promise((resolve, reject) => {        const sseUrl = new URL('http://localhost:3001/sse');        sseUrl.searchParams.set('action', 'getBucket');        sseUrl.searchParams.set('prefix', 'images');                const sse = new EventSource(sseUrl);                sse.addEventListener('initialData', (event) => {            try {                const result = JSON.parse(event.data);                updateGallery(result.Contents);                sse.close();                resolve(result);            } catch (e) {                sse.close();                reject(e);            }        });                sse.onerror = () => {            sse.close();            reject(new Error('SSE连接错误'));        };                // 设置10秒超时        setTimeout(() => {            sse.close();            reject(new Error('SSE连接超时'));        }, 10000);    });}
// 轮询备选方案async function fetchWithPolling() { return new Promise((resolve, reject) => { const sseUrl = new URL('http://localhost:3001/sse'); sseUrl.searchParams.set('action', 'getBucket'); sseUrl.searchParams.set('prefix', 'images'); const sse = new EventSource(sseUrl); sse.addEventListener('initialData', (event) => { try { const result = JSON.parse(event.data); updateGallery(result.Contents); sse.close(); resolve(result); } catch (e) { sse.close(); reject(e); } }); sse.onerror = () => { sse.close(); reject(new Error('SSE连接错误')); }; setTimeout(() => { sse.close(); reject(new Error('SSE连接超时')); }, 10000); });}
// 加载图片列表async function loadImages() { try { document.querySelector('.loader').classList.add('active'); // 尝试SSE连接 try { return await connectSSE(); } catch (sseError) { console.warn('SSE连接失败,回退到轮询模式:', sseError); return await fetchWithPolling(); } } catch (error) { console.error('加载图片失败:', error); imageGallery.innerHTML = '<p class="error-message">加载失败: ' + error.message + '</p>'; throw error; } finally { document.querySelector('.loader').classList.remove('active'); }}
document.addEventListener('DOMContentLoaded', function() { const fileInput = document.getElementById('fileInput'); const uploadBtn = document.getElementById('uploadBtn'); const uploadStatus = document.getElementById('uploadStatus'); const imageGallery = document.getElementById('imageGallery'); // 初始化时加载图片 loadImages(); // 上传按钮点击事件 uploadBtn.addEventListener('click', async function() { const files = fileInput.files; if (files.length === 0) { uploadStatus.textContent = '请先选择图片文件'; return; } uploadStatus.textContent = '上传中...'; document.querySelector('.loader').classList.add('active'); try { // 上传每张图片 for (let i = 0; i < files.length; i++) { const file = files[i]; await uploadImage(file); } uploadStatus.textContent = `成功上传 ${files.length} 张图片`; // 重新加载图片列表 loadImages(); } catch (error) { uploadStatus.textContent = '上传失败: ' + error.message; console.error('上传错误:', error); } finally { document.querySelector('.loader').classList.remove('active'); } }); // 上传图片到COS (简化版) async function uploadImage(file) { return new Promise((resolve, reject) => { const sseUrl = new URL('http://localhost:3001/sse'); sseUrl.searchParams.set('action', 'putObject'); sseUrl.searchParams.set('fileName', file.name); sseUrl.searchParams.set('targetDir', 'images/'); const sse = new EventSource(sseUrl); sse.addEventListener('uploadComplete', async (event) => { try { const result = JSON.parse(event.data); if (result.error) { throw new Error(result.error); } // 上传文件内容 const content = await file.text(); const uploadResponse = await fetch('http://localhost:3001/upload', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName: file.name, content: content }) }); if (!uploadResponse.ok) { throw new Error('文件内容上传失败'); } sse.close(); alert(`文件 ${file.name} 上传成功!`); loadImages(); resolve(result); } catch (error) { sse.close(); reject(error); } }); sse.onerror = () => { sse.close(); reject(new Error('上传连接错误')); }; setTimeout(() => { sse.close(); reject(new Error('上传操作超时')); }, 30000); }) // 加载图片列表 async function loadImages() { try { document.querySelector('.loader').classList.add('active'); // 先尝试SSE连接 try { return await connectSSE(); } catch (sseError) { console.warn('SSE连接失败,回退到轮询模式:', sseError); return await fetchWithPolling(); } } catch (error) { console.error('加载图片失败:', error); imageGallery.innerHTML = '<p class="error-message">加载失败: ' + error.message + '</p>'; throw error; } finally { document.querySelector('.loader').classList.remove('active'); } }
// SSE连接实现 function connectSSE() { return new Promise((resolve, reject) => { const sseUrl = new URL('http://localhost:3001/sse'); sseUrl.searchParams.set('action', 'getBucket'); sseUrl.searchParams.set('prefix', 'images'); const sse = new EventSource(sseUrl); sse.addEventListener('initialData', (event) => { try { const result = JSON.parse(event.data); updateGallery(result.Contents); sse.close(); resolve(result); } catch (e) { sse.close(); reject(e); } }); sse.onerror = () => { sse.close(); reject(new Error('SSE连接错误')); }; // 设置10秒超时 setTimeout(() => { sse.close(); reject(new Error('SSE连接超时')); }, 10000); }); }
// 轮询备选方案 async function fetchWithPolling() { const response = await fetch('/mcp/use_mcp_tool', { method: 'GET', headers: { 'X-Server-Name': 'TencentCloudCOSMCPServer', 'X-Tool-Name': 'getBucket', 'X-Arguments': JSON.stringify({Prefix: 'images/'}) } }); const result = await response.json(); updateGallery(result.Contents); return result; }
// 更新图片库 function updateGallery(images) { imageGallery.innerHTML = ''; if (images?.length > 0) { images.forEach(image => { if (image.Key?.match(/\.(jpg|png|jpeg|gif|webp)$/i)) { createImageElement(image.Key); } }); } else { imageGallery.innerHTML = '<p class="empty-message">暂无图片,请上传</p>'; } } } // 创建图片元素 function createImageElement(imageKey) { try { const imageItem = document.createElement('div'); imageItem.className = 'image-item'; const img = document.createElement('img'); // 直接生成COS访问URL const imageUrl = `https://bucket-1302073945.cos.ap-beijing.myqcloud.com/${encodeURIComponent(imageKey)}`; img.src = imageUrl; img.alt = imageKey.split('/').pop(); img.loading = 'lazy'; // 启用懒加载 // 图片加载错误处理 img.onerror = () => { console.error('图片加载失败:', imageUrl); img.src = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="%23eee"/><text x="50" y="50" font-family="Arial" font-size="10" text-anchor="middle" fill="%23aaa">图片加载失败</text></svg>'; }; // 点击图片显示详情 img.addEventListener('click', () => showImageDetails(imageKey, imageUrl)); const deleteBtn = document.createElement('button'); deleteBtn.className = 'delete-btn'; deleteBtn.innerHTML = '×'; deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); deleteImage(imageKey); }); imageItem.appendChild(img); imageItem.appendChild(deleteBtn); imageGallery.appendChild(imageItem); } catch (error) { console.error('创建图片元素失败:', error); } }
// 显示图片详情 async function showImageDetails(imageKey, imageUrl) { const modal = document.getElementById('imageModal'); const modalImage = document.getElementById('modalImage'); const imageName = document.getElementById('imageName'); const imageSize = document.getElementById('imageSize'); const imageDate = document.getElementById('imageDate'); const imageUrlSpan = document.getElementById('imageUrl'); const deleteImageBtn = document.getElementById('deleteImageBtn'); // 设置图片和基本信息 modalImage.src = imageUrl; imageName.textContent = imageKey.split('/').pop(); imageUrlSpan.textContent = imageUrl; // 获取图片元信息 const sseUrl = new URL('http://localhost:3001/sse'); sseUrl.searchParams.set('action', 'imageInfo'); sseUrl.searchParams.set('objectKey', imageKey); const sse = new EventSource(sseUrl); sse.addEventListener('imageInfo', (event) => { try { const result = JSON.parse(event.data); if (result && !result.error) { imageSize.textContent = `${(result.size / 1024).toFixed(2)} KB`; imageDate.textContent = new Date(result.lastModified).toLocaleString(); } else { imageSize.textContent = '获取失败'; imageDate.textContent = '获取失败'; } } catch (error) { console.error('获取图片信息失败:', error); imageSize.textContent = '获取失败'; imageDate.textContent = '获取失败'; } finally { sse.close(); } }); sse.onerror = () => { console.error('获取图片信息连接错误'); imageSize.textContent = '获取失败'; imageDate.textContent = '获取失败'; sse.close(); }; setTimeout(() => { sse.close(); imageSize.textContent = '获取超时'; imageDate.textContent = '获取超时'; }, 5000); // 设置删除按钮事件 deleteImageBtn.onclick = (e) => { e.stopPropagation(); deleteImage(imageKey); modal.classList.remove('active'); }; // 设置复制链接按钮 document.getElementById('copyUrlBtn').onclick = () => { navigator.clipboard.writeText(imageUrl) .then(() => alert('链接已复制到剪贴板')) .catch(err => console.error('复制失败:', err)); }; // 关闭模态框事件 document.querySelector('.close-modal').onclick = () => { modal.classList.remove('active'); }; // 点击模态框外部关闭 modal.onclick = (e) => { if (e.target === modal) { modal.classList.remove('active'); } }; // 显示模态框 modal.classList.add('active'); } // 删除图片 async function deleteImage(imageKey) { const imageName = imageKey.split('/').pop(); if (!confirm(`确定要永久删除 "${imageName}" 吗?此操作不可撤销!`)) return; return new Promise((resolve, reject) => { // 显示删除中状态 const loadingIndicator = document.createElement('div'); loadingIndicator.className = 'deleting-indicator'; loadingIndicator.textContent = `正在删除 ${imageName}...`; document.body.appendChild(loadingIndicator); const sseUrl = new URL('http://localhost:3001/sse'); sseUrl.searchParams.set('action', 'deleteObject'); sseUrl.searchParams.set('objectKey', imageKey); const sse = new EventSource(sseUrl); sse.addEventListener('deleteComplete', (event) => { try { const result = JSON.parse(event.data); if (result.error) { throw new Error(result.error); } sse.close(); loadingIndicator.textContent = `"${imageName}" 已删除`; setTimeout(() => { loadingIndicator.remove(); }, 2000); // 关闭图片详情模态框(如果打开) const modal = document.getElementById('imageModal'); if (modal.classList.contains('active')) { modal.classList.remove('active'); } // 重新加载图片列表 loadImages(); resolve(result); } catch (error) { sse.close(); loadingIndicator.remove(); console.error('删除图片失败:', error); alert(`删除失败: ${error.message}`); reject(error); } }); sse.onerror = () => { sse.close(); loadingIndicator.remove(); reject(new Error('删除连接错误')); }; setTimeout(() => { sse.close(); loadingIndicator.remove(); reject(new Error('删除操作超时')); }, 15000); }); }});
复制代码

最后总结

到这里,整篇文章基本也就算写完了,其实生成页面的操作不复杂,复杂的是在页面上面自动配置的腾讯云 COS MCP Server 的调用工具一直连不上的问题,在解决这个问题期间又会遇到各种各样的问题。单是解决问题这块就耗时三个小时多,这里需要注意的是,在 AI 对话框返回的状态,可能并不是真实的执行状态

另外就是通过 AI 对话框响应执行的命令,总是会自动截断我自己开的 Craft 命令行操作界面,实际上我本地的 cos-mcp 是正常启动的,但是对话框命令自动执行后,就会停掉我自己开的 Craft 启动的 cos-mcp,这点不是很智能,正常情况下,对于 AI 对话框中需要执行命令的操作应该是单独打开命令行操作页面才对。另外就是对于 cos-mcp 启动后通过 SSE 访问请求不到的情况,问题给 Craft 之后,基本上 Craft 会进行多轮的类似下面的操作

就是说 Craft 也是会不断的进行各种情况的尝试,那么这个尝试的过程就是比较耗时的。

最后,对于 MCP Server 的配置以及调用操作还是很简单的,可能在页面直接调用 MCP Server 本身就会有各种各样的问题,正常情况下还是通过传统的 API 方式调用更快捷一些。但是在 AI 对话框页面,配置了 MCP Server 之后可以通过自然语言的方式调用不同的 MCP Server 还是很方面的,比如我们可以直接说【获取文件列表】

其实后面如果有一个可以直接跟随前端页面一起运行的 MCP Server 服务的话,那么就可以直接在页面调用 MCP Server 工具进行操作而不会有连接问题、跨域问题等一些问题了。在开发工具中的 MCP Server 在使用上,以及 AI 自动根据自然语言调用不同 MCP Server 上,这一点操作还是很流畅的。

发布于: 刚刚阅读数: 7
用户头像

让技术不再枯燥,让每一位技术人爱上技术 2022-07-22 加入

还未添加个人简介

评论

发布
暂无评论
腾讯云COS MCP Server + CodeBuddy ,让你的idea 不止停留在想象中..._MCP_六月的雨在InfoQ_InfoQ写作社区