Compare commits
3 Commits
1d2b4b3e1e
...
71ab7523da
| Author | SHA1 | Date |
|---|---|---|
|
|
71ab7523da | |
|
|
e5ce8c20b5 | |
|
|
5389345a00 |
|
|
@ -3,7 +3,7 @@ import { generateCardDeck, type GenerateCardDeckParams, type CardField } from '.
|
||||||
import { readFrontmatter, type ReadFrontmatterParams, type DeckFrontmatter } from '../tools/frontmatter/read-frontmatter.js';
|
import { readFrontmatter, type ReadFrontmatterParams, type DeckFrontmatter } from '../tools/frontmatter/read-frontmatter.js';
|
||||||
import { writeFrontmatter, type WriteFrontmatterParams } from '../tools/frontmatter/write-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 { cardCrud, type CardCrudParams, type CardData } from '../tools/card/card-crud.js';
|
||||||
import { ensureDeckPreview, type EnsureDeckPreviewParams } from '../tools/ensure-deck-preview.js';
|
import { previewDeck, type PreviewDeckParams } from '../tools/preview-deck.js';
|
||||||
import {
|
import {
|
||||||
designCardGame,
|
designCardGame,
|
||||||
getDesignCardGamePrompt,
|
getDesignCardGamePrompt,
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
readResource,
|
readResource,
|
||||||
type DocResource
|
type DocResource
|
||||||
} from '../resources/docs.js';
|
} from '../resources/docs.js';
|
||||||
|
import { createContentServer, type ContentServer } from './serve.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MCP 服务器命令
|
* MCP 服务器命令
|
||||||
|
|
@ -68,6 +69,9 @@ async function mcpServeAction(host: string, options: MCPOptions) {
|
||||||
process.chdir(cwd);
|
process.chdir(cwd);
|
||||||
console.error(`MCP 服务器工作目录:${cwd}`);
|
console.error(`MCP 服务器工作目录:${cwd}`);
|
||||||
|
|
||||||
|
// 启动内容服务器(预览服务器)
|
||||||
|
const contentServer: ContentServer = createContentServer(cwd, 3000);
|
||||||
|
|
||||||
// 动态导入 MCP SDK
|
// 动态导入 MCP SDK
|
||||||
const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
|
const { Server } = await import('@modelcontextprotocol/sdk/server/index.js');
|
||||||
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
|
||||||
|
|
@ -196,8 +200,8 @@ async function mcpServeAction(host: string, options: MCPOptions) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'deck_ensure_preview',
|
name: 'deck_preview',
|
||||||
description: '确保 CSV 对应的 Markdown 预览文件存在',
|
description: '保存 CSV 对应的 Markdown 预览文件并打开浏览器',
|
||||||
inputSchema: {
|
inputSchema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
|
@ -320,7 +324,7 @@ async function mcpServeAction(host: string, options: MCPOptions) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'deck_card_crud': {
|
case 'deck_card_crud': {
|
||||||
const params = args as unknown as CardCrudParams;
|
let params = args as unknown as CardCrudParams;
|
||||||
|
|
||||||
if (!params.csv_file || !params.action) {
|
if (!params.csv_file || !params.action) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -329,6 +333,15 @@ async function mcpServeAction(host: string, options: MCPOptions) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析 cards 参数(可能是 JSON 字符串)
|
||||||
|
if (params.cards && typeof params.cards === 'string') {
|
||||||
|
try {
|
||||||
|
params.cards = JSON.parse(params.cards);
|
||||||
|
} catch (e) {
|
||||||
|
// 如果不是 JSON,保持原样
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const result = cardCrud(params);
|
const result = cardCrud(params);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -346,8 +359,8 @@ async function mcpServeAction(host: string, options: MCPOptions) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'deck_ensure_preview': {
|
case 'deck_preview': {
|
||||||
const params = args as unknown as EnsureDeckPreviewParams;
|
const params = args as unknown as PreviewDeckParams;
|
||||||
|
|
||||||
if (!params.csv_file) {
|
if (!params.csv_file) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -356,10 +369,15 @@ async function mcpServeAction(host: string, options: MCPOptions) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = ensureDeckPreview(params);
|
const result = previewDeck(params);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{ type: 'text', text: result.success ? `✅ ${result.message}` : `❌ ${result.message}` }],
|
content: [{
|
||||||
|
type: 'text',
|
||||||
|
text: result.success
|
||||||
|
? `✅ ${result.message}\n\n预览 URL: ${result.preview_url}`
|
||||||
|
: `❌ ${result.message}`
|
||||||
|
}],
|
||||||
isError: !result.success,
|
isError: !result.success,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -452,6 +470,65 @@ async function mcpServeAction(host: string, options: MCPOptions) {
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
console.error('TTRPG 卡牌生成 MCP 服务器已启动(stdio)');
|
console.error('TTRPG 卡牌生成 MCP 服务器已启动(stdio)');
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
let isShuttingDown = false;
|
||||||
|
async function shutdown() {
|
||||||
|
if (isShuttingDown) return;
|
||||||
|
isShuttingDown = true;
|
||||||
|
|
||||||
|
console.error('\n正在关闭服务器...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先关闭服务器,这会触发 transport 的关闭
|
||||||
|
await server.close();
|
||||||
|
console.error('MCP 服务器已关闭');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('关闭 MCP 服务器时出错:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 确保 transport 被关闭
|
||||||
|
await transport.close();
|
||||||
|
console.error('MCP Transport 已关闭');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('关闭 MCP Transport 时出错:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 关闭内容服务器
|
||||||
|
contentServer.close();
|
||||||
|
console.error('内容服务器已关闭');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('关闭内容服务器时出错:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 给异步清理一些时间
|
||||||
|
setTimeout(() => {
|
||||||
|
process.exit(0);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听进程退出事件
|
||||||
|
process.on('SIGINT', () => shutdown());
|
||||||
|
process.on('SIGTERM', () => shutdown());
|
||||||
|
|
||||||
|
// Windows 上 Ctrl+C 会触发 'SIGINT',但也监听 'exit' 事件作为后备
|
||||||
|
process.on('exit', () => {
|
||||||
|
contentServer.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 stdin 结束事件(当客户端关闭连接时)
|
||||||
|
process.stdin.on('end', () => {
|
||||||
|
console.error('客户端已断开连接,正在关闭...');
|
||||||
|
shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 stdin 关闭事件
|
||||||
|
process.stdin.on('close', () => {
|
||||||
|
console.error('stdin 已关闭,正在关闭服务器...');
|
||||||
|
shutdown();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// TODO: 支持 HTTP 传输
|
// TODO: 支持 HTTP 传输
|
||||||
console.error('HTTP 传输模式尚未实现,请使用 stdio 模式');
|
console.error('HTTP 传输模式尚未实现,请使用 stdio 模式');
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function getMimeType(filePath: string): string {
|
||||||
/**
|
/**
|
||||||
* 扫描目录内的 .md 文件,生成索引
|
* 扫描目录内的 .md 文件,生成索引
|
||||||
*/
|
*/
|
||||||
function scanDirectory(dir: string): ContentIndex {
|
export function scanDirectory(dir: string): ContentIndex {
|
||||||
const index: ContentIndex = {};
|
const index: ContentIndex = {};
|
||||||
|
|
||||||
function scan(currentPath: string, relativePath: string) {
|
function scan(currentPath: string, relativePath: string) {
|
||||||
|
|
@ -176,10 +176,35 @@ function createRequestHandler(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动开发服务器
|
* 内容服务器接口
|
||||||
*/
|
*/
|
||||||
export const serveCommand: ServeCommandHandler = async (dir, options) => {
|
export interface ContentServer {
|
||||||
const contentDir = resolve(dir);
|
/**
|
||||||
|
* HTTP 服务器实例
|
||||||
|
*/
|
||||||
|
server: Server;
|
||||||
|
/**
|
||||||
|
* 文件监听器
|
||||||
|
*/
|
||||||
|
watcher: ReturnType<typeof watch>;
|
||||||
|
/**
|
||||||
|
* 内容索引
|
||||||
|
*/
|
||||||
|
index: ContentIndex;
|
||||||
|
/**
|
||||||
|
* 关闭服务器
|
||||||
|
*/
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建内容服务器
|
||||||
|
*/
|
||||||
|
export function createContentServer(
|
||||||
|
contentDir: string,
|
||||||
|
port: number,
|
||||||
|
distPath: string = distDir,
|
||||||
|
): ContentServer {
|
||||||
let contentIndex: ContentIndex = {};
|
let contentIndex: ContentIndex = {};
|
||||||
|
|
||||||
// 扫描内容目录生成索引
|
// 扫描内容目录生成索引
|
||||||
|
|
@ -231,19 +256,38 @@ export const serveCommand: ServeCommandHandler = async (dir, options) => {
|
||||||
// 创建请求处理器
|
// 创建请求处理器
|
||||||
const handleRequest = createRequestHandler(
|
const handleRequest = createRequestHandler(
|
||||||
contentDir,
|
contentDir,
|
||||||
distDir,
|
distPath,
|
||||||
() => contentIndex,
|
() => contentIndex,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 创建 HTTP 服务器
|
// 创建 HTTP 服务器
|
||||||
const server = createServer(handleRequest);
|
const server = createServer(handleRequest);
|
||||||
|
|
||||||
const port = parseInt(options.port, 10);
|
|
||||||
|
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
console.log(`\n开发服务器已启动:http://localhost:${port}`);
|
console.log(`\n开发服务器已启动:http://localhost:${port}`);
|
||||||
console.log(`内容目录:${contentDir}`);
|
console.log(`内容目录:${contentDir}`);
|
||||||
console.log(`静态资源目录:${distDir}`);
|
console.log(`静态资源目录:${distPath}`);
|
||||||
console.log(`索引文件:http://localhost:${port}/__CONTENT_INDEX.json\n`);
|
console.log(`索引文件:http://localhost:${port}/__CONTENT_INDEX.json\n`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
server,
|
||||||
|
watcher,
|
||||||
|
index: contentIndex,
|
||||||
|
close() {
|
||||||
|
console.log("关闭内容服务器...");
|
||||||
|
server.close();
|
||||||
|
watcher.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动开发服务器
|
||||||
|
*/
|
||||||
|
export const serveCommand: ServeCommandHandler = async (dir, options) => {
|
||||||
|
const contentDir = resolve(dir);
|
||||||
|
const port = parseInt(options.port, 10);
|
||||||
|
|
||||||
|
createContentServer(contentDir, port);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||||
|
import { dirname, join, basename, extname, relative } from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import yaml from 'js-yaml';
|
||||||
|
import type { DeckFrontmatter, DeckConfig } from './frontmatter/read-frontmatter.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览 Deck 的参数
|
||||||
|
*/
|
||||||
|
export interface PreviewDeckParams {
|
||||||
|
/**
|
||||||
|
* CSV 文件路径
|
||||||
|
*/
|
||||||
|
csv_file: string;
|
||||||
|
/**
|
||||||
|
* Markdown 文件路径(可选,默认与 CSV 同名)
|
||||||
|
*/
|
||||||
|
md_file?: string;
|
||||||
|
/**
|
||||||
|
* 标题(可选,默认从 CSV 文件名推断)
|
||||||
|
*/
|
||||||
|
title?: string;
|
||||||
|
/**
|
||||||
|
* 描述(可选)
|
||||||
|
*/
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览 Deck 的结果
|
||||||
|
*/
|
||||||
|
export interface PreviewDeckResult {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
md_file?: string;
|
||||||
|
created?: boolean;
|
||||||
|
preview_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 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('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开浏览器
|
||||||
|
*/
|
||||||
|
function openBrowser(url: string) {
|
||||||
|
try {
|
||||||
|
// Windows 使用 start 命令
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
execSync(`start "" "${url}"`, { stdio: 'ignore' });
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
execSync(`open "${url}"`, { stdio: 'ignore' });
|
||||||
|
} else {
|
||||||
|
execSync(`xdg-open "${url}"`, { stdio: 'ignore' });
|
||||||
|
}
|
||||||
|
console.error(`已打开浏览器:${url}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开浏览器失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览 Deck - 保存 Markdown 并打开浏览器
|
||||||
|
*/
|
||||||
|
export function previewDeck(params: PreviewDeckParams): PreviewDeckResult {
|
||||||
|
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)}]`)) {
|
||||||
|
// 仍然打开浏览器
|
||||||
|
const relativeMdPath = relative(process.cwd(), mdFilePath).split('\\').join('/');
|
||||||
|
const previewUrl = `http://localhost:3000/${relativeMdPath}`;
|
||||||
|
openBrowser(previewUrl);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `预览文件 ${mdFilePath} 已存在`,
|
||||||
|
md_file: mdFilePath,
|
||||||
|
created: false,
|
||||||
|
preview_url: previewUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} 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');
|
||||||
|
|
||||||
|
// 计算预览 URL
|
||||||
|
const relativeMdPath = relative(process.cwd(), mdFilePath).split('\\').join('/');
|
||||||
|
const previewUrl = `http://localhost:3000/${relativeMdPath.slice(0, -3)}`;
|
||||||
|
|
||||||
|
// 打开浏览器
|
||||||
|
openBrowser(previewUrl);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: created
|
||||||
|
? `创建预览文件 ${mdFilePath}`
|
||||||
|
: `更新预览文件 ${mdFilePath}`,
|
||||||
|
md_file: mdFilePath,
|
||||||
|
created,
|
||||||
|
preview_url: previewUrl
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `写入失败:${error instanceof Error ? error.message : '未知错误'}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue