From 06c3916a958fc278d3f9343a4d8c17fca8ea517d Mon Sep 17 00:00:00 2001 From: hyper Date: Thu, 9 Jul 2026 14:13:34 +0800 Subject: [PATCH] feat: rename project to TTTK - Update package name and CLI binary to 'tttk' - Revise documentation to reflect new name - Add default serve behavior when no subcommand is given - Add directory existence check in serve command - Refactor inline comments to Chinese --- README.md | 17 +++++------- docs/cli.md | 32 +++++++++++----------- docs/development.md | 2 +- package.json | 8 +++--- src/cli/commands/compile.ts | 12 ++++----- src/cli/commands/serve.ts | 52 ++++++++++++++++++----------------- src/cli/index.ts | 29 +++++++++++++++----- src/cli/journal.ts | 54 ++++++++++++++++++------------------- 8 files changed, 111 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 2a4f574..b6b3fd3 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,12 @@ -# TTRPG Tools +# TTTK — Tabletop Toolkit -一个基于 `solid.js` 和 `rsbuild` 的 TTRPG 工具箱,支持 Markdown 解析、自定义组件和 CLI 工具。 +A TTRPG content preview and compilation tool based on `solid.js` and `rsbuild`. Write markdown, preview in browser. ## 快速开始 ```bash -# 安装依赖 -npm install - -# 全局安装 CLI -npm link - -# 预览内容 -ttrpg serve ./content +# 无需安装,直接使用 +npx tttk serve ./content ``` ## 文档导航 @@ -20,6 +14,7 @@ ttrpg serve ./content | 文档 | 说明 | |------|------| | [📖 CLI 使用说明](./docs/cli.md) | CLI 安装、命令和用法 | +| [📦 npm](https://www.npmjs.com/package/tttk) | `npx tttk serve ./content` | | [🛠️ 开发指南](./docs/development.md) | 项目结构、开发规范和构建 | | [📝 Markdown 编写说明](./docs/markdown.md) | Markdown 语法和组件用法 | | [📊 CSV 编写说明](./docs/csv.md) | CSV 文件格式、字段定义、变量语法 | @@ -27,7 +22,7 @@ ttrpg serve ./content ## 功能概览 -- **CLI 工具**: `serve` 预览模式 和 `compile` 编译模式 +- **CLI 工具**: `tttk serve` 预览模式 和 `tttk compile` 编译模式 - **Markdown 解析**: 支持指令语法、YAML 标签、mermaid 图表 - **TTRPG 组件**: 骰子、表格、卡牌、标记、命令追踪器等 diff --git a/docs/cli.md b/docs/cli.md index d58d2c2..59a0f70 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,18 +1,18 @@ # CLI 使用说明 -TTRPG Tools 提供一个 CLI 工具,用于将目录内的各种 TTRPG 文档编译为 HTML。 +TTTK 提供一个 CLI 工具,用于将目录内的各种 TTRPG 文档编译为 HTML。 ## 安装 ```bash -# 克隆仓库后安装依赖 -npm install +# 无需安装,直接运行 +npx tttk serve ./content -# 全局链接 CLI 工具 -npm link +# 或全局安装 +npm install -g tttk ``` -安装完成后,可在任意目录使用 `ttrpg` 命令。 +安装完成后,可在任意目录使用 `tttk` 命令。 ## 命令 @@ -23,7 +23,7 @@ CLI 使用子命令组织: 运行一个 Web 服务器预览目录中的内容,并实时监听文件更新。 ```bash -ttrpg serve [dir] -p 3000 +tttk serve [dir] -p 3000 ``` **参数:** @@ -50,13 +50,13 @@ ttrpg serve [dir] -p 3000 ```bash # 预览当前目录 -ttrpg serve +tttk serve # 预览指定目录 -ttrpg serve ./my-ttrpg-content +tttk serve ./my-ttrpg-content # 指定端口 -ttrpg serve ./docs -p 8080 +tttk serve ./docs -p 8080 ``` ### compile - 编译模式 @@ -64,7 +64,7 @@ ttrpg serve ./docs -p 8080 将目录中的内容输出为带 hash 路由、单个 HTML 入口的 Web 应用。 ```bash -ttrpg compile [dir] -o ./dist/output +tttk compile [dir] -o ./dist/output ``` **参数:** @@ -90,13 +90,13 @@ ttrpg compile [dir] -o ./dist/output ```bash # 编译当前目录 -ttrpg compile +tttk compile # 编译指定目录并输出到指定位置 -ttrpg compile ./content -o ./build +tttk compile ./content -o ./build # 编译并部署 -ttrpg compile ./docs -o ./public && npm run preview +tttk compile ./docs -o ./public && npm run preview ``` ## 输入文件 @@ -162,7 +162,7 @@ my-ttrpg-content/ ```bash # 使用其他端口 -ttrpg serve -p 8080 +tttk serve -p 8080 ``` ### 编译输出为空 @@ -174,5 +174,5 @@ ttrpg serve -p 8080 ls -R ./content # 重新编译 -ttrpg compile ./content -o ./dist/output +tttk compile ./content -o ./dist/output ``` diff --git a/docs/development.md b/docs/development.md index 998b9a7..68f4d0f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -28,7 +28,7 @@ npm test ## 项目结构 ``` -ttrpg-tools/ +tttk/ ├── src/ │ ├── cli/ # CLI 工具源码 │ │ ├── commands/ # 命令实现 (serve, compile) diff --git a/package.json b/package.json index d602a10..5941976 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,10 @@ { - "name": "ttrpg-tools", + "name": "tttk", "version": "0.0.1", - "description": "A tabletop RPG toolbox based on solid.js and rsbuild", + "description": "A tabletop RPG toolkit for previewing and compiling TTRPG campaign content", "type": "module", "bin": { - "ttrpg": "./dist/cli/index.js", - "ttt": "./dist/cli/index.js" + "tttk": "./dist/cli/index.js" }, "main": "./dist/index.js", "module": "./dist/index.js", @@ -22,6 +21,7 @@ "cli:dev": "tsc -p tsconfig.cli.json --watch", "cli:build": "tsc -p tsconfig.cli.json", "ttrpg": "node --loader ts-node/esm ./src/cli/index.ts", + "tttk": "node --loader ts-node/esm ./src/cli/index.ts", "test": "jest", "prepare": "npx husky" }, diff --git a/src/cli/commands/compile.ts b/src/cli/commands/compile.ts index 20c9538..47e24a4 100644 --- a/src/cli/commands/compile.ts +++ b/src/cli/commands/compile.ts @@ -1,14 +1,14 @@ -import type { CompileCommandHandler } from '../types.js'; +import type { CompileCommandHandler } from "../types.js"; export const compileCommand: CompileCommandHandler = async (dir, options) => { - console.log(`开始编译...`); - console.log(`目录:${dir}`); + console.log("开始编译..."); + console.log(`源目录:${dir}`); console.log(`输出目录:${options.output}`); - + // TODO: 实现编译逻辑 // 1. 扫描目录下的所有 .md 文件 // 2. 解析 markdown 并生成路由 // 3. 打包为带 hash 路由的单个 HTML 入口 - - console.log('编译完成!'); + + console.log("编译完成!"); }; diff --git a/src/cli/commands/serve.ts b/src/cli/commands/serve.ts index 7994ae5..ff95952 100644 --- a/src/cli/commands/serve.ts +++ b/src/cli/commands/serve.ts @@ -22,14 +22,14 @@ interface ContentIndex { } /** - * 获取 CLI 脚本文件所在目录路径(用于定位 dist 文件夹) + * 获取 CLI 脚本文件所在目录路径(用于定位 dist 目录) */ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const distDir = resolve(__dirname, "..", "..", "..", "dist", "web"); /** - * MIME 类型映射 + * MIME 类型映射表 */ const MIME_TYPES: Record = { ".html": "text/html", @@ -48,7 +48,7 @@ const MIME_TYPES: Record = { }; /** - * 获取文件扩展名对应的 MIME 类型 + * 根据文件扩展名获取对应的 MIME 类型 */ function getMimeType(filePath: string): string { const ext = extname(filePath).toLowerCase(); @@ -56,7 +56,7 @@ function getMimeType(filePath: string): string { } /** - * Get the best network IP for display (prefer LAN IPv4, fallback to localhost). + * 获取最佳网络 IP 用于显示(优先局域网 IPv4,回退到 localhost) */ function getBestIP(): string { const interfaces = networkInterfaces(); @@ -85,7 +85,7 @@ function getBestIP(): string { } /** - * Extract the from an SVG string, or null if absent. + * 从 SVG 字符串中提取 <title> 内容,无则返回 null */ function extractSvgTitle(svg: string): string | null { const m = /<title[^>]*>([\s\S]*?)<\/title>/i.exec(svg); @@ -93,17 +93,15 @@ function extractSvgTitle(svg: string): string | null { } /** - * Derive a human-readable label from a filename. - * "character-sheet" -> "Character Sheet" + * 从文件名生成可读标签 + * 如 "character-sheet" -> "Character Sheet" */ function labelFromId(id: string): string { - return id - .replace(/[-_]/g, " ") - .replace(/\b\w/g, (c) => c.toUpperCase()); + return id.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()); } /** - * 扫描目录内的 .md 文件,生成索引 + * 扫描目录内的 .md 等文件,生成内容索引与块数据 */ export function scanDirectory(dir: string): { index: ContentIndex; @@ -170,7 +168,7 @@ export function scanDirectory(dir: string): { } /** - * 发送文件响应 + * 发送文件响应(流式传输) */ function sendFile(res: ServerResponse, filePath: string) { res.writeHead(200, { @@ -181,7 +179,7 @@ function sendFile(res: ServerResponse, filePath: string) { } /** - * 发送 404 响应 + * 发送 404 未找到响应 */ function send404(res: ServerResponse) { res.writeHead(404, { "Content-Type": "text/plain" }); @@ -189,7 +187,7 @@ function send404(res: ServerResponse) { } /** - * 发送 JSON 响应 + * 发送 JSON 格式的响应 */ function sendJson(res: ServerResponse, data: unknown) { res.writeHead(200, { @@ -201,7 +199,7 @@ function sendJson(res: ServerResponse, data: unknown) { /** * 尝试提供静态文件 - * @returns 如果文件存在并已成功发送则返回 true + * @returns 文件存在并成功发送时返回 true */ function tryServeStatic( res: ServerResponse, @@ -224,7 +222,7 @@ function tryServeStatic( } /** - * 创建请求处理器 + * 创建 HTTP 请求处理器 */ function createRequestHandler( contentDir: string, @@ -274,7 +272,7 @@ function createRequestHandler( } /** - * 内容服务器接口 + * 内容服务器实例接口 */ export interface ContentServer { /** @@ -300,7 +298,7 @@ export interface ContentServer { } /** - * 创建内容服务器 + * 创建内容服务器(HTTP + WebSocket + 文件监听) */ export function createContentServer( contentDir: string, @@ -324,7 +322,7 @@ export function createContentServer( statSheets: [], }; - /** Re-scan completions from current content index and collected blocks */ + /** 从当前内容索引和已收集的块重新扫描补全数据 */ function recomputeCompletions(): void { completionsIndex = scanCompletions(contentIndex, collectedBlocks); console.log( @@ -333,7 +331,7 @@ export function createContentServer( } // 扫描内容目录生成索引 - console.log("扫描内容目录..."); + console.log("正在扫描内容目录..."); const scanResult = scanDirectory(contentDir); contentIndex = scanResult.index; collectedBlocks = scanResult.blocks; @@ -341,7 +339,7 @@ export function createContentServer( recomputeCompletions(); // 监听文件变化 - console.log("监听文件变化..."); + console.log("正在监听文件变化..."); const watcher = watch(contentDir, { ignored: /(^|[\/\\])\./, persistent: true, @@ -431,7 +429,7 @@ export function createContentServer( // ---- Journal / MQTT broker ---- let journal: Awaited<ReturnType<typeof createJournalServer>> | null = null; - // 创建 HTTP 服务器 BEFORE journal so we can pass it in + // 在 journal 之前创建 HTTP 服务器,以便传入给 journal const handleRequest = createRequestHandler( contentDir, distPath, @@ -454,7 +452,7 @@ export function createContentServer( } console.log(`内容目录:${contentDir}`); console.log(`静态资源目录:${distPath}`); - console.log(`Journal 连接:ws://${bestIP}:${port}\n`); + console.log(`日志连接:ws://${bestIP}:${port}\n`); }); return { @@ -463,7 +461,7 @@ export function createContentServer( index: contentIndex, completions: completionsIndex, close() { - console.log("关闭内容服务器..."); + console.log("正在关闭内容服务器..."); server.close(); watcher.close(); if (journal) journal.close(); @@ -476,6 +474,12 @@ export function createContentServer( */ export const serveCommand: ServeCommandHandler = async (dir, options) => { const contentDir = resolve(dir); + + if (!existsSync(contentDir)) { + console.error(`错误:目录不存在:${contentDir}`); + process.exit(1); + } + const port = parseInt(options.port, 10); const host = options.host || "0.0.0.0"; diff --git a/src/cli/index.ts b/src/cli/index.ts index fc36357..a6bd307 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -8,25 +8,42 @@ import type { ServeOptions, CompileOptions } from "./types.js"; const program = new Command(); program - .name("ttrpg") - .description("TTRPG 工具箱 - 用于编译和预览 TTRPG 文档") + .name("tttk") + .description( + "TTRPG 内容预览服务器。不带参数运行时默认启动当前目录的预览服务。", + ) .version("0.0.1"); program .command("serve") - .description("运行一个 web 服务器预览目录中的内容,并实时监听更新") + .description("启动 web 服务器预览目录中的内容,并实时监听文件更新") .argument("[dir]", "要预览的目录", ".") .option("-p, --port <port>", "HTTP 端口号", "3000") - .option("-h, --host <host>", "主机地址", "0.0.0.0") + .option("--host <host>", "主机地址", "0.0.0.0") .action(serveCommand); program .command("compile") - .description("将目录中的内容输出为带 hash 路由、单个 html 入口的 web 应用") + .description("[实验性] 将目录内容编译为带 hash 路由的单页 Web 应用 (开发中)") .argument("[dir]", "要编译的目录", ".") .option("-o, --output <dir>", "输出目录", "./dist/output") .action(compileCommand); program.addCommand(mcpCommand); -program.parse(); +// 无子命令时默认执行 serve(即 `npx tttk` 等同于 `npx tttk serve .`) +const args = process.argv.slice(2); +if (args.length > 0) { + const first = args[0]; + const isKnownCommand = + !first.startsWith("-") && + program.commands.some( + (c) => c.name() === first || c.aliases().includes(first), + ); + if (!isKnownCommand) { + args.unshift("serve"); + } +} else { + args.unshift("serve"); +} +program.parse([process.argv[0], process.argv[1], ...args]); diff --git a/src/cli/journal.ts b/src/cli/journal.ts index 9c96462..c841487 100644 --- a/src/cli/journal.ts +++ b/src/cli/journal.ts @@ -1,14 +1,14 @@ /** - * Journal stream persistence — MQTT broker + JSONL append + session manifest + * 日志流持久化 — MQTT 代理 + JSONL 追加写入 + 会话清单 * - * Runs an aedes MQTT broker over WebSocket, attached to the existing - * HTTP server. Uses ws v8's built-in createWebSocketStream. + * 在现有 HTTP 服务器上挂载 aedes MQTT 代理(WebSocket 传输), + * 使用 ws v8 内置的 createWebSocketStream。 * - * Pattern from aedes docs: + * 实现参考 aedes 文档: * https://github.com/moscajs/aedes#mqtt-server-over-websocket * - * Persistence uses aedes's internal subscribe/publish API, so the - * server process doesn't need to connect to itself as an MQTT client. + * 持久化使用 aedes 内部 subscribe/publish API,无需服务器进程 + * 以 MQTT 客户端身份连接自身。 */ import { join, resolve } from "path"; @@ -26,7 +26,7 @@ import type { AedesPublishPacket } from "aedes"; import { WebSocketServer, createWebSocketStream } from "ws"; // --------------------------------------------------------------------------- -// Types +// 类型定义 // --------------------------------------------------------------------------- interface SessionMeta { @@ -46,7 +46,7 @@ export interface JournalServer { } // --------------------------------------------------------------------------- -// Path helpers +// 路径工具函数 // --------------------------------------------------------------------------- function dataDir(contentDir: string): string { @@ -88,7 +88,7 @@ function appendStream(dataRoot: string, id: string, line: string): void { } // --------------------------------------------------------------------------- -// Server factory +// 服务器工厂 // --------------------------------------------------------------------------- const $SESSIONS = "ttrpg/$SESSIONS"; @@ -98,15 +98,15 @@ export async function createJournalServer( httpServer: HttpServer, ): Promise<JournalServer> { const root = dataDir(contentDir); - console.log(`[journal] data dir: ${root}`); + console.log(`[日志] 数据目录:${root}`); - // ---- MQTT Broker ---- + // ---- MQTT 代理 ---- const { Aedes: AedesFactory } = await import("aedes"); - // aedes requires listen() to initialize persistence before handling connections. + // aedes 需要先 listen() 初始化持久化,再处理连接 const broker = await AedesFactory.createBroker(); - // ---- WebSocket server (attached to the existing HTTP server) ---- - // Pattern from aedes docs: pass { server } to WSS, use 'connection' event + // ---- WebSocket 服务器(挂载到现有 HTTP 服务器上) ---- + // 参考 aedes 文档:给 WSS 传入 { server },使用 'connection' 事件 const wss = new WebSocketServer({ server: httpServer }); wss.on("connection", (ws, req) => { @@ -115,13 +115,13 @@ export async function createJournalServer( }); broker.on("client", (client: import("aedes").Client) => { - console.log(`[journal] client ready: ${client.id}`); + console.log(`[日志] 客户端已连接:${client.id}`); }); broker.on("clientDisconnect", (client: import("aedes").Client) => { - console.log(`[journal] client disconnected: ${client.id}`); + console.log(`[日志] 客户端已断开:${client.id}`); }); - // ---- Persistence (internal subscribe, no separate MQTT client) ---- + // ---- 持久化(内部订阅,无需独立 MQTT 客户端) ---- broker.subscribe( "ttrpg/+/stream", (packet, cb) => { @@ -130,7 +130,7 @@ export async function createJournalServer( try { appendStream(root, sessionId, packet.payload.toString()); } catch (e) { - console.error(`[journal] stream append err for ${sessionId}:`, e); + console.error(`[日志] 会话 ${sessionId} 流记录写入失败:`, e); } } cb(); @@ -150,7 +150,7 @@ export async function createJournalServer( () => {}, ); - // Publish the initial manifest on startup so clients see existing sessions + // 启动时发布初始清单,让客户端能看到已有会话 const initialManifest = loadManifest(root); broker.publish( { @@ -164,14 +164,14 @@ export async function createJournalServer( () => {}, ); - console.log("[journal] persistence listener active"); - console.log("[journal] broker available at ws://<host>:<port>"); + console.log("[日志] 持久化监听已启动"); + console.log("[日志] MQTT 代理已就绪:ws://<host>:<port>"); return { broker, wsServer: wss, async close() { - console.log("[journal] shutting down..."); + console.log("[日志] 正在关闭..."); await new Promise<void>((r) => wss.close(() => r())); await new Promise<void>((r) => broker.close(() => r())); }, @@ -179,7 +179,7 @@ export async function createJournalServer( } // --------------------------------------------------------------------------- -// Helpers +// 工具函数 // --------------------------------------------------------------------------- function extractSessionId(topic: string): string | null { @@ -188,7 +188,7 @@ function extractSessionId(topic: string): string | null { } // --------------------------------------------------------------------------- -// Session lifecycle +// 会话生命周期管理 // --------------------------------------------------------------------------- function handleMetaChange( @@ -212,7 +212,7 @@ function handleMetaChange( } catch { /* */ } - console.log(`[journal] session deleted: ${sessionId}`); + console.log(`[日志] 会话已删除:${sessionId}`); } } else { manifest.sessions[sessionId] = { @@ -220,7 +220,7 @@ function handleMetaChange( created: meta.created || Date.now(), players: meta.players || [], }; - console.log(`[journal] session updated: ${sessionId} (${meta.name})`); + console.log(`[日志] 会话已更新:${sessionId} (${meta.name})`); } saveManifest(root, manifest); @@ -236,6 +236,6 @@ function handleMetaChange( () => {}, ); } catch (e) { - console.error(`[journal] meta parse err for ${sessionId}:`, e); + console.error(`[日志] 会话 ${sessionId} 元数据解析失败:`, e); } }