openEuler-portal-mcp开发者指南:如何扩展自定义查询工具
openEuler-portal-mcp开发者指南如何扩展自定义查询工具【免费下载链接】openEuler-portal-mcpThe repository of openEuler portal MCP Server项目地址: https://gitcode.com/openeuler/openEuler-portal-mcp前往项目官网免费下载https://ar.openeuler.org/ar/openEuler-portal-mcp是一个基于Model Context ProtocolMCP的开源项目为Claude等AI工具提供openEuler官网相关信息的查询能力。本文将详细介绍如何扩展自定义查询工具帮助开发者快速上手并为项目添加新的功能模块。 项目架构概述openEuler-portal-mcp采用分层架构设计包含以下几个核心模块MCP协议层使用modelcontextprotocol/sdk实现标准MCP协议传输层支持Stdio本地连接和SSE远程连接两种模式核心层工具注册中心和请求路由分发工具层包含21个查询工具和操作工具服务层提供共享服务如文档版本缓存辅助层格式化工具和智能推荐系统️ 现有工具概览项目目前提供21个工具分为两大类查询类工具19个get_sig_infoSIG信息查询get_cve_infoCVE安全公告查询get_docs_info文档内容检索get_package_info软件包信息查询get_issue_info社区Issue查询get_pull_request_info社区PR查询操作类工具2个execute_user_operation用户操作执行需OPENEULER_TOKENexecute_forum_operation论坛用户操作执行需FORUM_TOKEN 创建新工具的完整步骤步骤1创建工具文件在src/tools/目录下创建新的工具文件例如src/tools/getMyCustomTool.jsimport { addField, addFields, addIndentedField, addIndentedFields, truncate, stripHtml } from ../utils/formatHelpers.js; import { appendRecommendation } from ../utils/toolRecommendations.js; // 数据源URL定义 const MY_API_URL https://api.example.com/data; // 缓存机制可选 let cachedData null; let cacheExpiry 0; const CACHE_DURATION 15 * 60 * 1000; // 15分钟 // 主工具函数 export async function getMyCustomTool(params) { const { query_type list, keyword } params; try { let result ; // 根据查询类型处理不同逻辑 switch (query_type) { case list: result await fetchListData(keyword); break; case detail: result await fetchDetailData(keyword); break; default: return 请指定有效的查询类型list列表或 detail详情; } // 添加智能推荐 result appendRecommendation(result, get_my_custom_tool); return result; } catch (error) { console.error(自定义工具执行错误:, error); return 查询失败: ${error.message}; } } // 辅助函数获取列表数据 async function fetchListData(keyword) { const now Date.now(); // 检查缓存 if (cachedData now cacheExpiry) { return formatListData(cachedData); } // 发起API请求 const response await fetch(MY_API_URL, { signal: AbortSignal.timeout(15000) // 15秒超时 }); if (!response.ok) { throw new Error(API请求失败: ${response.status}); } const data await response.json(); // 更新缓存 cachedData data; cacheExpiry now CACHE_DURATION; return formatListData(data); } // 辅助函数格式化列表数据 function formatListData(data) { let output ## 自定义数据列表\n\n; if (data.length 0) { return 未找到相关数据。; } data.forEach((item, index) { output ### ${index 1}. ${item.name}\n; output addField(描述, item.description || 暂无描述); output addField(状态, item.status || 未知); output addField(更新时间, item.updated_at || 未知); output \n; }); return output; } // 工具定义必须导出 export const toolDefinition { name: get_my_custom_tool, description: 查询自定义数据信息支持列表和详情查询, inputSchema: { type: object, properties: { query_type: { type: string, description: 查询类型list列表查询或 detail详情查询, enum: [list, detail], default: list }, keyword: { type: string, description: 搜索关键词可选 } } } };步骤2注册工具到主入口在src/index.js中添加新工具的导入和注册// 在导入部分添加 import { getMyCustomTool, toolDefinition as getMyCustomToolDef } from ./tools/getMyCustomTool.js; // 在工具注册部分添加 const tools [ // ... 现有工具 getMyCustomToolDef, ]; const toolHandlers { // ... 现有处理器 get_my_custom_tool: getMyCustomTool, };步骤3配置智能推荐在src/utils/toolRecommendations.js中添加新工具的推荐配置// 在toolRecommendations对象中添加 const toolRecommendations { // ... 现有配置 get_my_custom_tool: { related: [get_sig_info, get_organization_info, get_search_info], scenarios: [ 用户想查询自定义数据时, 需要了解特定信息时, 搜索相关数据时 ] }, // 在其他工具的推荐中添加新工具 get_sig_info: { related: [get_my_custom_tool, get_organization_info, get_meeting_info, get_development_info], // ... } };步骤4编写单元测试在tests/目录下创建测试文件getMyCustomTool.test.jsimport { describe, it, expect, beforeEach, afterEach, vi } from vitest; import { getMyCustomTool } from ../src/tools/getMyCustomTool.js; describe(getMyCustomTool, () { beforeEach(() { vi.useFakeTimers(); }); afterEach(() { vi.useRealTimers(); }); it(应该返回列表数据, async () { const result await getMyCustomTool({ query_type: list }); expect(result).toContain(自定义数据列表); }); it(应该处理API错误, async () { global.fetch vi.fn(() Promise.reject(new Error(网络错误))); const result await getMyCustomTool({ query_type: list }); expect(result).toContain(查询失败); }); }); 工具开发最佳实践1. 缓存策略设计项目采用三级缓存机制建议根据数据特性选择合适的缓存策略// 共享缓存跨工具使用 // 位置src/services/docsVersionService.js // 适用场景多个工具共享的数据如文档版本信息 // 本地缓存工具内部使用 // 适用场景单个工具专用的数据 const CACHE_DURATION 15 * 60 * 1000; // 15分钟 let cachedData null; let cacheExpiry 0; // 长期缓存24小时 // 适用场景用户信息等变化频率低的数据 const LONG_CACHE_DURATION 24 * 60 * 60 * 1000;2. 错误处理规范遵循统一的错误处理模式try { // 业务逻辑 const response await fetch(url, { signal: AbortSignal.timeout(15000) // 15秒超时 }); if (!response.ok) { throw new Error(API请求失败: ${response.status}); } // 数据处理 const data await response.json(); return formatData(data); } catch (error) { console.error(工具执行错误:, error); // 返回用户友好的错误信息 if (error.name TimeoutError) { return 请求超时请稍后重试。; } return 查询失败: ${error.message}; }3. 输出格式化标准使用项目提供的格式化工具确保输出一致性import { addField, addFields, addIndentedField, addIndentedFields, truncate, stripHtml } from ../utils/formatHelpers.js; function formatData(data) { let output ## 数据标题\n\n; // 添加字段 output addField(名称, data.name); output addField(描述, data.description); // 添加多行字段 output addIndentedField(详细信息, data.details); // 处理HTML内容 const cleanContent stripHtml(data.content); return output; }4. 智能推荐集成每个工具都应集成智能推荐系统import { appendRecommendation } from ../utils/toolRecommendations.js; async function myTool(params) { // ... 业务逻辑 let result formatData(data); // 添加推荐 result appendRecommendation(result, my_tool_name); return result; } 项目文件结构详解了解项目结构有助于更好地扩展功能openEuler-portal-mcp/ ├── src/ │ ├── index.js # 主入口文件工具注册中心 │ ├── services/ │ │ └── docsVersionService.js # 共享服务文档版本缓存 │ ├── tools/ # 工具函数目录21个工具 │ │ ├── getSigInfo.js # SIG信息查询 │ │ ├── getCveInfo.js # CVE安全公告查询 │ │ ├── getDocsInfo.js # 文档内容检索 │ │ ├── getDocsSearchContent.js # 文档内容搜索 │ │ ├── getPackageInfo.js # 软件包信息查询 │ │ ├── getIssueInfo.js # 社区Issue查询 │ │ ├── getPullRequestInfo.js # 社区PR查询 │ │ ├── executeUserOperation.js # 用户操作执行 │ │ └── executeForumOperation.js # 论坛用户操作执行 │ └── utils/ │ ├── formatHelpers.js # 输出格式化辅助函数 │ └── toolRecommendations.js # 智能推荐系统配置 ├── tests/ # 测试文件目录 │ ├── getSigInfo.test.js # SIG查询测试 │ ├── getCveInfo.test.js # CVE查询测试 │ └── ... # 其他工具测试 ├── docs/ │ ├── ARCHITECTURE.md # 项目架构文档 │ └── TOOL_SELECTION.md # 工具选择说明 └── package.json # 项目配置 工具间的协作模式1. 数据共享机制通过服务层实现工具间的数据共享// 在services目录下创建共享服务 // src/services/mySharedService.js export class MySharedService { static cache new Map(); static async getSharedData() { const cacheKey shared_data; const cached this.cache.get(cacheKey); if (cached Date.now() cached.expiry) { return cached.data; } // 获取数据 const data await fetchData(); // 更新缓存15分钟 this.cache.set(cacheKey, { data, expiry: Date.now() 15 * 60 * 1000 }); return data; } } // 在工具中使用共享服务 import { MySharedService } from ../services/mySharedService.js; async function myTool() { const sharedData await MySharedService.getSharedData(); // 使用共享数据 }2. 推荐链设计工具之间形成推荐链引导用户深入查询// 在toolRecommendations.js中配置推荐关系 const toolRecommendations { get_sig_info: { related: [get_meeting_info, get_organization_info, get_development_info], scenarios: [ 查询SIG信息后可能需要查看该SIG的会议安排, 了解SIG后可能需要查看社区组织架构, 对SIG感兴趣可能需要查看相关开发活动 ] }, get_meeting_info: { related: [get_sig_info, execute_user_operation, get_forum_info], scenarios: [ 查看会议后可能需要了解相关SIG信息, 如需参加会议可能需要执行用户操作, 会议讨论可能在论坛有相关帖子 ] } }; 测试与验证1. 单元测试编写为每个工具编写全面的单元测试// tests/getMyCustomTool.test.js import { describe, it, expect, beforeEach, afterEach, vi } from vitest; import { getMyCustomTool } from ../src/tools/getMyCustomTool.js; describe(getMyCustomTool工具测试, () { beforeEach(() { vi.useFakeTimers(); // 模拟fetch global.fetch vi.fn(); }); afterEach(() { vi.useRealTimers(); vi.restoreAllMocks(); }); describe(列表查询, () { it(应该成功返回数据列表, async () { const mockData [{ name: 测试数据, status: active }]; global.fetch.mockResolvedValue({ ok: true, json: () Promise.resolve(mockData) }); const result await getMyCustomTool({ query_type: list }); expect(result).toContain(自定义数据列表); expect(result).toContain(测试数据); }); it(应该处理空数据情况, async () { global.fetch.mockResolvedValue({ ok: true, json: () Promise.resolve([]) }); const result await getMyCustomTool({ query_type: list }); expect(result).toContain(未找到相关数据); }); }); describe(错误处理, () { it(应该处理网络错误, async () { global.fetch.mockRejectedValue(new Error(网络错误)); const result await getMyCustomTool({ query_type: list }); expect(result).toContain(查询失败); }); it(应该处理API错误响应, async () { global.fetch.mockResolvedValue({ ok: false, status: 500 }); const result await getMyCustomTool({ query_type: list }); expect(result).toContain(API请求失败); }); }); });2. 集成测试验证工具在MCP环境下的集成# 启动开发服务器 npm start # 测试工具调用 curl -X POST http://localhost:3000/message \ -H Content-Type: application/json \ -d { tool: get_my_custom_tool, params: { query_type: list } } 性能优化建议1. 缓存优化策略// 根据数据特性选择缓存时间 const CACHE_CONFIG { STATIC_DATA: 60 * 60 * 1000, // 1小时静态数据 DYNAMIC_DATA: 15 * 60 * 1000, // 15分钟动态数据 USER_DATA: 24 * 60 * 60 * 1000, // 24小时用户数据 }; // 实现智能缓存失效 class SmartCache { constructor(duration) { this.cache new Map(); this.duration duration; } get(key) { const item this.cache.get(key); if (!item) return null; if (Date.now() item.expiry) { this.cache.delete(key); return null; } return item.data; } set(key, data) { this.cache.set(key, { data, expiry: Date.now() this.duration }); } // 批量清理过期缓存 cleanup() { const now Date.now(); for (const [key, item] of this.cache.entries()) { if (now item.expiry) { this.cache.delete(key); } } } }2. 请求优化// 使用AbortSignal控制超时 const controller new AbortController(); const timeoutId setTimeout(() controller.abort(), 15000); try { const response await fetch(url, { signal: controller.signal, headers: { User-Agent: openEuler-portal-mcp/1.0, Accept: application/json } }); clearTimeout(timeoutId); // 处理响应 } catch (error) { clearTimeout(timeoutId); if (error.name AbortError) { throw new Error(请求超时); } throw error; } 工具间的依赖关系了解现有工具的依赖关系有助于设计新工具工具名称依赖的服务依赖的工具输出格式get_docs_infodocsVersionServiceget_docs_search_contentMarkdown文档get_docs_search_contentdocsVersionServiceget_docs_info结构化搜索结果execute_user_operation无get_meeting_info操作结果推荐get_development_info无get_issue_info,get_pull_request_info开发活动报告 实际扩展案例案例1添加新的API查询工具假设需要添加一个查询openEuler社区活动的新工具// src/tools/getCommunityEvents.js export async function getCommunityEvents(params) { const { event_type, date_range } params; // 实现逻辑... } export const toolDefinition { name: get_community_events, description: 查询openEuler社区活动信息, inputSchema: { type: object, properties: { event_type: { type: string, description: 活动类型meetup、webinar、hackathon, enum: [meetup, webinar, hackathon] }, date_range: { type: string, description: 时间范围upcoming、past、all } } } };案例2扩展现有工具功能在现有工具基础上添加新功能// 在getSigInfo.js中添加新功能 async function getSigStatistics(sigName, period) { // 添加SIG统计信息查询 const stats await fetchSigStats(sigName, period); let output ## ${sigName} SIG 统计信息${period}\n\n; output addField(活跃成员数, stats.active_members); output addField(PR数量, stats.pr_count); output addField(Issue数量, stats.issue_count); output addField(代码提交数, stats.commits); return output; } 学习资源与参考1. 核心参考文件项目架构文档docs/ARCHITECTURE.md - 详细的项目架构说明工具选择机制docs/TOOL_SELECTION.md - AI如何选择工具的机制主入口文件src/index.js - 工具注册和MCP服务器配置2. 最佳实践示例复杂工具示例src/tools/getSigInfo.js - 包含缓存、模糊匹配的完整实现API集成示例src/tools/getCveInfo.js - 外部API调用和错误处理用户操作示例src/tools/executeUserOperation.js - Token验证和用户操作3. 实用工具模块格式化工具src/utils/formatHelpers.js - 统一的输出格式化函数推荐系统src/utils/toolRecommendations.js - 智能推荐配置 常见问题与解决方案问题1工具无法被AI识别解决方案确保在src/index.js中正确导入和注册工具检查toolDefinition的name属性是否符合MCP命名规范验证inputSchema定义是否正确问题2API调用失败解决方案添加适当的超时控制15秒实现重试机制提供友好的错误信息问题3缓存不生效解决方案检查缓存时间戳逻辑确保缓存键的唯一性验证缓存清理机制问题4推荐系统不工作解决方案在toolRecommendations.js中正确配置推荐关系确保在工具函数中调用appendRecommendation检查推荐场景描述是否清晰 未来扩展方向1. 数据源扩展集成更多openEuler官方API支持第三方数据源添加离线数据支持2. 功能增强实现更复杂的查询条件添加数据可视化输出支持批量操作3. 性能优化实现增量更新添加压缩传输优化内存使用4. 用户体验改进添加更多上下文感知实现个性化推荐支持多语言输出 总结扩展openEuler-portal-mcp的自定义查询工具是一个系统化的过程需要遵循项目的架构规范和最佳实践。通过本文的指南您可以快速创建新工具按照标准模板创建工具文件正确集成到系统在主入口注册并配置推荐确保代码质量编写全面的单元测试优化性能合理使用缓存和错误处理提供良好用户体验集成智能推荐系统记住每个新工具都应该遵循单一职责原则实现适当的错误处理集成缓存机制提供清晰的文档包含完整的测试用例通过遵循这些指导原则您可以为openEuler-portal-mcp项目贡献高质量的工具扩展帮助更多开发者通过AI工具更好地访问openEuler社区资源。【免费下载链接】openEuler-portal-mcpThe repository of openEuler portal MCP Server项目地址: https://gitcode.com/openeuler/openEuler-portal-mcp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Kiran会话管理器社区贡献指南:如何参与开源项目开发

Kiran会话管理器社区贡献指南:如何参与开源项目开发

Kiran会话管理器社区贡献指南:如何参与开源项目开发 【免费下载链接】kiran-session-manager The session manager will load all necessary applications for a full-featured user session. 项目地址: https://gitcode.com/openeuler/kiran-session-manager …

2026/7/2 21:12:37阅读更多 →
openEuler sync-bot 与 CI/CD 集成:构建完整的自动化开发流水线

openEuler sync-bot 与 CI/CD 集成:构建完整的自动化开发流水线

openEuler sync-bot 与 CI/CD 集成:构建完整的自动化开发流水线 【免费下载链接】sync-bot A tool for handling synchronization between branches 项目地址: https://gitcode.com/openeuler/sync-bot 前往项目官网免费下载:https://ar.openeule…

2026/7/2 21:12:37阅读更多 →
conda-ecopkgs高级用法:多版本支持、依赖管理和环境隔离技巧

conda-ecopkgs高级用法:多版本支持、依赖管理和环境隔离技巧

conda-ecopkgs高级用法:多版本支持、依赖管理和环境隔离技巧 【免费下载链接】conda-ecopkgs This repo aims to manage the conda packages which support openEuler. 项目地址: https://gitcode.com/openeuler/conda-ecopkgs 前往项目官网免费下载&#xf…

