diff --git a/package-lock.json b/package-lock.json index abef164..386cc03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@modelcontextprotocol/sdk": "^0.5.0", "@solidjs/router": "^0.15.0", "@types/three": "^0.183.1", "chokidar": "^5.0.0", @@ -2418,6 +2419,17 @@ "langium": "^4.0.0" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.5.0.tgz", + "integrity": "sha512-RXgulUX6ewvxjAG0kOpLMEdXXWkzWgaoCGaA2CwNW7cQCIphjpJhjpHSiaPdVCnisjRF/0Cm9KWHUuIoeiAblQ==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "raw-body": "^3.0.0", + "zod": "^3.23.8" + } + }, "node_modules/@module-federation/error-codes": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", @@ -4397,6 +4409,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "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", @@ -4637,6 +4658,15 @@ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "license": "MIT" }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5396,6 +5426,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -6147,6 +6186,26 @@ "dev": true, "license": "MIT" }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -9350,6 +9409,37 @@ "dev": true, "license": "MIT" }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -9595,6 +9685,12 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9730,6 +9826,15 @@ "node": ">=10" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -9967,6 +10072,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -10209,6 +10323,15 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/upath": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", @@ -10668,6 +10791,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 89ddfa5..e477fd0 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "author": "", "license": "MIT", "dependencies": { + "@modelcontextprotocol/sdk": "^0.5.0", "@solidjs/router": "^0.15.0", "@types/three": "^0.183.1", "chokidar": "^5.0.0", diff --git a/src/cli/commands/mcp.ts b/src/cli/commands/mcp.ts new file mode 100644 index 0000000..c7bccf7 --- /dev/null +++ b/src/cli/commands/mcp.ts @@ -0,0 +1,284 @@ +import { Command } from 'commander'; +import { generateCardDeck, type GenerateCardDeckParams, type CardField } from '../tools/generate-card-deck.js'; + +/** + * MCP 服务器命令 + * + * 提供 MCP (Model Context Protocol) 服务器功能,用于与 AI 助手集成 + */ + +export interface MCPOptions { + port?: string; +} + +export const mcpCommand = new Command('mcp') + .description('MCP 服务器 - 用于 AI 助手集成的工具协议') + .addCommand( + new Command('serve') + .description('启动 MCP 服务器') + .argument('[host]', '服务器地址', 'stdio') + .option('-p, --port ', 'HTTP 端口(仅 HTTP 传输)', '3001') + .action(mcpServeAction) + ) + .addCommand( + new Command('generate-card-deck') + .description('生成卡牌组(快速命令)') + .requiredOption('--name ', '卡牌组名称') + .requiredOption('--output ', '输出目录') + .option('-c, --count ', '卡牌数量', '10') + .option('--fields ', '字段列表(逗号分隔)', 'name,type,cost,description') + .option('--description ', '卡牌组描述') + .option('--size ', '卡牌尺寸', '54x86') + .option('--grid ', '网格布局', '5x8') + .action(generateCardDeckAction) + ); + +/** + * MCP 服务器启动处理函数 + */ +async function mcpServeAction(host: string, options: MCPOptions) { + // 动态导入 MCP SDK + const { Server } = await import('@modelcontextprotocol/sdk/server/index.js'); + const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js'); + const { + CallToolRequestSchema, + ListToolsRequestSchema, + } = await import('@modelcontextprotocol/sdk/types.js'); + + const server = new Server( + { + name: 'ttrpg-card-generator', + version: '0.1.0', + }, + { + capabilities: { + tools: {}, + }, + } + ); + + // 处理工具列表请求 + server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'generate_card_deck', + description: '生成 TTRPG 卡牌组内容,包括 Markdown 介绍文件、CSV 数据文件和 md-deck 组件配置', + inputSchema: { + type: 'object', + properties: { + deck_name: { + type: 'string', + description: '卡牌组名称,将用于生成文件名', + }, + output_dir: { + type: 'string', + description: '输出目录路径', + }, + card_count: { + type: 'number', + description: '卡牌数量', + default: 10, + }, + card_template: { + type: 'object', + description: '卡牌模板定义', + properties: { + fields: { + type: 'array', + description: '卡牌字段列表', + items: { + type: 'object', + properties: { + name: { + type: 'string', + description: '字段名称(英文,用于 CSV 列名)', + }, + description: { + type: 'string', + description: '字段描述', + }, + examples: { + type: 'array', + description: '示例值列表', + items: { type: 'string' }, + }, + }, + required: ['name'], + }, + }, + examples: { + type: 'array', + description: '完整的卡牌示例数据', + items: { + type: 'object', + additionalProperties: { type: 'string' }, + }, + }, + }, + required: ['fields'], + }, + deck_config: { + type: 'object', + description: 'md-deck 组件配置', + properties: { + size: { + type: 'string', + description: '卡牌尺寸,格式 "宽 x 高"(单位 mm)', + default: '54x86', + }, + grid: { + type: 'string', + description: '网格布局,格式 "列 x 行"', + default: '5x8', + }, + bleed: { + type: 'number', + description: '出血边距(mm)', + default: 1, + }, + padding: { + type: 'number', + description: '内边距(mm)', + default: 2, + }, + shape: { + type: 'string', + description: '卡牌形状', + enum: ['rectangle', 'circle', 'hex', 'diamond'], + default: 'rectangle', + }, + layers: { + type: 'string', + description: '正面图层配置,格式 "字段:行,列范围,字体大小"', + }, + back_layers: { + type: 'string', + description: '背面图层配置', + }, + }, + }, + description: { + type: 'string', + description: '卡牌组的介绍描述(可选)', + }, + }, + required: ['deck_name', 'output_dir'], + }, + }, + ], + }; + }); + + // 处理工具调用请求 + server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params; + + if (name === 'generate_card_deck') { + try { + const params = args as unknown as GenerateCardDeckParams; + + // 验证必需参数 + if (!params.deck_name || !params.output_dir) { + return { + content: [ + { + type: 'text', + text: '错误:缺少必需参数 deck_name 或 output_dir', + }, + ], + isError: true, + }; + } + + // 生成卡牌组 + const result = generateCardDeck(params); + + return { + content: [ + { + type: 'text', + text: result.message, + }, + { + type: 'text', + text: `\n## 生成的组件代码\n\n\`\`\`markdown\n${result.deckComponent}\n\`\`\``, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: 'text', + text: `生成失败:${error instanceof Error ? error.message : '未知错误'}`, + }, + ], + isError: true, + }; + } + } + + return { + content: [ + { + type: 'text', + text: `未知工具:${name}`, + }, + ], + isError: true, + }; + }); + + // 启动服务器 + if (host === 'stdio') { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error('TTRPG 卡牌生成 MCP 服务器已启动(stdio)'); + } else { + // TODO: 支持 HTTP 传输 + console.error('HTTP 传输模式尚未实现,请使用 stdio 模式'); + process.exit(1); + } +} + +/** + * 快速生成卡牌组命令处理函数 + */ +async function generateCardDeckAction(options: { + name: string; + output: string; + count: string; + fields: string; + description?: string; + size: string; + grid: string; +}) { + const fieldNames = options.fields.split(',').map(f => f.trim()); + + const template = { + fields: fieldNames.map((name): CardField => ({ + name, + examples: ['示例 1', '示例 2'] + })) + }; + + const params: GenerateCardDeckParams = { + deck_name: options.name, + output_dir: options.output, + card_count: parseInt(options.count, 10), + card_template: template, + deck_config: { + size: options.size, + grid: options.grid + }, + description: options.description + }; + + const result = generateCardDeck(params); + + console.log('\n✅ 卡牌组生成成功!\n'); + console.log(result.message); + console.log('\n📦 组件代码:'); + console.log(` ${result.deckComponent}\n`); +} diff --git a/src/cli/index.ts b/src/cli/index.ts index 3d78669..942a0bf 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -2,6 +2,7 @@ import { Command } from 'commander'; import { serveCommand } from './commands/serve.js'; import { compileCommand } from './commands/compile.js'; +import { mcpCommand } from './commands/mcp.js'; import type { ServeOptions, CompileOptions } from './types.js'; const program = new Command(); @@ -25,4 +26,7 @@ program .option('-o, --output ', '输出目录', './dist/output') .action(compileCommand); +program + .addCommand(mcpCommand); + program.parse(); diff --git a/src/cli/tools/generate-card-deck.ts b/src/cli/tools/generate-card-deck.ts new file mode 100644 index 0000000..1da0446 --- /dev/null +++ b/src/cli/tools/generate-card-deck.ts @@ -0,0 +1,289 @@ +import { writeFileSync, mkdirSync, existsSync } from 'fs'; +import { join } from 'path'; + +/** + * 卡牌字段定义 + */ +export interface CardField { + name: string; + description?: string; + examples?: string[]; +} + +/** + * 卡牌模板配置 + */ +export interface CardTemplate { + fields: CardField[]; + examples?: Record[]; +} + +/** + * Deck 配置 + */ +export interface DeckConfig { + size?: string; + grid?: string; + bleed?: number; + padding?: number; + shape?: 'rectangle' | 'circle' | 'hex' | 'diamond'; + layers?: string; + backLayers?: string; +} + +/** + * 生成卡牌组的参数 + */ +export interface GenerateCardDeckParams { + deck_name: string; + output_dir: string; + card_count?: number; + card_template?: CardTemplate; + deck_config?: DeckConfig; + description?: string; +} + +/** + * 生成卡牌数据 CSV + */ +function generateCardCSV( + template: CardTemplate, + cardCount: number +): string { + const fields = template.fields; + + // 构建 CSV 表头 + const headers = ['label', ...fields.map(f => f.name), 'body']; + + // 生成示例数据 + const rows: string[][] = []; + const examples = template.examples || []; + + for (let i = 0; i < cardCount; i++) { + const row: string[] = [(i + 1).toString()]; + + // 为每个字段生成值 + for (const field of fields) { + let value = ''; + + if (examples.length > 0) { + // 从示例中循环取值 + const exampleIndex = i % examples.length; + const example = examples[exampleIndex]; + value = example[field.name] || field.examples?.[i % (field.examples?.length || 1)] || ''; + } else if (field.examples && field.examples.length > 0) { + // 从字段的示例中取值 + value = field.examples[i % field.examples.length]; + } else { + // 默认占位符 + value = `{{${field.name}_${i + 1}}}`; + } + + row.push(value); + } + + // body 列使用模板语法 + const bodyParts: string[] = []; + for (const field of fields) { + bodyParts.push(`**${field.name}:** {{${field.name}}}`); + } + row.push(bodyParts.join('\n\n')); + + rows.push(row); + } + + // 组合 CSV 内容 + const csvLines = [headers.join(',')]; + for (const row of rows) { + csvLines.push(row.map(cell => { + // 处理包含逗号或换行的单元格 + if (cell.includes(',') || cell.includes('\n') || cell.includes('"')) { + return `"${cell.replace(/"/g, '""')}"`; + } + return cell; + }).join(',')); + } + + return csvLines.join('\n'); +} + +/** + * 生成卡牌介绍的 Markdown 文件 + */ +function generateDeckMarkdown( + deckName: string, + csvFileName: string, + deckConfig: DeckConfig, + description?: string +): string { + const mdLines: string[] = []; + + // 标题 + mdLines.push(`# ${deckName}`); + mdLines.push(''); + + // 描述 + if (description) { + mdLines.push(description); + mdLines.push(''); + } + + // 卡牌预览组件 + mdLines.push('## 卡牌预览'); + mdLines.push(''); + + // 构建 :md-deck 组件代码 + const deckComponent = buildDeckComponent(csvFileName, deckConfig); + mdLines.push(deckComponent); + mdLines.push(''); + + // 使用说明 + mdLines.push('## 使用说明'); + mdLines.push(''); + mdLines.push('- 点击卡牌可以查看详情'); + mdLines.push('- 使用右上角的按钮可以随机抽取卡牌'); + mdLines.push('- 可以通过编辑面板调整卡牌样式和布局'); + mdLines.push(''); + + return mdLines.join('\n'); +} + +/** + * 构建 :md-deck 组件代码 + */ +function buildDeckComponent( + csvFileName: string, + config: DeckConfig +): string { + const parts = [`:md-deck[${csvFileName}]`]; + const attrs: string[] = []; + + if (config.size) { + attrs.push(`size="${config.size}"`); + } + + if (config.grid) { + attrs.push(`grid="${config.grid}"`); + } + + if (config.bleed !== undefined && config.bleed !== 1) { + attrs.push(`bleed="${config.bleed}"`); + } + + if (config.padding !== undefined && config.padding !== 2) { + attrs.push(`padding="${config.padding}"`); + } + + if (config.shape && config.shape !== 'rectangle') { + attrs.push(`shape="${config.shape}"`); + } + + if (config.layers) { + attrs.push(`layers="${config.layers}"`); + } + + if (config.backLayers) { + attrs.push(`back-layers="${config.backLayers}"`); + } + + if (attrs.length > 0) { + parts.push(`{${attrs.join(' ')}}`); + } + + return parts.join(''); +} + +/** + * 自动生成图层配置 + */ +function autoGenerateLayers(fields: CardField[]): string { + if (fields.length === 0) return ''; + + const layers: string[] = []; + const totalHeight = 8; + const heightPerField = Math.floor((totalHeight - 2) / fields.length); + + for (let i = 0; i < fields.length; i++) { + const field = fields[i]; + const y1 = 2 + i * heightPerField; + const y2 = y1 + heightPerField - 1; + const fontSize = Math.min(12, Math.floor(80 / fields.length)); + layers.push(`${field.name}:1,${y1}-${y2},${fontSize}`); + } + + return layers.join(' '); +} + +/** + * 生成卡牌组的主函数 + */ +export function generateCardDeck(params: GenerateCardDeckParams): { + mdFile: string; + csvFile: string; + deckComponent: string; + message: string; +} { + const { + deck_name, + output_dir, + card_count = 10, + card_template, + deck_config = {}, + description + } = params; + + // 确保输出目录存在 + if (!existsSync(output_dir)) { + mkdirSync(output_dir, { recursive: true }); + } + + // 生成文件名 + const safeName = deck_name.toLowerCase().replace(/\s+/g, '-'); + const csvFileName = `${safeName}.csv`; + const mdFileName = `${safeName}.md`; + + // 创建默认模板(如果没有提供) + const template: CardTemplate = card_template || { + fields: [ + { name: 'name', description: '卡牌名称', examples: ['示例卡牌 1', '示例卡牌 2'] }, + { name: 'type', description: '卡牌类型', examples: ['物品', '法术'] }, + { name: 'cost', description: '费用', examples: ['1', '2'] }, + { name: 'description', description: '效果描述', examples: ['这是一个效果描述', '这是另一个效果'] } + ] + }; + + // 创建默认配置(如果没有提供) + const config: DeckConfig = { + size: deck_config.size || '54x86', + grid: deck_config.grid || '5x8', + bleed: deck_config.bleed ?? 1, + padding: deck_config.padding ?? 2, + shape: deck_config.shape || 'rectangle', + layers: deck_config.layers || autoGenerateLayers(template.fields) + }; + + // 生成 CSV 内容 + const csvContent = generateCardCSV(template, card_count); + const csvPath = join(output_dir, csvFileName); + writeFileSync(csvPath, csvContent, 'utf-8'); + + // 生成 Markdown 内容 + const mdContent = generateDeckMarkdown( + deck_name, + `./${csvFileName}`, + config, + description + ); + const mdPath = join(output_dir, mdFileName); + writeFileSync(mdPath, mdContent, 'utf-8'); + + // 构建完整的 deck 组件代码 + const deckComponent = buildDeckComponent(`./${csvFileName}`, config); + + return { + mdFile: mdPath, + csvFile: csvPath, + deckComponent, + message: `已生成卡牌组 "${deck_name}":\n- Markdown 文件:${mdPath}\n- CSV 数据:${csvPath}\n- 组件代码:${deckComponent}` + }; +}