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:
@@ -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);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user