2026/7/2 21:12:37阅读更多 →
3种专业方案彻底清理Windows系统组件:EdgeRemover高效卸载工具完整指南

3种专业方案彻底清理Windows系统组件:EdgeRemover高效卸载工具完整指南

3种专业方案彻底清理Windows系统组件:EdgeRemover高效卸载工具完整指南 【免费下载链接】EdgeRemover A PowerShell script that correctly uninstalls or reinstalls Microsoft Edge on Windows 10 & 11. 项目地址: https://gitcode.com/gh_mirrors/ed/EdgeR…

2026/7/2 22:37:56阅读更多 →
Java开发者专用:docx4j全栈办公文档处理资源包(含多语言教程、API文档与实战示例)

Java开发者专用:docx4j全栈办公文档处理资源包(含多语言教程、API文档与实战示例)

本文还有配套的精品资源,点击获取 简介:面向Java后端和企业级文档自动化场景,提供开箱即用的docx4j完整开发支持:涵盖Word/Excel/PPT三格式(.docx/.xlsx/.pptx)的深度生成、解析与模板渲染能力。内含最新…

2026/7/2 22:37:56阅读更多 →
基于Docker-Mailserver构建三层加密私有邮件服务器实战指南

基于Docker-Mailserver构建三层加密私有邮件服务器实战指南

