Compare commits
2 Commits
b099d5695d
...
7b68504b1e
| Author | SHA1 | Date |
|---|---|---|
|
|
7b68504b1e | |
|
|
834db72ec4 |
|
|
@ -0,0 +1,222 @@
|
||||||
|
# MCP 服务器使用说明
|
||||||
|
|
||||||
|
TTRPG Tools 提供了一个 MCP (Model Context Protocol) 服务器,用于与 AI 助手集成,自动化生成卡牌内容。
|
||||||
|
|
||||||
|
## 命令结构
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ttrpg mcp [command]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 子命令
|
||||||
|
|
||||||
|
#### 1. `mcp serve` - 启动 MCP 服务器
|
||||||
|
|
||||||
|
启动 MCP 服务器,供 AI 助手连接使用。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用 stdio 传输(默认)
|
||||||
|
ttrpg mcp serve
|
||||||
|
|
||||||
|
# 指定传输方式
|
||||||
|
ttrpg mcp serve stdio
|
||||||
|
```
|
||||||
|
|
||||||
|
**MCP 客户端配置示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"ttrpg-card-generator": {
|
||||||
|
"command": "node",
|
||||||
|
"args": ["path/to/ttrpg-tools/dist/cli/index.js", "mcp", "serve"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. `mcp generate-card-deck` - 快速生成卡牌组
|
||||||
|
|
||||||
|
通过命令行快速生成卡牌组,无需 MCP 客户端。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ttrpg mcp generate-card-deck --name "卡牌名称" --output "./输出目录" [选项]
|
||||||
|
```
|
||||||
|
|
||||||
|
**选项:**
|
||||||
|
|
||||||
|
| 选项 | 说明 | 默认值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `--name <name>` | 卡牌组名称(必需) | - |
|
||||||
|
| `--output <dir>` | 输出目录(必需) | - |
|
||||||
|
| `-c, --count <number>` | 卡牌数量 | `10` |
|
||||||
|
| `--fields <fields>` | 字段列表(逗号分隔) | `name,type,cost,description` |
|
||||||
|
| `--description <desc>` | 卡牌组描述 | - |
|
||||||
|
| `--size <size>` | 卡牌尺寸 | `54x86` |
|
||||||
|
| `--grid <grid>` | 网格布局 | `5x8` |
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生成基础魔法物品卡牌
|
||||||
|
ttrpg mcp generate-card-deck --name "魔法物品" --output "./content"
|
||||||
|
|
||||||
|
# 生成自定义 NPC 卡牌
|
||||||
|
ttrpg mcp generate-card-deck \
|
||||||
|
--name "NPC Cards" \
|
||||||
|
--output "./content/npcs" \
|
||||||
|
--count 20 \
|
||||||
|
--fields "name,class,level,background" \
|
||||||
|
--description "快速生成游戏中的 NPC 角色"
|
||||||
|
|
||||||
|
# 生成塔罗牌组
|
||||||
|
ttrpg mcp generate-card-deck \
|
||||||
|
--name "Tarot Deck" \
|
||||||
|
--output "./content/tarot" \
|
||||||
|
--count 22 \
|
||||||
|
--fields "name,number,meaning" \
|
||||||
|
--size "70x120" \
|
||||||
|
--grid "3x4"
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP 工具:generate_card_deck
|
||||||
|
|
||||||
|
通过 MCP 协议,AI 助手可以调用 `generate_card_deck` 工具生成卡牌组。
|
||||||
|
|
||||||
|
### 工具参数
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必需 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `deck_name` | string | ✓ | 卡牌组名称 |
|
||||||
|
| `output_dir` | string | ✓ | 输出目录路径 |
|
||||||
|
| `card_count` | number | ✗ | 卡牌数量(默认:10) |
|
||||||
|
| `card_template` | object | ✗ | 卡牌模板定义 |
|
||||||
|
| `deck_config` | object | ✗ | md-deck 组件配置 |
|
||||||
|
| `description` | string | ✗ | 卡牌组描述 |
|
||||||
|
|
||||||
|
### card_template 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "字段名称(英文,用于 CSV 列名)",
|
||||||
|
"description": "字段描述",
|
||||||
|
"examples": ["示例值 1", "示例值 2"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"examples": [
|
||||||
|
{
|
||||||
|
"字段名称": "值 1",
|
||||||
|
"字段名称 2": "值 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### deck_config 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"size": "54x86",
|
||||||
|
"grid": "5x8",
|
||||||
|
"bleed": 1,
|
||||||
|
"padding": 2,
|
||||||
|
"shape": "rectangle",
|
||||||
|
"layers": "name:1,2-3,12 desc:1,4-8,10",
|
||||||
|
"back_layers": "back:1,2-8,12"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用示例(AI 助手)
|
||||||
|
|
||||||
|
**用户请求:**
|
||||||
|
> 帮我生成一个魔法物品卡牌组,包含 15 张卡牌,字段有名称、稀有度、效果描述
|
||||||
|
|
||||||
|
**AI 助手调用工具:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"deck_name": "魔法物品",
|
||||||
|
"output_dir": "./content",
|
||||||
|
"card_count": 15,
|
||||||
|
"card_template": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"description": "物品名称",
|
||||||
|
"examples": ["火球术卷轴", "治疗药水", "隐形斗篷"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rarity",
|
||||||
|
"description": "稀有度",
|
||||||
|
"examples": ["稀有", "普通", "珍贵"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "effect",
|
||||||
|
"description": "效果描述",
|
||||||
|
"examples": ["造成 5d6 火焰伤害", "恢复 2d4+2 生命值", "隐身 1 小时"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**工具返回:**
|
||||||
|
- 生成的 Markdown 文件路径
|
||||||
|
- 生成的 CSV 文件路径
|
||||||
|
- `:md-deck` 组件代码
|
||||||
|
|
||||||
|
## 输出文件
|
||||||
|
|
||||||
|
运行工具后会生成:
|
||||||
|
|
||||||
|
1. **Markdown 文件** (`{deck_name}.md`)
|
||||||
|
- 包含卡牌组标题和描述
|
||||||
|
- 嵌入 `:md-deck` 组件代码
|
||||||
|
- 使用说明
|
||||||
|
|
||||||
|
2. **CSV 文件** (`{deck_name}.csv`)
|
||||||
|
- 包含所有卡牌数据
|
||||||
|
- 支持 `{{字段名}}` 变量语法
|
||||||
|
- 可使用 front matter 添加共享属性
|
||||||
|
|
||||||
|
3. **组件代码**
|
||||||
|
- 可直接插入任何 Markdown 文件
|
||||||
|
- 格式:`:md-deck[./xxx.csv]{size="54x86" grid="5x8" ...}`
|
||||||
|
|
||||||
|
## 与 TTRPG Tools 集成
|
||||||
|
|
||||||
|
生成的卡牌组可以直接使用 TTRPG Tools 预览和编译:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 预览卡牌内容
|
||||||
|
ttrpg serve ./content
|
||||||
|
|
||||||
|
# 编译为静态网站
|
||||||
|
ttrpg compile ./content -o ./dist
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发说明
|
||||||
|
|
||||||
|
### 添加新的 MCP 工具
|
||||||
|
|
||||||
|
1. 在 `src/cli/tools/` 目录创建工具模块
|
||||||
|
2. 在 `src/cli/commands/mcp.ts` 中注册工具
|
||||||
|
3. 实现 `ListToolsRequestSchema` 和 `CallToolRequestSchema` 处理器
|
||||||
|
|
||||||
|
### 代码结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/cli/
|
||||||
|
├── commands/
|
||||||
|
│ ├── serve.ts
|
||||||
|
│ ├── compile.ts
|
||||||
|
│ └── mcp.ts # MCP 命令入口
|
||||||
|
├── tools/
|
||||||
|
│ └── generate-card-deck.ts # 卡牌生成工具
|
||||||
|
└── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 依赖
|
||||||
|
|
||||||
|
MCP 服务器使用 `@modelcontextprotocol/sdk` 包,已包含在项目依赖中。
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^0.5.0",
|
||||||
"@solidjs/router": "^0.15.0",
|
"@solidjs/router": "^0.15.0",
|
||||||
"@types/three": "^0.183.1",
|
"@types/three": "^0.183.1",
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
|
|
@ -2418,6 +2419,17 @@
|
||||||
"langium": "^4.0.0"
|
"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": {
|
"node_modules/@module-federation/error-codes": {
|
||||||
"version": "0.22.0",
|
"version": "0.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz",
|
"resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz",
|
||||||
|
|
@ -4397,6 +4409,15 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/call-bind-apply-helpers": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"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==",
|
"integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/convert-source-map": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
|
|
@ -5396,6 +5426,15 @@
|
||||||
"node": ">=0.4.0"
|
"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": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
|
@ -6147,6 +6186,26 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/http-proxy-agent": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
|
||||||
|
|
@ -9350,6 +9409,37 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
"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==",
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/shebang-command": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
|
@ -9730,6 +9826,15 @@
|
||||||
"node": ">=10"
|
"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": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
|
@ -9967,6 +10072,15 @@
|
||||||
"node": ">=8.0"
|
"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": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "4.1.4",
|
"version": "4.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
||||||
|
|
@ -10209,6 +10323,15 @@
|
||||||
"node": ">= 4.0.0"
|
"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": {
|
"node_modules/upath": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
|
||||||
|
|
@ -10668,6 +10791,15 @@
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^0.5.0",
|
||||||
"@solidjs/router": "^0.15.0",
|
"@solidjs/router": "^0.15.0",
|
||||||
"@types/three": "^0.183.1",
|
"@types/three": "^0.183.1",
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,291 @@
|
||||||
|
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;
|
||||||
|
cwd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mcpCommand = new Command('mcp')
|
||||||
|
.description('MCP 服务器 - 用于 AI 助手集成的工具协议')
|
||||||
|
.addCommand(
|
||||||
|
new Command('serve')
|
||||||
|
.description('启动 MCP 服务器')
|
||||||
|
.argument('[host]', '服务器地址', 'stdio')
|
||||||
|
.option('-p, --port <port>', 'HTTP 端口(仅 HTTP 传输)', '3001')
|
||||||
|
.option('--cwd <dir>', '工作目录(工具调用的相对路径基准)', process.cwd())
|
||||||
|
.action(mcpServeAction)
|
||||||
|
)
|
||||||
|
.addCommand(
|
||||||
|
new Command('generate-card-deck')
|
||||||
|
.description('生成卡牌组(快速命令)')
|
||||||
|
.requiredOption('--name <name>', '卡牌组名称')
|
||||||
|
.requiredOption('--output <dir>', '输出目录')
|
||||||
|
.option('-c, --count <number>', '卡牌数量', '10')
|
||||||
|
.option('--fields <fields>', '字段列表(逗号分隔)', 'name,type,cost,description')
|
||||||
|
.option('--description <desc>', '卡牌组描述')
|
||||||
|
.option('--size <size>', '卡牌尺寸', '54x86')
|
||||||
|
.option('--grid <grid>', '网格布局', '5x8')
|
||||||
|
.action(generateCardDeckAction)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 服务器启动处理函数
|
||||||
|
*/
|
||||||
|
async function mcpServeAction(host: string, options: MCPOptions) {
|
||||||
|
// 切换到指定的工作目录
|
||||||
|
const cwd = options.cwd || process.cwd();
|
||||||
|
process.chdir(cwd);
|
||||||
|
console.error(`MCP 服务器工作目录:${cwd}`);
|
||||||
|
|
||||||
|
// 动态导入 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: '输出目录路径(相对路径相对于 MCP 服务器工作目录)',
|
||||||
|
},
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import { serveCommand } from './commands/serve.js';
|
import { serveCommand } from './commands/serve.js';
|
||||||
import { compileCommand } from './commands/compile.js';
|
import { compileCommand } from './commands/compile.js';
|
||||||
|
import { mcpCommand } from './commands/mcp.js';
|
||||||
import type { ServeOptions, CompileOptions } from './types.js';
|
import type { ServeOptions, CompileOptions } from './types.js';
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
@ -25,4 +26,7 @@ program
|
||||||
.option('-o, --output <dir>', '输出目录', './dist/output')
|
.option('-o, --output <dir>', '输出目录', './dist/output')
|
||||||
.action(compileCommand);
|
.action(compileCommand);
|
||||||
|
|
||||||
|
program
|
||||||
|
.addCommand(mcpCommand);
|
||||||
|
|
||||||
program.parse();
|
program.parse();
|
||||||
|
|
|
||||||
|
|
@ -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<string, string>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}`
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue