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

引言

最近在一次上班过程中听到了产品经理的抱怨,后来一时兴起就给产品经理写了一篇基于腾讯云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-server
npm install -g live-server

# 2. 启动本地服务器
cd D:/2024code/image-maneger
live-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上,这一点操作还是很流畅的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csdn565973850

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值