1. 项目概述:为什么我们需要一个真正私有的邮件堡垒? 如果你还在用Gmail、Outlook或者QQ邮箱处理那些包含敏感信息的邮件,比如合同草稿、身份凭证、内部沟通,那你可能正在把你的数字隐私暴露在聚光灯下。商业邮件服务商的数据挖掘…

2026/7/2 22:37:56阅读更多 →
远程代码执行漏洞实战修复:从原理到应急响应全流程

远程代码执行漏洞实战修复:从原理到应急响应全流程

1. 项目概述:一次真实的远程代码执行漏洞修复实战最近在内部安全巡检中,我们团队发现并成功修复了一个影响范围不小的远程代码执行漏洞。这个漏洞的编号是CVE-2023-XXXX,它允许攻击者在特定条件下,通过构造恶意请求,在…

2026/7/2 22:37:56阅读更多 →
GetQzonehistory终极指南:如何用Python一键找回所有QQ空间记忆

GetQzonehistory终极指南:如何用Python一键找回所有QQ空间记忆

GetQzonehistory终极指南:如何用Python一键找回所有QQ空间记忆 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否还记得十年前在QQ空间写下的第一条说说?那些…

2026/7/2 22:37:56阅读更多 →
Python网络安全毕业设计:从流量分析到主动防护的实战指南

