This commit is contained in:
2026-03-18 12:08:28 +08:00
parent eecf429a20
commit 62aff91a86
8 changed files with 1241 additions and 118 deletions
+252 -43
View File
@@ -1,9 +1,13 @@
import { Command } from 'commander';
import { generateCardDeck, type GenerateCardDeckParams, type CardField } from '../tools/generate-card-deck.js';
import { readFrontmatter, type ReadFrontmatterParams, type DeckFrontmatter } from '../tools/frontmatter/read-frontmatter.js';
import { writeFrontmatter, type WriteFrontmatterParams } from '../tools/frontmatter/write-frontmatter.js';
import { cardCrud, type CardCrudParams, type CardData } from '../tools/card/card-crud.js';
import { ensureDeckPreview, type EnsureDeckPreviewParams } from '../tools/ensure-deck-preview.js';
/**
* MCP 服务器命令
*
*
* 提供 MCP (Model Context Protocol) 服务器功能,用于与 AI 助手集成
*/
@@ -70,7 +74,7 @@ async function mcpServeAction(host: string, options: MCPOptions) {
tools: [
{
name: 'generate_card_deck',
description: '生成 TTRPG 卡牌组内容,包括 Markdown 介绍文件、CSV 数据文件和 md-deck 组件配置',
description: '生成 TTRPG 卡牌组内容,包括 Markdown 介绍文件、CSV 数据文件和 md-deck 组件配置(一站式快捷工具)',
inputSchema: {
type: 'object',
properties: {
@@ -173,6 +177,129 @@ async function mcpServeAction(host: string, options: MCPOptions) {
required: ['deck_name', 'output_dir'],
},
},
{
name: 'deck_frontmatter_read',
description: '读取 CSV 文件的 frontmatter(包含模板定义和 deck 配置)',
inputSchema: {
type: 'object',
properties: {
csv_file: {
type: 'string',
description: 'CSV 文件路径(相对路径相对于 MCP 服务器工作目录)',
},
},
required: ['csv_file'],
},
},
{
name: 'deck_frontmatter_write',
description: '写入/更新 CSV 文件的 frontmatter(模板定义和 deck 配置)',
inputSchema: {
type: 'object',
properties: {
csv_file: {
type: 'string',
description: 'CSV 文件路径',
},
frontmatter: {
type: 'object',
description: '要写入的 frontmatter 数据',
properties: {
fields: {
type: 'array',
description: '字段定义列表',
items: {
type: 'object',
properties: {
name: { type: 'string', description: '字段名称' },
description: { type: 'string', description: '字段描述' },
examples: { type: 'array', items: { type: 'string' }, description: '示例值列表' },
},
required: ['name'],
},
},
deck: {
type: 'object',
description: 'Deck 配置',
properties: {
size: { type: 'string', description: '卡牌尺寸,格式 "宽 x 高"' },
grid: { type: 'string', description: '网格布局,格式 "列 x 行"' },
bleed: { type: 'number', description: '出血边距(mm' },
padding: { type: 'number', description: '内边距(mm' },
shape: { type: 'string', enum: ['rectangle', 'circle', 'hex', 'diamond'] },
layers: { type: 'string', description: '正面图层配置' },
back_layers: { type: 'string', description: '背面图层配置' },
},
},
},
},
merge: {
type: 'boolean',
description: '是否合并现有 frontmatter(默认 true',
default: true,
},
},
required: ['csv_file', 'frontmatter'],
},
},
{
name: 'deck_card_crud',
description: '卡牌 CRUD 操作(创建/读取/更新/删除),支持批量操作',
inputSchema: {
type: 'object',
properties: {
csv_file: {
type: 'string',
description: 'CSV 文件路径',
},
action: {
type: 'string',
description: '操作类型',
enum: ['create', 'read', 'update', 'delete'],
},
cards: {
type: ['array', 'object'],
description: '卡牌数据(单张或数组)',
items: {
type: 'object',
additionalProperties: { type: 'string' },
},
},
label: {
type: ['string', 'array'],
description: '要操作的卡牌 label(用于 read/update/delete',
items: { type: 'string' },
},
},
required: ['csv_file', 'action'],
},
},
{
name: 'deck_ensure_preview',
description: '确保 CSV 对应的 Markdown 预览文件存在',
inputSchema: {
type: 'object',
properties: {
csv_file: {
type: 'string',
description: 'CSV 文件路径',
},
md_file: {
type: 'string',
description: 'Markdown 文件路径(可选,默认与 CSV 同名)',
},
title: {
type: 'string',
description: '标题(可选,默认从 CSV 文件名推断)',
},
description: {
type: 'string',
description: '描述(可选)',
},
},
required: ['csv_file'],
},
},
],
};
});
@@ -181,60 +308,142 @@ async function mcpServeAction(host: string, options: MCPOptions) {
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === 'generate_card_deck') {
try {
const params = args as unknown as GenerateCardDeckParams;
try {
switch (name) {
case 'generate_card_deck': {
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);
// 验证必需参数
if (!params.deck_name || !params.output_dir) {
return {
content: [
{
type: 'text',
text: '错误:缺少必需参数 deck_name 或 output_dir',
text: result.message,
},
{
type: 'text',
text: `\n## 生成的组件代码\n\n\`\`\`markdown\n${result.deckComponent}\n\`\`\``,
},
],
isError: true,
};
}
// 生成卡牌组(使用当前工作目录)
const result = generateCardDeck(params);
case 'deck_frontmatter_read': {
const params = args as unknown as ReadFrontmatterParams;
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,
};
if (!params.csv_file) {
return {
content: [{ type: 'text', text: '错误:缺少必需参数 csv_file' }],
isError: true,
};
}
const result = readFrontmatter(params);
return {
content: [
{
type: 'text',
text: result.message,
},
...(result.frontmatter ? [{
type: 'text',
text: `\n## Frontmatter\n\n\`\`\`json\n${JSON.stringify(result.frontmatter, null, 2)}\n\`\`\``,
}] : []),
],
};
}
case 'deck_frontmatter_write': {
const params = args as unknown as WriteFrontmatterParams;
if (!params.csv_file || !params.frontmatter) {
return {
content: [{ type: 'text', text: '错误:缺少必需参数 csv_file 或 frontmatter' }],
isError: true,
};
}
const result = writeFrontmatter(params);
return {
content: [{ type: 'text', text: result.success ? `${result.message}` : `${result.message}` }],
isError: !result.success,
};
}
case 'deck_card_crud': {
const params = args as unknown as CardCrudParams;
if (!params.csv_file || !params.action) {
return {
content: [{ type: 'text', text: '错误:缺少必需参数 csv_file 或 action' }],
isError: true,
};
}
const result = cardCrud(params);
return {
content: [
{
type: 'text',
text: result.success ? `${result.message}` : `${result.message}`,
},
...(result.cards && result.cards.length > 0 ? [{
type: 'text',
text: `\n## 卡牌数据\n\n\`\`\`json\n${JSON.stringify(result.cards, null, 2)}\n\`\`\``,
}] : []),
],
isError: !result.success,
};
}
case 'deck_ensure_preview': {
const params = args as unknown as EnsureDeckPreviewParams;
if (!params.csv_file) {
return {
content: [{ type: 'text', text: '错误:缺少必需参数 csv_file' }],
isError: true,
};
}
const result = ensureDeckPreview(params);
return {
content: [{ type: 'text', text: result.success ? `${result.message}` : `${result.message}` }],
isError: !result.success,
};
}
default:
return {
content: [{ type: 'text', text: `未知工具:${name}` }],
isError: true,
};
}
} catch (error) {
return {
content: [{ type: 'text', text: `工具调用失败:${error instanceof Error ? error.message : '未知错误'}` }],
isError: true,
};
}
return {
content: [
{
type: 'text',
text: `未知工具:${name}`,
},
],
isError: true,
};
});
// 启动服务器
+301
View File
@@ -0,0 +1,301 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { parse } from 'csv-parse/browser/esm/sync';
import { stringify } from 'csv-stringify/browser/esm/sync';
import yaml from 'js-yaml';
import type { DeckFrontmatter } from '../frontmatter/read-frontmatter.js';
/**
* 卡牌数据
*/
export interface CardData {
label?: string;
[key: string]: string | undefined;
}
/**
* 卡牌 CRUD 参数
*/
export interface CardCrudParams {
/**
* CSV 文件路径
*/
csv_file: string;
/**
* 操作类型
*/
action: 'create' | 'read' | 'update' | 'delete';
/**
* 卡牌数据(单张或数组)
*/
cards?: CardData | CardData[];
/**
* 要读取/更新的卡牌 label(用于 read/update/delete
*/
label?: string | string[];
}
/**
* 卡牌 CRUD 结果
*/
export interface CardCrudResult {
success: boolean;
message: string;
cards?: CardData[];
count?: number;
}
/**
* 解析 CSV 文件的 frontmatter
*/
function parseFrontMatter(content: string): { frontmatter?: DeckFrontmatter; csvContent: string } {
const parts = content.trim().split(/(?:^|\n)---\s*\n/g);
if (parts.length !== 3 || parts[0] !== '') {
return { csvContent: content };
}
try {
const frontmatterStr = parts[1].trim();
const frontmatter = yaml.load(frontmatterStr) as DeckFrontmatter | undefined;
const csvContent = parts.slice(2).join('---\n').trimStart();
return { frontmatter, csvContent };
} catch (error) {
console.warn('Failed to parse front matter:', error);
return { csvContent: content };
}
}
/**
* 序列化 frontmatter 为 YAML 字符串
*/
function serializeFrontMatter(frontmatter: DeckFrontmatter): string {
const yamlStr = yaml.dump(frontmatter, {
indent: 2,
lineWidth: -1,
noRefs: true,
quotingType: '"',
forceQuotes: false
});
return `---\n${yamlStr}---\n`;
}
/**
* 加载 CSV 数据(包含 frontmatter
*/
function loadCSVWithFrontmatter(filePath: string): {
frontmatter?: DeckFrontmatter;
records: CardData[];
headers: string[];
} {
const content = readFileSync(filePath, 'utf-8');
const { frontmatter, csvContent } = parseFrontMatter(content);
const records = parse(csvContent, {
columns: true,
comment: '#',
trim: true,
skipEmptyLines: true
}) as CardData[];
// 获取表头
const firstLine = csvContent.split('\n')[0];
const headers = firstLine.split(',').map(h => h.trim());
return { frontmatter, records, headers };
}
/**
* 保存 CSV 数据(包含 frontmatter
*/
function saveCSVWithFrontmatter(
filePath: string,
frontmatter: DeckFrontmatter | undefined,
records: CardData[],
headers?: string[]
): void {
// 序列化 frontmatter
const frontmatterStr = frontmatter ? serializeFrontMatter(frontmatter) : '';
// 确定表头
if (!headers || headers.length === 0) {
// 从 records 和 frontmatter.fields 推断表头
headers = ['label'];
if (frontmatter?.fields && Array.isArray(frontmatter.fields)) {
for (const field of frontmatter.fields) {
if (field.name && typeof field.name === 'string') {
headers.push(field.name);
}
}
}
headers.push('body');
}
// 确保所有 record 都有 headers 中的列
for (const record of records) {
for (const header of headers) {
if (!(header in record)) {
record[header] = '';
}
}
}
// 序列化 CSV
const csvContent = stringify(records, {
header: true,
columns: headers
});
// 写入文件
writeFileSync(filePath, frontmatterStr + csvContent, 'utf-8');
}
/**
* 生成下一个 label
*/
function generateNextLabel(records: CardData[]): string {
const maxLabel = records.reduce((max, record) => {
const label = record.label ? parseInt(record.label, 10) : 0;
return label > max ? label : max;
}, 0);
return (maxLabel + 1).toString();
}
/**
* 卡牌 CRUD 操作
*/
export function cardCrud(params: CardCrudParams): CardCrudResult {
const { csv_file, action, cards, label } = params;
// 检查文件是否存在(create 操作可以不存在)
if (action !== 'create' && !existsSync(csv_file)) {
return {
success: false,
message: `文件不存在:${csv_file}`
};
}
try {
let frontmatter: DeckFrontmatter | undefined;
let records: CardData[] = [];
let headers: string[] = [];
// 加载现有数据
if (existsSync(csv_file)) {
const data = loadCSVWithFrontmatter(csv_file);
frontmatter = data.frontmatter;
records = data.records;
headers = data.headers;
}
// 执行操作
switch (action) {
case 'create': {
const newCards = Array.isArray(cards) ? cards : (cards ? [cards] : []);
for (const card of newCards) {
if (!card.label) {
card.label = generateNextLabel(records);
}
records.push(card);
}
saveCSVWithFrontmatter(csv_file, frontmatter, records, headers);
return {
success: true,
message: `成功创建 ${newCards.length} 张卡牌`,
cards: newCards,
count: newCards.length
};
}
case 'read': {
const labelsToRead = Array.isArray(label) ? label : (label ? [label] : null);
let resultCards: CardData[];
if (labelsToRead && labelsToRead.length > 0) {
resultCards = records.filter(r => labelsToRead.includes(r.label || ''));
} else {
resultCards = records;
}
return {
success: true,
message: `成功读取 ${resultCards.length} 张卡牌`,
cards: resultCards,
count: resultCards.length
};
}
case 'update': {
const labelsToUpdate = Array.isArray(label) ? label : (label ? [label] : null);
const updateCards = Array.isArray(cards) ? cards : (cards ? [cards] : []);
let updatedCount = 0;
if (labelsToUpdate && labelsToUpdate.length > 0) {
// 按 label 更新
for (const updateCard of updateCards) {
const targetLabel = updateCard.label || labelsToUpdate[updatedCount % labelsToUpdate.length];
const index = records.findIndex(r => r.label === targetLabel);
if (index !== -1) {
records[index] = { ...records[index], ...updateCard };
updatedCount++;
}
}
} else {
// 按 cards 中的 label 更新
for (const updateCard of updateCards) {
if (updateCard.label) {
const index = records.findIndex(r => r.label === updateCard.label);
if (index !== -1) {
records[index] = { ...records[index], ...updateCard };
updatedCount++;
}
}
}
}
saveCSVWithFrontmatter(csv_file, frontmatter, records, headers);
return {
success: true,
message: `成功更新 ${updatedCount} 张卡牌`,
cards: updateCards,
count: updatedCount
};
}
case 'delete': {
const labelsToDelete = Array.isArray(label) ? label : (label ? [label] : null);
let deletedCount = 0;
if (labelsToDelete && labelsToDelete.length > 0) {
const beforeCount = records.length;
records = records.filter(r => !labelsToDelete.includes(r.label || ''));
deletedCount = beforeCount - records.length;
} else if (cards) {
const cardsToDelete = Array.isArray(cards) ? cards : [cards];
const beforeCount = records.length;
records = records.filter(r =>
!cardsToDelete.some(c => c.label && r.label === c.label)
);
deletedCount = beforeCount - records.length;
}
saveCSVWithFrontmatter(csv_file, frontmatter, records, headers);
return {
success: true,
message: `成功删除 ${deletedCount} 张卡牌`,
count: deletedCount
};
}
default:
return {
success: false,
message: `未知操作:${action}`
};
}
} catch (error) {
return {
success: false,
message: `操作失败:${error instanceof Error ? error.message : '未知错误'}`
};
}
}
+217
View File
@@ -0,0 +1,217 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { dirname, join, basename, extname } from 'path';
import yaml from 'js-yaml';
import type { DeckFrontmatter, DeckConfig } from './frontmatter/read-frontmatter.js';
/**
* 确保 Deck 预览文件的参数
*/
export interface EnsureDeckPreviewParams {
/**
* CSV 文件路径
*/
csv_file: string;
/**
* Markdown 文件路径(可选,默认与 CSV 同名)
*/
md_file?: string;
/**
* 标题(可选,默认从 CSV 文件名推断)
*/
title?: string;
/**
* 描述(可选)
*/
description?: string;
}
/**
* 确保 Deck 预览文件的结果
*/
export interface EnsureDeckPreviewResult {
success: boolean;
message: string;
md_file?: string;
created?: boolean;
}
/**
* 解析 CSV 文件的 frontmatter
*/
function parseFrontMatter(content: string): { frontmatter?: DeckFrontmatter; csvContent: string } {
const parts = content.trim().split(/(?:^|\n)---\s*\n/g);
if (parts.length !== 3 || parts[0] !== '') {
return { csvContent: content };
}
try {
const frontmatterStr = parts[1].trim();
const frontmatter = yaml.load(frontmatterStr) as DeckFrontmatter | undefined;
const csvContent = parts.slice(2).join('---\n').trimStart();
return { frontmatter, csvContent };
} catch (error) {
console.warn('Failed to parse front matter:', error);
return { csvContent: content };
}
}
/**
* 构建 md-deck 组件代码
*/
function buildDeckComponent(csvPath: string, config?: DeckConfig): string {
const parts = [`:md-deck[${csvPath}]`];
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?.back_layers) {
attrs.push(`back-layers="${config.back_layers}"`);
}
if (attrs.length > 0) {
parts.push(`{${attrs.join(' ')}}`);
}
return parts.join('');
}
/**
* 确保 Markdown 预览文件存在
*/
export function ensureDeckPreview(params: EnsureDeckPreviewParams): EnsureDeckPreviewResult {
const { csv_file, md_file, title, description } = params;
// 检查 CSV 文件是否存在
if (!existsSync(csv_file)) {
return {
success: false,
message: `CSV 文件不存在:${csv_file}`
};
}
// 确定 Markdown 文件路径
let mdFilePath = md_file;
if (!mdFilePath) {
// 使用与 CSV 相同的路径和文件名,只是扩展名不同
const dir = dirname(csv_file);
const name = basename(csv_file, extname(csv_file));
mdFilePath = join(dir, `${name}.md`);
}
const created = !existsSync(mdFilePath);
// 如果文件已存在,检查是否已有 md-deck 组件
if (!created) {
try {
const existingContent = readFileSync(mdFilePath, 'utf-8');
// 如果已有 md-deck 组件引用该 CSV,直接返回
if (existingContent.includes(`:md-deck[${csv_file}]`) ||
existingContent.includes(`:md-deck[./${basename(csv_file)}]`)) {
return {
success: true,
message: `预览文件 ${mdFilePath} 已存在`,
md_file: mdFilePath,
created: false
};
}
} catch (error) {
// 读取失败,继续创建
}
}
// 读取 CSV 的 frontmatter 获取配置
let deckConfig: DeckConfig | undefined;
try {
const csvContent = readFileSync(csv_file, 'utf-8');
const { frontmatter } = parseFrontMatter(csvContent);
deckConfig = frontmatter?.deck;
} catch (error) {
// 忽略错误,使用默认配置
}
// 确定标题
let deckTitle = title;
if (!deckTitle) {
// 从 CSV 文件名推断
const name = basename(csv_file, extname(csv_file));
deckTitle = name.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
}
// 构建 md-deck 组件代码(使用相对路径)
const mdDir = dirname(mdFilePath);
const csvDir = dirname(csv_file);
let relativeCsvPath: string;
if (mdDir === csvDir) {
relativeCsvPath = `./${basename(csv_file)}`;
} else {
relativeCsvPath = `./${basename(csv_file)}`;
}
const deckComponent = buildDeckComponent(relativeCsvPath, deckConfig);
// 生成 Markdown 内容
const mdLines: string[] = [];
mdLines.push(`# ${deckTitle}`);
mdLines.push('');
if (description) {
mdLines.push(description);
mdLines.push('');
}
mdLines.push('## 卡牌预览');
mdLines.push('');
mdLines.push(deckComponent);
mdLines.push('');
mdLines.push('## 使用说明');
mdLines.push('');
mdLines.push('- 点击卡牌可以查看详情');
mdLines.push('- 使用右上角的按钮可以随机抽取卡牌');
mdLines.push('- 可以通过编辑面板调整卡牌样式和布局');
mdLines.push('');
// 写入文件
try {
writeFileSync(mdFilePath, mdLines.join('\n'), 'utf-8');
return {
success: true,
message: created
? `创建预览文件 ${mdFilePath}`
: `更新预览文件 ${mdFilePath}`,
md_file: mdFilePath,
created
};
} catch (error) {
return {
success: false,
message: `写入失败:${error instanceof Error ? error.message : '未知错误'}`
};
}
}
@@ -0,0 +1,127 @@
import { readFileSync, existsSync } from 'fs';
import yaml from 'js-yaml';
/**
* 读取 CSV frontmatter 的参数
*/
export interface ReadFrontmatterParams {
/**
* CSV 文件路径(相对路径相对于 MCP 服务器工作目录)
*/
csv_file: string;
}
/**
* Frontmatter 数据结构
*/
export interface DeckFrontmatter {
/**
* 字段定义
*/
fields?: CardField[];
/**
* Deck 配置
*/
deck?: DeckConfig;
/**
* 其他自定义属性
*/
[key: string]: unknown;
}
/**
* 卡牌字段定义
*/
export interface CardField {
name: string;
description?: string;
examples?: string[];
[key: string]: unknown;
}
/**
* Deck 配置
*/
export interface DeckConfig {
size?: string;
grid?: string;
bleed?: number;
padding?: number;
shape?: 'rectangle' | 'circle' | 'hex' | 'diamond';
layers?: string;
back_layers?: string;
[key: string]: unknown;
}
/**
* 读取 CSV frontmatter 的结果
*/
export interface ReadFrontmatterResult {
success: boolean;
frontmatter?: DeckFrontmatter;
message: string;
}
/**
* 解析 CSV 文件的 frontmatter
*/
function parseFrontMatter(content: string): { frontmatter?: DeckFrontmatter; csvContent: string } {
const parts = content.trim().split(/(?:^|\n)---\s*\n/g);
// 至少需要三个部分:空字符串、front matter、CSV 内容
if (parts.length !== 3 || parts[0] !== '') {
return { csvContent: content };
}
try {
const frontmatterStr = parts[1].trim();
const frontmatter = yaml.load(frontmatterStr) as DeckFrontmatter | undefined;
const csvContent = parts.slice(2).join('---\n').trimStart();
return { frontmatter, csvContent };
} catch (error) {
console.warn('Failed to parse front matter:', error);
return { csvContent: content };
}
}
/**
* 读取 CSV 文件的 frontmatter
*/
export function readFrontmatter(params: ReadFrontmatterParams): ReadFrontmatterResult {
const { csv_file } = params;
// 检查文件是否存在
if (!existsSync(csv_file)) {
return {
success: false,
message: `文件不存在:${csv_file}`
};
}
try {
// 读取文件内容
const content = readFileSync(csv_file, 'utf-8');
// 解析 frontmatter
const { frontmatter } = parseFrontMatter(content);
if (!frontmatter) {
return {
success: true,
frontmatter: {},
message: `文件 ${csv_file} 没有 frontmatter,返回空对象`
};
}
return {
success: true,
frontmatter,
message: `成功读取 ${csv_file} 的 frontmatter`
};
} catch (error) {
return {
success: false,
message: `读取失败:${error instanceof Error ? error.message : '未知错误'}`
};
}
}
@@ -0,0 +1,131 @@
import { readFileSync, writeFileSync, existsSync } from 'fs';
import yaml from 'js-yaml';
import type { DeckFrontmatter } from './read-frontmatter.js';
/**
* 写入 CSV frontmatter 的参数
*/
export interface WriteFrontmatterParams {
/**
* CSV 文件路径(相对路径相对于 MCP 服务器工作目录)
*/
csv_file: string;
/**
* 要写入的 frontmatter 数据
*/
frontmatter: DeckFrontmatter;
/**
* 是否合并现有 frontmatter(默认 true
*/
merge?: boolean;
}
/**
* 写入 CSV frontmatter 的结果
*/
export interface WriteFrontmatterResult {
success: boolean;
message: string;
csv_file?: string;
}
/**
* 解析 CSV 文件的 frontmatter
*/
function parseFrontMatter(content: string): { frontmatter?: DeckFrontmatter; csvContent: string } {
const parts = content.trim().split(/(?:^|\n)---\s*\n/g);
// 至少需要三个部分:空字符串、front matter、CSV 内容
if (parts.length !== 3 || parts[0] !== '') {
return { csvContent: content };
}
try {
const frontmatterStr = parts[1].trim();
const frontmatter = yaml.load(frontmatterStr) as DeckFrontmatter | undefined;
const csvContent = parts.slice(2).join('---\n').trimStart();
return { frontmatter, csvContent };
} catch (error) {
console.warn('Failed to parse front matter:', error);
return { csvContent: content };
}
}
/**
* 序列化 frontmatter 为 YAML 字符串
*/
function serializeFrontMatter(frontmatter: DeckFrontmatter): string {
const yamlStr = yaml.dump(frontmatter, {
indent: 2,
lineWidth: -1, // 不限制行宽
noRefs: true, // 不使用引用
quotingType: '"',
forceQuotes: false
});
return `---\n${yamlStr}---\n`;
}
/**
* 写入/更新 CSV 文件的 frontmatter
*/
export function writeFrontmatter(params: WriteFrontmatterParams): WriteFrontmatterResult {
const { csv_file, frontmatter, merge = true } = params;
let csvContent = '';
let existingFrontmatter: DeckFrontmatter | undefined;
// 如果文件存在且需要合并,先读取现有内容
if (merge && existsSync(csv_file)) {
try {
const content = readFileSync(csv_file, 'utf-8');
const result = parseFrontMatter(content);
existingFrontmatter = result.frontmatter;
csvContent = result.csvContent;
} catch (error) {
return {
success: false,
message: `读取现有文件失败:${error instanceof Error ? error.message : '未知错误'}`
};
}
}
// 合并或替换 frontmatter
const finalFrontmatter = merge && existingFrontmatter
? { ...existingFrontmatter, ...frontmatter }
: frontmatter;
// 序列化 frontmatter
const frontmatterStr = serializeFrontMatter(finalFrontmatter);
// 如果文件不存在或没有 CSV 内容,创建一个空的 CSV 内容
if (!csvContent.trim()) {
// 从 frontmatter 推断 CSV 表头
const headers = ['label'];
if (finalFrontmatter.fields && Array.isArray(finalFrontmatter.fields)) {
for (const field of finalFrontmatter.fields) {
if (field.name && typeof field.name === 'string') {
headers.push(field.name);
}
}
}
headers.push('body');
csvContent = headers.join(',') + '\n';
}
// 写入文件
try {
const fullContent = frontmatterStr + csvContent;
writeFileSync(csv_file, fullContent, 'utf-8');
return {
success: true,
message: `成功写入 frontmatter 到 ${csv_file}`,
csv_file
};
} catch (error) {
return {
success: false,
message: `写入失败:${error instanceof Error ? error.message : '未知错误'}`
};
}
}