feat: add MQTT journal server for session persistence

Implement a journal server that uses an Aedes MQTT broker to
persist stream messages to JSONL files and manage session metadata
via a manifest.

- Add `aedes`, `mqtt`, and `zod` dependencies
- Implement `createJournalServer` in `src/cli/journal.ts`
- Integrate journal server into the `serve` command
- Add `--mqtt-port` option to the CLI
- Add session lifecycle management (create, update, delete) via
  retained MQTT topics
This commit is contained in:
hypercross 2026-07-06 14:19:16 +08:00
parent 08389601ac
commit 21f8fbcd0b
6 changed files with 952 additions and 41 deletions

669
package-lock.json generated
View File

@ -13,6 +13,7 @@
"@solidjs/router": "^0.15.0",
"@thisbeyond/solid-dnd": "^0.7.5",
"@types/three": "^0.183.1",
"aedes": "^1.1.1",
"chokidar": "^5.0.0",
"commander": "^14.0.3",
"csv-parse": "^6.1.0",
@ -23,11 +24,13 @@
"marked-directive": "^1.0.7",
"marked-gfm-heading-id": "^4.1.3",
"mermaid": "^11.0.0",
"mqtt": "^5.15.1",
"solid-element": "^1.9.1",
"solid-js": "^1.9.3",
"three": "^0.183.2",
"three-3mf-exporter": "^45.1.0",
"yarn-spinner-loader": "^0.1.0"
"yarn-spinner-loader": "^0.1.0",
"zod": "^4.4.3"
},
"bin": {
"ttrpg": "dist/cli/index.js"
@ -721,6 +724,15 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@ -1871,6 +1883,15 @@
"zod": "^3.23.8"
}
},
"node_modules/@modelcontextprotocol/sdk/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"
}
},
"node_modules/@module-federation/error-codes": {
"version": "0.22.0",
"resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz",
@ -3310,12 +3331,20 @@
"version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
"integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/readable-stream": {
"version": "4.0.24",
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.24.tgz",
"integrity": "sha512-NRvUNC/JFGPJvqdAfEve8oginbM6V08u5NzLWpG8MwA2kTPOLnqk+wpwuPT+mp3aUsxyuT6m2gnrPuHYCruzEg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@ -3364,6 +3393,15 @@
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yargs": {
"version": "17.0.35",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
@ -3405,6 +3443,18 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@ -3441,6 +3491,138 @@
"node": ">=0.4.0"
}
},
"node_modules/aedes": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/aedes/-/aedes-1.1.1.tgz",
"integrity": "sha512-kmKFRgQrjGyv9HuVqpNGM/aPD2K4bf20aF1UbrxEvh3j8BJ1OD0rHi3E3u3xfrgc3ydj2+9UiPdW0sHiP6d+Sg==",
"license": "MIT",
"dependencies": {
"aedes-packet": "^3.0.0",
"aedes-persistence": "^10.2.2",
"mqemitter": "^7.1.0",
"mqtt-packet": "^9.0.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/aedes"
},
"peerDependencies": {
"aedes-persistence-level": "^9.1.2",
"aedes-persistence-mongodb": "^9.3.1",
"aedes-persistence-redis": "^11.2.1"
},
"peerDependenciesMeta": {
"aedes-persistence-level": {
"optional": true
},
"aedes-persistence-mongodb": {
"optional": true
},
"aedes-persistence-redis": {
"optional": true
}
}
},
"node_modules/aedes-packet": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-3.0.0.tgz",
"integrity": "sha512-swASey0BxGs4/npZGWoiVDmnEyPvVFIRY6l2LVKL4rbiW8IhcIGDLfnb20Qo8U20itXlitAKPQ3MVTEbOGG5ZA==",
"license": "MIT",
"dependencies": {
"mqtt-packet": "^7.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/aedes-packet/node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/aedes-packet/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/aedes-packet/node_modules/mqtt-packet": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-7.1.2.tgz",
"integrity": "sha512-FFZbcZ2omsf4c5TxEQfcX9hI+JzDpDKPT46OmeIBpVA7+t32ey25UNqlqNXTmeZOr5BLsSIERpQQLsFWJS94SQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.2",
"debug": "^4.1.1",
"process-nextick-args": "^2.0.1"
}
},
"node_modules/aedes-packet/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/aedes-persistence": {
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/aedes-persistence/-/aedes-persistence-10.3.1.tgz",
"integrity": "sha512-C/z0zLEXY9PkUH+7pz31H5gGzlQZxqhFONK1lt5VON0hiHXQq6tr4cSIAtFUVQ1kyUdd7SsfkeKLAECp3gjjpw==",
"license": "MIT",
"dependencies": {
"aedes-packet": "^4.0.1",
"qlobber": "^8.0.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/aedes-persistence/node_modules/aedes-packet": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/aedes-packet/-/aedes-packet-4.0.1.tgz",
"integrity": "sha512-PLjKJElXEYB66RzHhJCw0SrRLfSmGRbbg2mtO5t/6FAbFp8vC0cG5ErhvPvfa9iKBrKZwoluWLgn8doBU8sRAw==",
"license": "MIT",
"dependencies": {
"mqtt-packet": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -3727,6 +3909,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.15",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.15.tgz",
@ -3740,6 +3942,63 @@
"node": ">=6.0.0"
}
},
"node_modules/bl": {
"version": "6.1.6",
"resolved": "https://registry.npmjs.org/bl/-/bl-6.1.6.tgz",
"integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==",
"license": "MIT",
"dependencies": {
"@types/readable-stream": "^4.0.0",
"buffer": "^6.0.3",
"inherits": "^2.0.4",
"readable-stream": "^4.2.0"
}
},
"node_modules/bl/node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/bl/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bl/node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/brace-expansion": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
@ -3763,6 +4022,18 @@
"node": ">=8"
}
},
"node_modules/broker-factory": {
"version": "3.1.15",
"resolved": "https://registry.npmjs.org/broker-factory/-/broker-factory-3.1.15.tgz",
"integrity": "sha512-ko+aWvgNuP49meGrdjUu7rC+Y+Wai3cCPxP3xWwHsHfehFjOh5ZQM2yC4gEB2UddeZ/YXhm0K1eG/L6fxym2Og==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.7",
"fast-unique-numbers": "^9.0.27",
"tslib": "^2.8.1",
"worker-factory": "^7.0.50"
}
},
"node_modules/browserslist": {
"version": "4.28.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
@ -3820,11 +4091,34 @@
"node-int64": "^0.4.0"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/bytes": {
@ -4063,6 +4357,12 @@
"node": ">=20"
}
},
"node_modules/commist": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz",
"integrity": "sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw==",
"license": "MIT"
},
"node_modules/component-register": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/component-register/-/component-register-0.8.8.tgz",
@ -4076,6 +4376,35 @@
"dev": true,
"license": "MIT"
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/concat-stream/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
@ -4778,7 +5107,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -5119,6 +5447,24 @@
"node": ">=0.10.0"
}
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/execa": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
@ -5199,6 +5545,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-unique-numbers": {
"version": "9.0.27",
"resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.27.tgz",
"integrity": "sha512-nDA9ADeINN8SA2u2wCtU+siWFTTDqQR37XvgPIDDmboWQeExz7X0mImxuaN+kJddliIqy2FpVRmnvRZ+j8i1/A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=18.2.0"
}
},
"node_modules/fast-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
@ -5250,6 +5609,16 @@
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fastparallel": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz",
"integrity": "sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==",
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4",
"xtend": "^4.0.2"
}
},
"node_modules/fastq": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
@ -5571,6 +5940,12 @@
"node": ">= 0.4"
}
},
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"license": "MIT"
},
"node_modules/html-encoding-sniffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
@ -5685,6 +6060,26 @@
"node": ">=0.10.0"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@ -5748,6 +6143,15 @@
"node": ">=12"
}
},
"node_modules/ip-address": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@ -7430,6 +7834,16 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-sdsl": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz",
"integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/js-sdsl"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -8167,7 +8581,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -8191,11 +8604,117 @@
"integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==",
"license": "BSD-3-Clause"
},
"node_modules/mqemitter": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/mqemitter/-/mqemitter-7.1.0.tgz",
"integrity": "sha512-GnBDNz3lxmllW201ne0mrmdy5tPOTnc79jjVcsfUa2LG2pUGeyGWVeiae6ZysfC/64XrYOqCKRAQYrB7pGyBVQ==",
"license": "ISC",
"dependencies": {
"fastparallel": "^2.4.1",
"qlobber": "^8.0.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/mqtt": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/mqtt/-/mqtt-5.15.1.tgz",
"integrity": "sha512-V1WnkGuJh3ec9QXzy5Iylw8OOBK+Xu1WhxcQ9mMpLThG+/JZIMV1PgLNRgIiqXhZnvnVLsuyxHl5A/3bHHbcAA==",
"license": "MIT",
"dependencies": {
"@types/readable-stream": "^4.0.21",
"@types/ws": "^8.18.1",
"commist": "^3.2.0",
"concat-stream": "^2.0.0",
"debug": "^4.4.1",
"help-me": "^5.0.0",
"lru-cache": "^10.4.3",
"minimist": "^1.2.8",
"mqtt-packet": "^9.0.2",
"number-allocator": "^1.0.14",
"readable-stream": "^4.7.0",
"rfdc": "^1.4.1",
"socks": "^2.8.6",
"split2": "^4.2.0",
"worker-timers": "^8.0.23",
"ws": "^8.18.3"
},
"bin": {
"mqtt": "build/bin/mqtt.js",
"mqtt_pub": "build/bin/pub.js",
"mqtt_sub": "build/bin/sub.js"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/mqtt-packet": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-9.0.2.tgz",
"integrity": "sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA==",
"license": "MIT",
"dependencies": {
"bl": "^6.0.8",
"debug": "^4.3.4",
"process-nextick-args": "^2.0.1"
}
},
"node_modules/mqtt/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/mqtt/node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/mqtt/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/mqtt/node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@ -8268,6 +8787,16 @@
"node": ">=8"
}
},
"node_modules/number-allocator": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.14.tgz",
"integrity": "sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.1",
"js-sdsl": "4.3.0"
}
},
"node_modules/nwsapi": {
"version": "2.2.23",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
@ -8624,6 +9153,15 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@ -8684,6 +9222,15 @@
],
"license": "MIT"
},
"node_modules/qlobber": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/qlobber/-/qlobber-8.0.1.tgz",
"integrity": "sha512-O+Wd1chXj5YE1DwmD+ae0bXiSLehmnS3czlC1R9FL/Nt/3q8uMS1bIHmg2lJfCoiimCxClWM8AAuJrF0EvNiog==",
"license": "MIT",
"engines": {
"node": ">= 16"
}
},
"node_modules/querystringify": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
@ -8874,6 +9421,12 @@
"node": ">=0.10.0"
}
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/robust-predicates": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
@ -9071,6 +9624,30 @@
"node": ">=8"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/socks": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
"integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
"license": "MIT",
"dependencies": {
"ip-address": "^10.1.1",
"smart-buffer": "^4.2.0"
},
"engines": {
"node": ">= 10.0.0",
"npm": ">= 3.0.0"
}
},
"node_modules/solid-element": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/solid-element/-/solid-element-1.9.1.tgz",
@ -9139,6 +9716,15 @@
"source-map": "^0.6.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -9611,7 +10197,6 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"devOptional": true,
"license": "0BSD"
},
"node_modules/type-detect": {
@ -9637,6 +10222,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@ -9675,7 +10266,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/universalify": {
@ -10016,6 +10606,53 @@
"dev": true,
"license": "MIT"
},
"node_modules/worker-factory": {
"version": "7.0.50",
"resolved": "https://registry.npmjs.org/worker-factory/-/worker-factory-7.0.50.tgz",
"integrity": "sha512-hhwc0G+sFwM4qBuhJIUBn2p1Jf8v/FwmLUANBf/Q+Lt2uI8mfIZQhXaZQACodQD4R7Zp6cn/6702bIvNn2puJQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.7",
"fast-unique-numbers": "^9.0.27",
"tslib": "^2.8.1"
}
},
"node_modules/worker-timers": {
"version": "8.0.33",
"resolved": "https://registry.npmjs.org/worker-timers/-/worker-timers-8.0.33.tgz",
"integrity": "sha512-RQVlKkek80v8M6SHvdMKiywaXFmX3XTpYTXTw0r62PdXB06t74XNxcTuG8wsQwnjorAQymNQyyQ0rzv7PykEMQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.7",
"tslib": "^2.8.1",
"worker-timers-broker": "^8.0.18",
"worker-timers-worker": "^9.0.15"
}
},
"node_modules/worker-timers-broker": {
"version": "8.0.18",
"resolved": "https://registry.npmjs.org/worker-timers-broker/-/worker-timers-broker-8.0.18.tgz",
"integrity": "sha512-FrjzDVX1wKfZN0gRbCFqv8VHuTncG4sbI/WGEg4tSSQeIsnwqg4YBYWMAHYLJtUDEYYmiK65UKFrVMVk2irDSg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.7",
"broker-factory": "^3.1.15",
"fast-unique-numbers": "^9.0.27",
"tslib": "^2.8.1",
"worker-timers-worker": "^9.0.15"
}
},
"node_modules/worker-timers-worker": {
"version": "9.0.15",
"resolved": "https://registry.npmjs.org/worker-timers-worker/-/worker-timers-worker-9.0.15.tgz",
"integrity": "sha512-KKUe7lZ/Aignr51H6hOUik8LwTnIgojH/1lwhli8A8qIEIyewogZTpNpMW5B6BF7nmwBOkUoTYgf1H3QShcjSA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.7",
"tslib": "^2.8.1",
"worker-factory": "^7.0.50"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@ -10059,7 +10696,6 @@
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@ -10094,6 +10730,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@ -10194,9 +10839,9 @@
}
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@ -36,6 +36,7 @@
"@solidjs/router": "^0.15.0",
"@thisbeyond/solid-dnd": "^0.7.5",
"@types/three": "^0.183.1",
"aedes": "^1.1.1",
"chokidar": "^5.0.0",
"commander": "^14.0.3",
"csv-parse": "^6.1.0",
@ -46,11 +47,13 @@
"marked-directive": "^1.0.7",
"marked-gfm-heading-id": "^4.1.3",
"mermaid": "^11.0.0",
"mqtt": "^5.15.1",
"solid-element": "^1.9.1",
"solid-js": "^1.9.3",
"three": "^0.183.2",
"three-3mf-exporter": "^45.1.0",
"yarn-spinner-loader": "^0.1.0"
"yarn-spinner-loader": "^0.1.0",
"zod": "^4.4.3"
},
"devDependencies": {
"@image-tracer-ts/core": "^1.0.2",

View File

@ -5,6 +5,7 @@ import { createReadStream } from "fs";
import { join, resolve, extname, sep, relative, dirname } from "path";
import { watch } from "chokidar";
import { fileURLToPath } from "url";
import { createJournalServer } from "../journal.js";
interface ContentIndex {
[path: string]: string;
@ -63,7 +64,11 @@ export function scanDirectory(dir: string): ContentIndex {
const stats = statSync(fullPath);
if (stats.isDirectory()) {
scan(fullPath, relPath);
} else if (entry.endsWith(".md") || entry.endsWith(".csv") || entry.endsWith(".yarn")) {
} else if (
entry.endsWith(".md") ||
entry.endsWith(".csv") ||
entry.endsWith(".yarn")
) {
try {
const content = readFileSync(fullPath, "utf-8");
index[normalizedRelPath] = content;
@ -205,6 +210,7 @@ export function createContentServer(
port: number,
distPath: string = distDir,
host: string = "0.0.0.0",
mqttPort: number = 1883,
): ContentServer {
let contentIndex: ContentIndex = {};
@ -216,14 +222,18 @@ export function createContentServer(
// 监听文件变化
console.log("监听文件变化...");
const watcher = watch(contentDir, {
ignored: /(^|[\/\\])\../,
ignored: /(^|[\/\\])\./,
persistent: true,
ignoreInitial: true,
});
watcher
.on("add", (path) => {
if (path.endsWith(".md") || path.endsWith(".csv") || path.endsWith(".yarn")) {
if (
path.endsWith(".md") ||
path.endsWith(".csv") ||
path.endsWith(".yarn")
) {
try {
const content = readFileSync(path, "utf-8");
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
@ -235,7 +245,11 @@ export function createContentServer(
}
})
.on("change", (path) => {
if (path.endsWith(".md") || path.endsWith(".csv") || path.endsWith(".yarn")) {
if (
path.endsWith(".md") ||
path.endsWith(".csv") ||
path.endsWith(".yarn")
) {
try {
const content = readFileSync(path, "utf-8");
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
@ -247,13 +261,24 @@ export function createContentServer(
}
})
.on("unlink", (path) => {
if (path.endsWith(".md") || path.endsWith(".csv") || path.endsWith(".yarn")) {
if (
path.endsWith(".md") ||
path.endsWith(".csv") ||
path.endsWith(".yarn")
) {
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
delete contentIndex[relPath];
console.log(`[删除] ${relPath}`);
}
});
// ---- Journal / MQTT broker ----
let journal: Awaited<ReturnType<typeof createJournalServer>> | null = null;
createJournalServer(contentDir, mqttPort).then((j) => {
journal = j;
});
// 创建请求处理器
const handleRequest = createRequestHandler(
contentDir,
@ -269,7 +294,9 @@ export function createContentServer(
console.log(`\n开发服务器已启动http://${displayHost}:${port}`);
console.log(`内容目录:${contentDir}`);
console.log(`静态资源目录:${distPath}`);
console.log(`索引文件http://${displayHost}:${port}/__CONTENT_INDEX.json\n`);
console.log(
`MQTT 代理端口:${mqttPort} (connect via tcp://${displayHost}:${mqttPort})\n`,
);
});
return {
@ -280,6 +307,7 @@ export function createContentServer(
console.log("关闭内容服务器...");
server.close();
watcher.close();
if (journal) journal.close();
},
};
}
@ -291,6 +319,7 @@ export const serveCommand: ServeCommandHandler = async (dir, options) => {
const contentDir = resolve(dir);
const port = parseInt(options.port, 10);
const host = options.host || "0.0.0.0";
const mqttPort = parseInt(options.mqttPort || "1883", 10);
createContentServer(contentDir, port, distDir, host);
createContentServer(contentDir, port, distDir, host, mqttPort);
};

View File

@ -1,33 +1,33 @@
#!/usr/bin/env node
import { Command } from 'commander';
import { serveCommand } from './commands/serve.js';
import { compileCommand } from './commands/compile.js';
import { mcpCommand } from './commands/mcp.js';
import type { ServeOptions, CompileOptions } from './types.js';
import { Command } from "commander";
import { serveCommand } from "./commands/serve.js";
import { compileCommand } from "./commands/compile.js";
import { mcpCommand } from "./commands/mcp.js";
import type { ServeOptions, CompileOptions } from "./types.js";
const program = new Command();
program
.name('ttrpg')
.description('TTRPG 工具箱 - 用于编译和预览 TTRPG 文档')
.version('0.0.1');
.name("ttrpg")
.description("TTRPG 工具箱 - 用于编译和预览 TTRPG 文档")
.version("0.0.1");
program
.command('serve')
.description('运行一个 web 服务器预览目录中的内容,并实时监听更新')
.argument('[dir]', '要预览的目录', '.')
.option('-p, --port <port>', '端口号', '3000')
.option('-h, --host <host>', '主机地址', '0.0.0.0')
.command("serve")
.description("运行一个 web 服务器预览目录中的内容,并实时监听更新")
.argument("[dir]", "要预览的目录", ".")
.option("-p, --port <port>", "HTTP 端口号", "3000")
.option("--mqtt-port <port>", "MQTT 代理端口号", "1883")
.option("-h, --host <host>", "主机地址", "0.0.0.0")
.action(serveCommand);
program
.command('compile')
.description('将目录中的内容输出为带 hash 路由、单个 html 入口的 web 应用')
.argument('[dir]', '要编译的目录', '.')
.option('-o, --output <dir>', '输出目录', './dist/output')
.command("compile")
.description("将目录中的内容输出为带 hash 路由、单个 html 入口的 web 应用")
.argument("[dir]", "要编译的目录", ".")
.option("-o, --output <dir>", "输出目录", "./dist/output")
.action(compileCommand);
program
.addCommand(mcpCommand);
program.addCommand(mcpCommand);
program.parse();

227
src/cli/journal.ts Normal file
View File

@ -0,0 +1,227 @@
/**
* Journal stream persistence MQTT broker + JSONL append + session manifest
*
* Starts an aedes MQTT broker and connects to itself to:
* 1. Persist all stream messages to .ttrpg/sessions/{id}/stream.jsonl
* 2. Manage session lifecycle via retained meta topics
*
* No HTTP routes the JSONL files are served by the existing static server.
*/
import { join, resolve } from "path";
import {
appendFileSync,
existsSync,
mkdirSync,
readFileSync,
rmdirSync,
unlinkSync,
writeFileSync,
} from "fs";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface SessionMeta {
name: string;
created: number;
players: string[];
}
interface SessionManifest {
sessions: Record<string, SessionMeta>;
}
export interface JournalServer {
/** MQTT broker instance */
broker: import("aedes").Aedes;
/** TCP server the broker is attached to */
tcpServer: import("net").Server;
/** Persistence client (server subscribes to itself) */
persistenceClient: import("mqtt").MqttClient | null;
/** Clean shutdown */
close(): Promise<void>;
}
// ---------------------------------------------------------------------------
// Path helpers
// ---------------------------------------------------------------------------
function dataDir(contentDir: string): string {
const d = resolve(contentDir, ".ttrpg");
mkdirSync(d, { recursive: true });
return d;
}
function manifestPath(dataRoot: string): string {
return join(dataRoot, "manifest.json");
}
function sessionDir(dataRoot: string, id: string): string {
return join(dataRoot, "sessions", id);
}
function streamPath(dataRoot: string, id: string): string {
return join(sessionDir(dataRoot, id), "stream.jsonl");
}
// ---------------------------------------------------------------------------
// Manifest
// ---------------------------------------------------------------------------
function loadManifest(dataRoot: string): SessionManifest {
const p = manifestPath(dataRoot);
if (!existsSync(p)) return { sessions: {} };
try {
return JSON.parse(readFileSync(p, "utf-8"));
} catch {
return { sessions: {} };
}
}
function saveManifest(dataRoot: string, m: SessionManifest): void {
writeFileSync(manifestPath(dataRoot), JSON.stringify(m, null, 2), "utf-8");
}
// ---------------------------------------------------------------------------
// JSONL persistence
// ---------------------------------------------------------------------------
function appendStream(dataRoot: string, id: string, line: string): void {
const sd = sessionDir(dataRoot, id);
mkdirSync(sd, { recursive: true });
appendFileSync(streamPath(dataRoot, id), line + "\n", "utf-8");
}
// ---------------------------------------------------------------------------
// Server factory
// ---------------------------------------------------------------------------
export async function createJournalServer(
contentDir: string,
mqttPort: number,
): Promise<JournalServer> {
const root = dataDir(contentDir);
console.log(`[journal] data dir: ${root}`);
// ---- MQTT Broker ----
const { Aedes: AedesFactory } = await import("aedes");
const { createServer } = await import("net");
const broker = new AedesFactory();
const tcpServer = createServer(broker.handle.bind(broker));
tcpServer.listen(mqttPort, "0.0.0.0", () => {
console.log(`[journal] MQTT broker on port ${mqttPort}`);
});
// ---- Persistence + Manifest client ----
let client: import("mqtt").MqttClient | null = null;
const { default: mqtt } = await import("mqtt");
client = mqtt.connect(`mqtt://127.0.0.1:${mqttPort}`, {
clientId: "journal-server",
});
client.on("connect", () => {
// 1. Persistence: append every stream message to JSONL
client!.subscribe("ttrpg/+/stream", { qos: 1 }, (err) => {
if (err) console.error("[journal] stream sub err:", err);
});
// 2. Session meta: manage manifest from retained meta topics
client!.subscribe("ttrpg/+/meta", { qos: 1 }, (err) => {
if (err) console.error("[journal] meta sub err:", err);
});
});
client.on("message", (topic, payload) => {
const parts = topic.split("/");
if (parts.length < 3) return;
const sessionId = parts[1];
const subtopic = parts[2];
if (subtopic === "stream") {
// Persist every stream message
try {
appendStream(root, sessionId, payload.toString());
} catch (e) {
console.error(`[journal] stream append err for ${sessionId}:`, e);
}
} else if (subtopic === "meta") {
// Session lifecycle
handleMetaChange(root, sessionId, client!, payload.toString());
}
});
console.log("[journal] persistence listener active");
return {
broker,
tcpServer,
persistenceClient: client,
async close() {
console.log("[journal] shutting down...");
client?.end(true);
await new Promise<void>((r) => broker.close(() => r()));
await new Promise<void>((r) => tcpServer.close(() => r()));
},
};
}
// ---------------------------------------------------------------------------
// Session lifecycle
// ---------------------------------------------------------------------------
const $SESSIONS = "ttrpg/$SESSIONS";
function handleMetaChange(
root: string,
sessionId: string,
client: import("mqtt").MqttClient,
rawPayload: string,
): void {
const manifest = loadManifest(root);
try {
const meta = JSON.parse(rawPayload) as SessionMeta | null;
if (!meta || !meta.name) {
// Tombstone: empty or invalid payload → delete session
if (manifest.sessions[sessionId]) {
delete manifest.sessions[sessionId];
// Wipe files
const sp = streamPath(root, sessionId);
if (existsSync(sp)) unlinkSync(sp);
const sd = sessionDir(root, sessionId);
try {
rmdirSync(sd);
} catch {
/* not empty, fine */
}
console.log(`[journal] session deleted: ${sessionId}`);
}
} else {
// Create or update
manifest.sessions[sessionId] = {
name: meta.name,
created: meta.created || Date.now(),
players: meta.players || [],
};
console.log(`[journal] session updated: ${sessionId} (${meta.name})`);
}
saveManifest(root, manifest);
// Republish full manifest as a retained message
client.publish($SESSIONS, JSON.stringify(manifest), {
qos: 1,
retain: true,
});
} catch (e) {
console.error(`[journal] meta parse err for ${sessionId}:`, e);
}
}

View File

@ -1,14 +1,21 @@
export interface ServeOptions {
port: string;
host?: string;
mqttPort?: string;
}
export interface CompileOptions {
output: string;
}
export type ServeCommandHandler = (dir: string, options: ServeOptions) => Promise<void>;
export type CompileCommandHandler = (dir: string, options: CompileOptions) => Promise<void>;
export type ServeCommandHandler = (
dir: string,
options: ServeOptions,
) => Promise<void>;
export type CompileCommandHandler = (
dir: string,
options: CompileOptions,
) => Promise<void>;
export interface MarkdownFile {
path: string;