Python网络安全毕业设计:从流量分析到主动防护的实战指南

1. 项目概述与核心价值最近几年,计算机专业的毕业设计选题里,用Python做网络安全分析和防护的题目热度一直居高不下。这背后反映的,其实是行业需求和教学实践的一次深度结合。对于学生来说,这个选题的吸引力在于,它不像…

2026/7/2 22:32:55阅读更多 →
AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

AI Coding 六个月真实ROI账本:产品经理的血泪教训,研发的冷静忠告

6个月前的2025年12月,Boris Cherny 公开宣布自己卸载了 IDE。一时间,Vibe Coding 成了全行业最热的话题。6个月后,当我们回过头来拉一份真实账本,发现事情远没有"一句话生成一个App"那么浪漫。本文从产品经理和研发两个…

2026/7/2 12:10:34阅读更多 →
审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

审计来了,数据权限全开——审计走了,怎么确保权限全部关掉?

引言:审计结束三个月了,审计员的权限还没关某城商行每年按照监管要求开展至少一次数据安全审计。审计期间,内审部门需要抽样检查各类业务数据——交易流水、客户信息、员工操作日志、权限配置记录。这些数据分布在不同系统中,审计…

2026/7/2 12:10:34阅读更多 →
塞尔达传说旷野之息存档修改器:3分钟掌握海拉鲁世界自由定制技巧

