feat: add completions API and scanning logic

Implement a new completions system that scans markdown files for
dice notation and headings. The server now exposes a
`/__COMPLETIONS.json`
endpoint and automatically recomputes the completion index when
files are added, updated, or deleted.
This commit is contained in:
2026-07-06 16:50:45 +08:00
parent 307a3c8320
commit 8284f5caee
5 changed files with 195 additions and 0 deletions
+30
View File
@@ -6,6 +6,10 @@ import { join, resolve, extname, sep, relative, dirname } from "path";
import { watch } from "chokidar";
import { fileURLToPath } from "url";
import { createJournalServer } from "../journal.js";
import {
scanCompletions,
type CompletionsPayload,
} from "../completions/index.js";
interface ContentIndex {
[path: string]: string;
@@ -144,6 +148,7 @@ function createRequestHandler(
contentDir: string,
distDir: string,
getIndex: () => ContentIndex,
getCompletions: () => CompletionsPayload,
) {
return (req: IncomingMessage, res: ServerResponse) => {
const url = req.url || "/";
@@ -155,6 +160,12 @@ function createRequestHandler(
return;
}
// 1b. 处理 /__COMPLETIONS.json
if (filePath === "/__COMPLETIONS.json") {
sendJson(res, getCompletions());
return;
}
// 2. 处理 /static/ 目录(从 dist/web)
if (filePath.startsWith("/static/")) {
if (tryServeStatic(res, filePath, distDir)) {
@@ -196,6 +207,10 @@ export interface ContentServer {
* 内容索引
*/
index: ContentIndex;
/**
* 补全数据
*/
completions: CompletionsPayload;
/**
* 关闭服务器
*/
@@ -212,11 +227,21 @@ export function createContentServer(
host: string = "0.0.0.0",
): ContentServer {
let contentIndex: ContentIndex = {};
let completionsIndex: CompletionsPayload = { dice: [], links: [] };
/** Re-scan completions from current content index (cached) */
function recomputeCompletions(): void {
completionsIndex = scanCompletions(contentIndex);
console.log(
`[completions] dice=${completionsIndex.dice.length} links=${completionsIndex.links.length}`,
);
}
// 扫描内容目录生成索引
console.log("扫描内容目录...");
contentIndex = scanDirectory(contentDir);
console.log(`已索引 ${Object.keys(contentIndex).length} 个文件`);
recomputeCompletions();
// 监听文件变化
console.log("监听文件变化...");
@@ -238,6 +263,7 @@ export function createContentServer(
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
contentIndex[relPath] = content;
console.log(`[新增] ${relPath}`);
if (relPath.endsWith(".md")) recomputeCompletions();
} catch (e) {
console.error(`读取新增文件失败:${path}`, e);
}
@@ -254,6 +280,7 @@ export function createContentServer(
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
contentIndex[relPath] = content;
console.log(`[更新] ${relPath}`);
if (relPath.endsWith(".md")) recomputeCompletions();
} catch (e) {
console.error(`读取更新文件失败:${path}`, e);
}
@@ -268,6 +295,7 @@ export function createContentServer(
const relPath = "/" + relative(contentDir, path).split(sep).join("/");
delete contentIndex[relPath];
console.log(`[删除] ${relPath}`);
if (relPath.endsWith(".md")) recomputeCompletions();
}
});
@@ -279,6 +307,7 @@ export function createContentServer(
contentDir,
distPath,
() => contentIndex,
() => completionsIndex,
);
const server = createServer(handleRequest);
@@ -298,6 +327,7 @@ export function createContentServer(
server,
watcher,
index: contentIndex,
completions: completionsIndex,
close() {
console.log("关闭内容服务器...");
server.close();