塞尔达传说旷野之息存档修改器:3分钟掌握海拉鲁世界自由定制技巧

塞尔达传说旷野之息存档修改器:3分钟掌握海拉鲁世界自由定制技巧 【免费下载链接】BOTW-Save-Editor-GUI A Work in Progress Save Editor for BOTW 项目地址: https://gitcode.com/gh_mirrors/bo/BOTW-Save-Editor-GUI 想在《塞尔达传说:旷野之息…

2026/7/2 0:03:01阅读更多 →
告别 AccessKey:多云平台 CLI OAuth 免密认证完全指南

告别 AccessKey:多云平台 CLI OAuth 免密认证完全指南

在本地开发环境使用云厂商 CLI 时,传统的 AccessKey(AK)方式需要手动创建、下载和保管密钥,不仅繁琐,还存在泄漏风险。其实,主流云平台都已提供基于 OAuth 2.0 的免密认证方案,让开发者可以通过浏览器登录一次性完成授权,CLI 自动管理临时凭证的刷新,兼顾了便利与安全…

2026/7/2 0:03:01阅读更多 →
基于13DOF传感器与PIC32MZ的高精度嵌入式导航系统设计

基于13DOF传感器与PIC32MZ的高精度嵌入式导航系统设计

1. 项目背景与核心价值在嵌入式系统开发领域,高精度定位与导航一直是极具挑战性的技术方向。传统方案往往面临成本、精度和实时性难以兼顾的困境。这个项目通过13DOF(13自由度)传感器组合与PIC32MZ2048EFH100高性能MCU的协同工作,…

2026/7/2 0:03:01阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/2 0:33:58阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/2 1:32:11阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/2 1:50:13阅读更多 →