@biaoo/tiangong-wiki 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/README.zh-CN.md +167 -0
- package/SKILL.md +116 -0
- package/agents/openai.yaml +4 -0
- package/assets/config.example.env +18 -0
- package/assets/templates/achievement.md +32 -0
- package/assets/templates/bridge.md +33 -0
- package/assets/templates/concept.md +47 -0
- package/assets/templates/faq.md +31 -0
- package/assets/templates/lesson.md +31 -0
- package/assets/templates/method.md +31 -0
- package/assets/templates/misconception.md +35 -0
- package/assets/templates/person.md +31 -0
- package/assets/templates/research-note.md +34 -0
- package/assets/templates/resume.md +34 -0
- package/assets/templates/source-summary.md +35 -0
- package/assets/vllm/qwen3_5_openai_developer.jinja +182 -0
- package/assets/wiki.config.default.json +193 -0
- package/dist/commands/check-config.js +77 -0
- package/dist/commands/create.js +32 -0
- package/dist/commands/daemon.js +186 -0
- package/dist/commands/dashboard.js +112 -0
- package/dist/commands/doctor.js +22 -0
- package/dist/commands/export-graph.js +28 -0
- package/dist/commands/export-index.js +31 -0
- package/dist/commands/find.js +36 -0
- package/dist/commands/fts.js +32 -0
- package/dist/commands/graph.js +35 -0
- package/dist/commands/init.js +48 -0
- package/dist/commands/lint.js +35 -0
- package/dist/commands/list.js +28 -0
- package/dist/commands/page-info.js +24 -0
- package/dist/commands/search.js +32 -0
- package/dist/commands/setup.js +15 -0
- package/dist/commands/stat.js +20 -0
- package/dist/commands/sync.js +38 -0
- package/dist/commands/template.js +71 -0
- package/dist/commands/type.js +88 -0
- package/dist/commands/vault.js +64 -0
- package/dist/core/agent.js +201 -0
- package/dist/core/cli-env.js +129 -0
- package/dist/core/codex-workflow.js +233 -0
- package/dist/core/config.js +126 -0
- package/dist/core/db.js +292 -0
- package/dist/core/embedding.js +104 -0
- package/dist/core/frontmatter.js +287 -0
- package/dist/core/indexer.js +241 -0
- package/dist/core/onboarding.js +967 -0
- package/dist/core/page-files.js +91 -0
- package/dist/core/paths.js +161 -0
- package/dist/core/presenters.js +23 -0
- package/dist/core/query.js +58 -0
- package/dist/core/runtime.js +20 -0
- package/dist/core/sync.js +235 -0
- package/dist/core/synology.js +412 -0
- package/dist/core/template-evolution.js +38 -0
- package/dist/core/vault-processing.js +742 -0
- package/dist/core/vault.js +594 -0
- package/dist/core/workflow-context.js +188 -0
- package/dist/core/workflow-result.js +162 -0
- package/dist/core/workspace-bootstrap.js +30 -0
- package/dist/core/workspace-skills.js +220 -0
- package/dist/daemon/client.js +147 -0
- package/dist/daemon/server.js +807 -0
- package/dist/daemon/state.js +53 -0
- package/dist/dashboard/assets/index-1FgAUZ28.css +1 -0
- package/dist/dashboard/assets/index-6A0PWT4X.js +154 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
- package/dist/dashboard/index.html +18 -0
- package/dist/index.js +86 -0
- package/dist/operations/dashboard.js +1231 -0
- package/dist/operations/export.js +110 -0
- package/dist/operations/query.js +649 -0
- package/dist/operations/type-template.js +210 -0
- package/dist/operations/write.js +143 -0
- package/dist/types/config.js +1 -0
- package/dist/types/page.js +1 -0
- package/dist/utils/case.js +22 -0
- package/dist/utils/errors.js +26 -0
- package/dist/utils/fs.js +77 -0
- package/dist/utils/output.js +33 -0
- package/dist/utils/process.js +60 -0
- package/dist/utils/segmenter.js +24 -0
- package/dist/utils/slug.js +10 -0
- package/dist/utils/time.js +24 -0
- package/package.json +64 -0
- package/references/cli-interface.md +312 -0
- package/references/env.md +122 -0
- package/references/template-design-guide.md +271 -0
- package/references/vault-to-wiki-instruction.md +110 -0
- package/references/wiki-maintenance-instruction.md +190 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getMeta } from "../core/db.js";
|
|
5
|
+
import { openRuntimeDb } from "../core/runtime.js";
|
|
6
|
+
import { exportGraphContent, exportIndexContent } from "../operations/export.js";
|
|
7
|
+
import { getDashboardGraphOverview, getDashboardLintSummary, getDashboardPageDetail, getDashboardPageSource, getDashboardQueueItemDetail, getDashboardQueueSummary, getDashboardStatus, getDashboardVaultFileDetail, getDashboardVaultSummary, listDashboardLintIssues, listDashboardQueueItems, listDashboardVaultFiles, openDashboardPageSource, openDashboardVaultFile, retryDashboardQueueItem, searchDashboardGraph, } from "../operations/dashboard.js";
|
|
8
|
+
import { diffVaultFiles, findPages, ftsSearchPages, getPageInfo, getVaultQueue, getWikiStat, listPages, listVaultFiles, renderLintResult, runLint, searchPages, traverseGraph, } from "../operations/query.js";
|
|
9
|
+
import { createTemplate, listTemplates, listTypes, recommendTypes, showTemplate, showType, } from "../operations/type-template.js";
|
|
10
|
+
import { createPage, runSync, runSyncCommand } from "../operations/write.js";
|
|
11
|
+
import { AppError, asAppError } from "../utils/errors.js";
|
|
12
|
+
import { pathExistsSync } from "../utils/fs.js";
|
|
13
|
+
import { addSeconds, toOffsetIso } from "../utils/time.js";
|
|
14
|
+
import { resolveRuntimePaths } from "../core/paths.js";
|
|
15
|
+
import { processVaultQueueBatch } from "../core/vault-processing.js";
|
|
16
|
+
import { clearDaemonArtifacts, createInitialDaemonState, writeDaemonPid, writeDaemonState } from "./state.js";
|
|
17
|
+
function logInfo(message) {
|
|
18
|
+
console.log(`[${toOffsetIso()}] ${message}`);
|
|
19
|
+
}
|
|
20
|
+
function logError(message) {
|
|
21
|
+
console.error(`[${toOffsetIso()}] ${message}`);
|
|
22
|
+
}
|
|
23
|
+
function writeJsonResponse(response, statusCode, payload) {
|
|
24
|
+
response.writeHead(statusCode, { "content-type": "application/json; charset=utf-8" });
|
|
25
|
+
response.end(`${JSON.stringify(payload, null, 2)}\n`);
|
|
26
|
+
}
|
|
27
|
+
function writeTextResponse(response, statusCode, contentType, payload) {
|
|
28
|
+
response.writeHead(statusCode, { "content-type": contentType });
|
|
29
|
+
response.end(payload);
|
|
30
|
+
}
|
|
31
|
+
function decodePathParam(value) {
|
|
32
|
+
try {
|
|
33
|
+
return decodeURIComponent(value);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new AppError(`Failed to decode dashboard route parameter: ${error instanceof Error ? error.message : String(error)}`, "config");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function parsePositiveIntegerParam(value, fallback) {
|
|
40
|
+
if (!value || !value.trim()) {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
const parsed = Number.parseInt(value, 10);
|
|
44
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
45
|
+
throw new AppError(`Expected a positive integer parameter, got ${value}`, "config");
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
function contentTypeForPath(filePath) {
|
|
50
|
+
switch (path.extname(filePath).toLowerCase()) {
|
|
51
|
+
case ".html":
|
|
52
|
+
return "text/html; charset=utf-8";
|
|
53
|
+
case ".js":
|
|
54
|
+
return "text/javascript; charset=utf-8";
|
|
55
|
+
case ".css":
|
|
56
|
+
return "text/css; charset=utf-8";
|
|
57
|
+
case ".json":
|
|
58
|
+
return "application/json; charset=utf-8";
|
|
59
|
+
case ".svg":
|
|
60
|
+
return "image/svg+xml";
|
|
61
|
+
case ".png":
|
|
62
|
+
return "image/png";
|
|
63
|
+
case ".jpg":
|
|
64
|
+
case ".jpeg":
|
|
65
|
+
return "image/jpeg";
|
|
66
|
+
case ".webp":
|
|
67
|
+
return "image/webp";
|
|
68
|
+
case ".woff":
|
|
69
|
+
return "font/woff";
|
|
70
|
+
case ".woff2":
|
|
71
|
+
return "font/woff2";
|
|
72
|
+
case ".ttf":
|
|
73
|
+
return "font/ttf";
|
|
74
|
+
case ".ico":
|
|
75
|
+
return "image/x-icon";
|
|
76
|
+
default:
|
|
77
|
+
return "application/octet-stream";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function extractDashboardLogFileId(message) {
|
|
81
|
+
const match = message.match(/^([^:\s][^:]*)\s*:/);
|
|
82
|
+
return match?.[1] ?? null;
|
|
83
|
+
}
|
|
84
|
+
function matchesDashboardLog(entry, filters) {
|
|
85
|
+
if (filters.level && entry.level !== filters.level) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (filters.fileId && entry.fileId !== filters.fileId) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (filters.query) {
|
|
92
|
+
const haystack = `${entry.message} ${entry.line} ${entry.fileId ?? ""}`.toLowerCase();
|
|
93
|
+
return haystack.includes(filters.query);
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
function writeSseEvent(response, event, payload) {
|
|
98
|
+
response.write(`event: ${event}\n`);
|
|
99
|
+
response.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
100
|
+
}
|
|
101
|
+
async function readJsonBody(request) {
|
|
102
|
+
const chunks = [];
|
|
103
|
+
for await (const chunk of request) {
|
|
104
|
+
chunks.push(Buffer.from(chunk));
|
|
105
|
+
}
|
|
106
|
+
if (chunks.length === 0) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw new AppError(`Failed to parse daemon request body: ${error instanceof Error ? error.message : String(error)}`, "config");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function isBusyError(error) {
|
|
117
|
+
return (typeof error.details === "object" &&
|
|
118
|
+
error.details !== null &&
|
|
119
|
+
"code" in error.details &&
|
|
120
|
+
error.details.code === "busy");
|
|
121
|
+
}
|
|
122
|
+
async function buildStatusPayload(env, state) {
|
|
123
|
+
let lastSyncAt = null;
|
|
124
|
+
try {
|
|
125
|
+
const { db } = openRuntimeDb(env);
|
|
126
|
+
try {
|
|
127
|
+
lastSyncAt = getMeta(db, "last_sync_at");
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
db.close();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
lastSyncAt = null;
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
running: true,
|
|
138
|
+
pid: state?.pid ?? process.pid,
|
|
139
|
+
host: state?.host ?? null,
|
|
140
|
+
port: state?.port ?? null,
|
|
141
|
+
lastSyncAt,
|
|
142
|
+
nextSyncAt: state?.nextRunAt ?? null,
|
|
143
|
+
lastResult: state?.lastResult ?? null,
|
|
144
|
+
syncIntervalSeconds: state?.syncIntervalSeconds ?? null,
|
|
145
|
+
launchMode: state?.launchMode ?? null,
|
|
146
|
+
currentTask: state?.currentTask ?? null,
|
|
147
|
+
state,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export async function runDaemonServer(options) {
|
|
151
|
+
const env = options.env ?? process.env;
|
|
152
|
+
const paths = resolveRuntimePaths(env);
|
|
153
|
+
const interval = paths.syncIntervalSeconds;
|
|
154
|
+
let state = null;
|
|
155
|
+
let cycleTimer = null;
|
|
156
|
+
let queuedCycle = false;
|
|
157
|
+
let stopping = false;
|
|
158
|
+
let currentWrite = null;
|
|
159
|
+
let server;
|
|
160
|
+
let resolveClosed = null;
|
|
161
|
+
let nextDashboardLogId = 1;
|
|
162
|
+
const dashboardDistPath = path.join(paths.packageRoot, "dist", "dashboard");
|
|
163
|
+
const dashboardLogHistory = [];
|
|
164
|
+
const dashboardLogClients = new Set();
|
|
165
|
+
const broadcastDashboardLog = (entry) => {
|
|
166
|
+
dashboardLogHistory.push(entry);
|
|
167
|
+
if (dashboardLogHistory.length > 400) {
|
|
168
|
+
dashboardLogHistory.shift();
|
|
169
|
+
}
|
|
170
|
+
for (const client of dashboardLogClients) {
|
|
171
|
+
if (matchesDashboardLog(entry, client.filters)) {
|
|
172
|
+
writeSseEvent(client.response, "log", entry);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const logInfo = (message) => {
|
|
177
|
+
const timestamp = toOffsetIso();
|
|
178
|
+
const entry = {
|
|
179
|
+
id: nextDashboardLogId++,
|
|
180
|
+
timestamp,
|
|
181
|
+
level: "info",
|
|
182
|
+
message,
|
|
183
|
+
line: `[${timestamp}] ${message}`,
|
|
184
|
+
fileId: extractDashboardLogFileId(message),
|
|
185
|
+
};
|
|
186
|
+
broadcastDashboardLog(entry);
|
|
187
|
+
console.log(entry.line);
|
|
188
|
+
};
|
|
189
|
+
const logError = (message) => {
|
|
190
|
+
const timestamp = toOffsetIso();
|
|
191
|
+
const entry = {
|
|
192
|
+
id: nextDashboardLogId++,
|
|
193
|
+
timestamp,
|
|
194
|
+
level: "error",
|
|
195
|
+
message,
|
|
196
|
+
line: `[${timestamp}] ${message}`,
|
|
197
|
+
fileId: extractDashboardLogFileId(message),
|
|
198
|
+
};
|
|
199
|
+
broadcastDashboardLog(entry);
|
|
200
|
+
console.error(entry.line);
|
|
201
|
+
};
|
|
202
|
+
const serveDashboardApp = (requestPath, response) => {
|
|
203
|
+
if (!pathExistsSync(dashboardDistPath)) {
|
|
204
|
+
throw new AppError(`Dashboard assets not found at ${dashboardDistPath}. Build the dashboard bundle before opening /dashboard.`, "not_found");
|
|
205
|
+
}
|
|
206
|
+
const relativeRequestPath = requestPath === "/dashboard" ? "" : requestPath.replace(/^\/dashboard\/?/, "");
|
|
207
|
+
const decodedRequestPath = relativeRequestPath ? decodePathParam(relativeRequestPath) : "";
|
|
208
|
+
const requestedFilePath = decodedRequestPath
|
|
209
|
+
? path.resolve(dashboardDistPath, decodedRequestPath)
|
|
210
|
+
: path.join(dashboardDistPath, "index.html");
|
|
211
|
+
const safeRelative = path.relative(dashboardDistPath, requestedFilePath);
|
|
212
|
+
if (safeRelative.startsWith("..") || path.isAbsolute(safeRelative)) {
|
|
213
|
+
throw new AppError(`Dashboard path is outside dist root: ${requestPath}`, "config");
|
|
214
|
+
}
|
|
215
|
+
const requestedLooksLikeFile = path.extname(decodedRequestPath) !== "";
|
|
216
|
+
const filePath = requestedLooksLikeFile
|
|
217
|
+
? requestedFilePath
|
|
218
|
+
: path.join(dashboardDistPath, "index.html");
|
|
219
|
+
if (!pathExistsSync(filePath)) {
|
|
220
|
+
throw new AppError(requestedLooksLikeFile
|
|
221
|
+
? `Dashboard asset not found at ${filePath}`
|
|
222
|
+
: `Dashboard entrypoint missing at ${filePath}`, "not_found");
|
|
223
|
+
}
|
|
224
|
+
writeTextResponse(response, 200, contentTypeForPath(filePath), readFileSync(filePath));
|
|
225
|
+
};
|
|
226
|
+
const streamDashboardLogs = (request, response, url) => {
|
|
227
|
+
const filters = {
|
|
228
|
+
level: (() => {
|
|
229
|
+
const level = url.searchParams.get("level");
|
|
230
|
+
if (!level) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
if (level === "info" || level === "error") {
|
|
234
|
+
return level;
|
|
235
|
+
}
|
|
236
|
+
throw new AppError(`Unsupported dashboard log level: ${level}`, "config");
|
|
237
|
+
})(),
|
|
238
|
+
query: (url.searchParams.get("query") ?? "").trim().toLowerCase(),
|
|
239
|
+
fileId: (() => {
|
|
240
|
+
const rawFileId = url.searchParams.get("fileId");
|
|
241
|
+
return rawFileId && rawFileId.trim() ? rawFileId.trim() : null;
|
|
242
|
+
})(),
|
|
243
|
+
};
|
|
244
|
+
const historyLimit = parsePositiveIntegerParam(url.searchParams.get("history"), 120);
|
|
245
|
+
response.writeHead(200, {
|
|
246
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
247
|
+
"cache-control": "no-cache, no-transform",
|
|
248
|
+
connection: "keep-alive",
|
|
249
|
+
});
|
|
250
|
+
response.write("retry: 1000\n\n");
|
|
251
|
+
const client = {
|
|
252
|
+
response,
|
|
253
|
+
filters,
|
|
254
|
+
heartbeat: setInterval(() => {
|
|
255
|
+
response.write(": ping\n\n");
|
|
256
|
+
}, 15_000),
|
|
257
|
+
};
|
|
258
|
+
dashboardLogClients.add(client);
|
|
259
|
+
const history = dashboardLogHistory
|
|
260
|
+
.filter((entry) => matchesDashboardLog(entry, filters))
|
|
261
|
+
.slice(-historyLimit);
|
|
262
|
+
writeSseEvent(response, "history", history);
|
|
263
|
+
const cleanup = () => {
|
|
264
|
+
clearInterval(client.heartbeat);
|
|
265
|
+
dashboardLogClients.delete(client);
|
|
266
|
+
};
|
|
267
|
+
request.on("close", cleanup);
|
|
268
|
+
response.on("close", cleanup);
|
|
269
|
+
};
|
|
270
|
+
const persistState = () => {
|
|
271
|
+
if (state) {
|
|
272
|
+
writeDaemonState(paths.daemonStatePath, state);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const clearTimer = () => {
|
|
276
|
+
if (cycleTimer) {
|
|
277
|
+
clearTimeout(cycleTimer);
|
|
278
|
+
cycleTimer = null;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
const scheduleNextCycle = () => {
|
|
282
|
+
clearTimer();
|
|
283
|
+
if (stopping || interval <= 0 || !state) {
|
|
284
|
+
if (state) {
|
|
285
|
+
state.nextRunAt = null;
|
|
286
|
+
persistState();
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const nextRun = addSeconds(new Date(), interval);
|
|
291
|
+
state.nextRunAt = toOffsetIso(nextRun);
|
|
292
|
+
persistState();
|
|
293
|
+
cycleTimer = setTimeout(() => {
|
|
294
|
+
cycleTimer = null;
|
|
295
|
+
if (stopping) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (currentWrite) {
|
|
299
|
+
queuedCycle = true;
|
|
300
|
+
if (state) {
|
|
301
|
+
state.nextRunAt = null;
|
|
302
|
+
persistState();
|
|
303
|
+
}
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
void runDefaultCycle("cycle").catch((error) => {
|
|
307
|
+
const appError = asAppError(error);
|
|
308
|
+
logError(`scheduled cycle failed: ${appError.message}`);
|
|
309
|
+
});
|
|
310
|
+
}, interval * 1000);
|
|
311
|
+
};
|
|
312
|
+
const afterWriteComplete = (task) => {
|
|
313
|
+
if (queuedCycle && !stopping) {
|
|
314
|
+
queuedCycle = false;
|
|
315
|
+
void runDefaultCycle("cycle").catch((error) => {
|
|
316
|
+
const appError = asAppError(error);
|
|
317
|
+
logError(`queued cycle failed: ${appError.message}`);
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (task === "cycle" || task === "sync-trigger") {
|
|
322
|
+
scheduleNextCycle();
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
persistState();
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const runWriteTask = async (task, run) => {
|
|
329
|
+
if (stopping) {
|
|
330
|
+
throw new AppError("Wiki daemon is shutting down.", "runtime");
|
|
331
|
+
}
|
|
332
|
+
if (currentWrite) {
|
|
333
|
+
throw new AppError(`Wiki daemon is busy running ${state?.currentTask ?? "another task"}.`, "runtime", {
|
|
334
|
+
code: "busy",
|
|
335
|
+
currentTask: state?.currentTask ?? "unknown",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
const promise = (async () => {
|
|
339
|
+
if (state) {
|
|
340
|
+
state.currentTask = task;
|
|
341
|
+
if (task === "cycle" || task === "sync-trigger") {
|
|
342
|
+
state.nextRunAt = null;
|
|
343
|
+
}
|
|
344
|
+
persistState();
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const result = await run();
|
|
348
|
+
if (state) {
|
|
349
|
+
state.lastRunAt = toOffsetIso();
|
|
350
|
+
state.lastResult = "ok";
|
|
351
|
+
state.lastError = null;
|
|
352
|
+
state.currentTask = "idle";
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
const appError = asAppError(error);
|
|
358
|
+
if (state) {
|
|
359
|
+
state.lastRunAt = toOffsetIso();
|
|
360
|
+
state.lastResult = "error";
|
|
361
|
+
state.lastError = appError.message;
|
|
362
|
+
state.currentTask = "idle";
|
|
363
|
+
}
|
|
364
|
+
throw appError;
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
currentWrite = null;
|
|
368
|
+
afterWriteComplete(task);
|
|
369
|
+
}
|
|
370
|
+
})();
|
|
371
|
+
currentWrite = promise;
|
|
372
|
+
return promise;
|
|
373
|
+
};
|
|
374
|
+
const runDefaultCycle = async (task) => {
|
|
375
|
+
return runWriteTask(task, async () => {
|
|
376
|
+
logInfo(`${task}: start`);
|
|
377
|
+
const syncResult = await runSync(env);
|
|
378
|
+
logInfo(`${task}: sync ok mode=${syncResult.mode} inserted=${syncResult.inserted} updated=${syncResult.updated} deleted=${syncResult.deleted} vaultChanges=${syncResult.vault.changes}`);
|
|
379
|
+
const queueResult = {
|
|
380
|
+
enabled: false,
|
|
381
|
+
processed: 0,
|
|
382
|
+
done: 0,
|
|
383
|
+
skipped: 0,
|
|
384
|
+
errored: 0,
|
|
385
|
+
batches: 0,
|
|
386
|
+
};
|
|
387
|
+
while (!stopping) {
|
|
388
|
+
const batchResult = await processVaultQueueBatch(env, {
|
|
389
|
+
log: (message) => logInfo(`queue ${message}`),
|
|
390
|
+
});
|
|
391
|
+
if (!batchResult.enabled) {
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
queueResult.enabled = true;
|
|
395
|
+
if (batchResult.processed === 0) {
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
queueResult.processed += batchResult.processed;
|
|
399
|
+
queueResult.done += batchResult.done;
|
|
400
|
+
queueResult.skipped += batchResult.skipped;
|
|
401
|
+
queueResult.errored += batchResult.errored;
|
|
402
|
+
queueResult.batches += 1;
|
|
403
|
+
}
|
|
404
|
+
if (queueResult.enabled) {
|
|
405
|
+
logInfo(`${task}: queue summary processed=${queueResult.processed} done=${queueResult.done} skipped=${queueResult.skipped} errored=${queueResult.errored} batches=${queueResult.batches}`);
|
|
406
|
+
}
|
|
407
|
+
return {
|
|
408
|
+
status: "started",
|
|
409
|
+
task,
|
|
410
|
+
sync: syncResult,
|
|
411
|
+
queue: queueResult,
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
const beginShutdown = async () => {
|
|
416
|
+
if (stopping) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
stopping = true;
|
|
420
|
+
clearTimer();
|
|
421
|
+
if (state) {
|
|
422
|
+
state.currentTask = "shutdown";
|
|
423
|
+
state.nextRunAt = null;
|
|
424
|
+
persistState();
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
await currentWrite?.catch(() => undefined);
|
|
428
|
+
}
|
|
429
|
+
finally {
|
|
430
|
+
await new Promise((resolve) => {
|
|
431
|
+
server.close(() => resolve());
|
|
432
|
+
});
|
|
433
|
+
clearDaemonArtifacts(paths);
|
|
434
|
+
resolveClosed?.();
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
const handleRequest = async (request, response) => {
|
|
438
|
+
const url = new URL(request.url ?? "/", `http://${paths.daemonHost}`);
|
|
439
|
+
const pathname = url.pathname;
|
|
440
|
+
const method = request.method ?? "GET";
|
|
441
|
+
try {
|
|
442
|
+
if (method === "GET" && pathname === "/health") {
|
|
443
|
+
writeJsonResponse(response, 200, {
|
|
444
|
+
ok: true,
|
|
445
|
+
service: "tiangong-wiki-daemon",
|
|
446
|
+
pid: state?.pid ?? process.pid,
|
|
447
|
+
host: state?.host ?? paths.daemonHost,
|
|
448
|
+
port: state?.port ?? null,
|
|
449
|
+
});
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (method === "GET" && pathname === "/status") {
|
|
453
|
+
writeJsonResponse(response, 200, await buildStatusPayload(env, state));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (method === "POST" && pathname === "/shutdown") {
|
|
457
|
+
writeJsonResponse(response, 200, {
|
|
458
|
+
status: "stopping",
|
|
459
|
+
pid: state?.pid ?? process.pid,
|
|
460
|
+
});
|
|
461
|
+
void beginShutdown();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (method === "POST" && pathname === "/sync") {
|
|
465
|
+
const body = await readJsonBody(request);
|
|
466
|
+
const pathValue = typeof body.path === "string" && body.path.trim()
|
|
467
|
+
? body.path.trim()
|
|
468
|
+
: Array.isArray(body.targetPaths) && typeof body.targetPaths[0] === "string"
|
|
469
|
+
? String(body.targetPaths[0])
|
|
470
|
+
: undefined;
|
|
471
|
+
const result = await runWriteTask("sync", async () => runSyncCommand(env, {
|
|
472
|
+
targetPaths: pathValue ? [pathValue] : undefined,
|
|
473
|
+
force: body.force === true,
|
|
474
|
+
skipEmbedding: body.skipEmbedding === true,
|
|
475
|
+
process: body.process === true,
|
|
476
|
+
vaultFileId: typeof body.vaultFileId === "string" && body.vaultFileId.trim() ? body.vaultFileId.trim() : undefined,
|
|
477
|
+
}));
|
|
478
|
+
writeJsonResponse(response, 200, result);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (method === "POST" && pathname === "/sync/trigger") {
|
|
482
|
+
if (currentWrite) {
|
|
483
|
+
throw new AppError(`Wiki daemon is busy running ${state?.currentTask ?? "another task"}.`, "runtime", {
|
|
484
|
+
code: "busy",
|
|
485
|
+
currentTask: state?.currentTask ?? "unknown",
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
void runDefaultCycle("sync-trigger").catch((error) => {
|
|
489
|
+
const appError = asAppError(error);
|
|
490
|
+
logError(`sync-trigger failed: ${appError.message}`);
|
|
491
|
+
});
|
|
492
|
+
writeJsonResponse(response, 200, {
|
|
493
|
+
status: "started",
|
|
494
|
+
currentTask: "sync-trigger",
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (method === "GET" && pathname === "/api/dashboard/status") {
|
|
499
|
+
const daemonStatus = await buildStatusPayload(env, state);
|
|
500
|
+
writeJsonResponse(response, 200, await getDashboardStatus(env, daemonStatus, { probe: false }));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if ((method === "GET" || method === "POST") && pathname === "/api/dashboard/status/refresh") {
|
|
504
|
+
const daemonStatus = await buildStatusPayload(env, state);
|
|
505
|
+
writeJsonResponse(response, 200, await getDashboardStatus(env, daemonStatus, { probe: true }));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (method === "GET" && pathname === "/api/dashboard/graph/overview") {
|
|
509
|
+
writeJsonResponse(response, 200, await getDashboardGraphOverview(env, {
|
|
510
|
+
limit: url.searchParams.get("limit") ?? undefined,
|
|
511
|
+
}));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (method === "GET" && pathname === "/api/dashboard/graph/search") {
|
|
515
|
+
writeJsonResponse(response, 200, await searchDashboardGraph(env, {
|
|
516
|
+
query: url.searchParams.get("query") ?? "",
|
|
517
|
+
limit: url.searchParams.get("limit") ?? undefined,
|
|
518
|
+
}));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (method === "GET" && pathname === "/api/dashboard/queue/summary") {
|
|
522
|
+
writeJsonResponse(response, 200, getDashboardQueueSummary(env));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (method === "GET" && pathname === "/api/dashboard/queue/items") {
|
|
526
|
+
writeJsonResponse(response, 200, listDashboardQueueItems(env, {
|
|
527
|
+
status: url.searchParams.get("status") ?? undefined,
|
|
528
|
+
query: url.searchParams.get("query") ?? undefined,
|
|
529
|
+
sourceType: url.searchParams.get("sourceType") ?? undefined,
|
|
530
|
+
limit: url.searchParams.get("limit") ?? undefined,
|
|
531
|
+
}));
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const queueRetryMatch = pathname.match(/^\/api\/dashboard\/queue\/items\/(.+)\/retry$/);
|
|
535
|
+
if (method === "POST" && queueRetryMatch) {
|
|
536
|
+
const fileId = decodePathParam(queueRetryMatch[1]);
|
|
537
|
+
writeJsonResponse(response, 200, await runWriteTask("queue-retry", async () => retryDashboardQueueItem(env, fileId)));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const queueItemMatch = pathname.match(/^\/api\/dashboard\/queue\/items\/(.+)$/);
|
|
541
|
+
if (method === "GET" && queueItemMatch) {
|
|
542
|
+
const fileId = decodePathParam(queueItemMatch[1]);
|
|
543
|
+
writeJsonResponse(response, 200, getDashboardQueueItemDetail(env, fileId));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const pageOpenSourceMatch = pathname.match(/^\/api\/dashboard\/pages\/(.+)\/open-source$/);
|
|
547
|
+
if (method === "POST" && pageOpenSourceMatch) {
|
|
548
|
+
const pageId = decodePathParam(pageOpenSourceMatch[1]);
|
|
549
|
+
const body = await readJsonBody(request);
|
|
550
|
+
writeJsonResponse(response, 200, await openDashboardPageSource(env, pageId, body.target === "page" ? "page" : "vault"));
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const pageSourceMatch = pathname.match(/^\/api\/dashboard\/pages\/(.+)\/source$/);
|
|
554
|
+
if (method === "GET" && pageSourceMatch) {
|
|
555
|
+
const pageId = decodePathParam(pageSourceMatch[1]);
|
|
556
|
+
writeJsonResponse(response, 200, await getDashboardPageSource(env, pageId));
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
const pageDetailMatch = pathname.match(/^\/api\/dashboard\/pages\/(.+)$/);
|
|
560
|
+
if (method === "GET" && pageDetailMatch) {
|
|
561
|
+
const pageId = decodePathParam(pageDetailMatch[1]);
|
|
562
|
+
writeJsonResponse(response, 200, getDashboardPageDetail(env, pageId));
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (method === "GET" && pathname === "/api/dashboard/vault/summary") {
|
|
566
|
+
writeJsonResponse(response, 200, getDashboardVaultSummary(env));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
if (method === "GET" && pathname === "/api/dashboard/vault/files") {
|
|
570
|
+
writeJsonResponse(response, 200, listDashboardVaultFiles(env, {
|
|
571
|
+
query: url.searchParams.get("query") ?? undefined,
|
|
572
|
+
sourceType: url.searchParams.get("sourceType") ?? undefined,
|
|
573
|
+
queueStatus: url.searchParams.get("queueStatus") ?? undefined,
|
|
574
|
+
limit: url.searchParams.get("limit") ?? undefined,
|
|
575
|
+
}));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const vaultFileOpenMatch = pathname.match(/^\/api\/dashboard\/vault\/files\/(.+)\/open$/);
|
|
579
|
+
if (method === "POST" && vaultFileOpenMatch) {
|
|
580
|
+
const fileId = decodePathParam(vaultFileOpenMatch[1]);
|
|
581
|
+
writeJsonResponse(response, 200, await openDashboardVaultFile(env, fileId));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const vaultFileDetailMatch = pathname.match(/^\/api\/dashboard\/vault\/files\/(.+)$/);
|
|
585
|
+
if (method === "GET" && vaultFileDetailMatch) {
|
|
586
|
+
const fileId = decodePathParam(vaultFileDetailMatch[1]);
|
|
587
|
+
writeJsonResponse(response, 200, await getDashboardVaultFileDetail(env, fileId));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (method === "GET" && pathname === "/api/dashboard/lint/summary") {
|
|
591
|
+
writeJsonResponse(response, 200, getDashboardLintSummary(env));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (method === "GET" && pathname === "/api/dashboard/lint/issues") {
|
|
595
|
+
writeJsonResponse(response, 200, listDashboardLintIssues(env, {
|
|
596
|
+
level: url.searchParams.get("level") ?? undefined,
|
|
597
|
+
groupBy: url.searchParams.get("groupBy") ?? undefined,
|
|
598
|
+
rule: url.searchParams.get("rule") ?? undefined,
|
|
599
|
+
pageId: url.searchParams.get("pageId") ?? undefined,
|
|
600
|
+
}));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (method === "GET" && pathname === "/api/dashboard/logs/stream") {
|
|
604
|
+
streamDashboardLogs(request, response, url);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (method === "GET" && pathname === "/find") {
|
|
608
|
+
writeJsonResponse(response, 200, findPages(env, Object.fromEntries(url.searchParams.entries())));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (method === "GET" && pathname === "/fts") {
|
|
612
|
+
writeJsonResponse(response, 200, ftsSearchPages(env, {
|
|
613
|
+
query: url.searchParams.get("query") ?? "",
|
|
614
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
615
|
+
limit: url.searchParams.get("limit") ?? undefined,
|
|
616
|
+
}));
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (method === "GET" && pathname === "/search") {
|
|
620
|
+
writeJsonResponse(response, 200, await searchPages(env, {
|
|
621
|
+
query: url.searchParams.get("query") ?? "",
|
|
622
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
623
|
+
limit: url.searchParams.get("limit") ?? undefined,
|
|
624
|
+
}));
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (method === "GET" && pathname === "/graph") {
|
|
628
|
+
writeJsonResponse(response, 200, traverseGraph(env, {
|
|
629
|
+
root: url.searchParams.get("root") ?? "",
|
|
630
|
+
depth: url.searchParams.get("depth") ?? undefined,
|
|
631
|
+
edgeType: url.searchParams.get("edgeType") ?? undefined,
|
|
632
|
+
direction: url.searchParams.get("direction") ?? undefined,
|
|
633
|
+
}));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
if (method === "GET" && pathname === "/page-info") {
|
|
637
|
+
const pageId = url.searchParams.get("pageId");
|
|
638
|
+
if (!pageId) {
|
|
639
|
+
throw new AppError("pageId is required", "config");
|
|
640
|
+
}
|
|
641
|
+
writeJsonResponse(response, 200, getPageInfo(env, pageId));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (method === "GET" && pathname === "/list") {
|
|
645
|
+
writeJsonResponse(response, 200, listPages(env, {
|
|
646
|
+
type: url.searchParams.get("type") ?? undefined,
|
|
647
|
+
sort: url.searchParams.get("sort") ?? undefined,
|
|
648
|
+
limit: url.searchParams.get("limit") ?? undefined,
|
|
649
|
+
}));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (method === "GET" && pathname === "/stat") {
|
|
653
|
+
writeJsonResponse(response, 200, getWikiStat(env));
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (method === "GET" && pathname === "/vault/list") {
|
|
657
|
+
writeJsonResponse(response, 200, listVaultFiles(env, {
|
|
658
|
+
path: url.searchParams.get("path") ?? undefined,
|
|
659
|
+
ext: url.searchParams.get("ext") ?? undefined,
|
|
660
|
+
}));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (method === "GET" && pathname === "/vault/diff") {
|
|
664
|
+
writeJsonResponse(response, 200, diffVaultFiles(env, {
|
|
665
|
+
since: url.searchParams.get("since") ?? undefined,
|
|
666
|
+
path: url.searchParams.get("path") ?? undefined,
|
|
667
|
+
}));
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (method === "GET" && pathname === "/vault/queue") {
|
|
671
|
+
writeJsonResponse(response, 200, getVaultQueue(env, {
|
|
672
|
+
status: url.searchParams.get("status") ?? undefined,
|
|
673
|
+
}));
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (method === "POST" && pathname === "/create") {
|
|
677
|
+
const body = await readJsonBody(request);
|
|
678
|
+
const type = typeof body.type === "string" ? body.type : "";
|
|
679
|
+
const title = typeof body.title === "string" ? body.title : "";
|
|
680
|
+
const nodeId = typeof body.nodeId === "string" ? body.nodeId : undefined;
|
|
681
|
+
const result = await runWriteTask("create", () => createPage(env, {
|
|
682
|
+
type,
|
|
683
|
+
title,
|
|
684
|
+
nodeId,
|
|
685
|
+
}));
|
|
686
|
+
writeJsonResponse(response, 200, result);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (method === "GET" && pathname === "/lint") {
|
|
690
|
+
writeJsonResponse(response, 200, runLint(env, {
|
|
691
|
+
path: url.searchParams.get("path") ?? undefined,
|
|
692
|
+
level: url.searchParams.get("level") ?? undefined,
|
|
693
|
+
}));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (method === "GET" && pathname === "/type/list") {
|
|
697
|
+
writeJsonResponse(response, 200, listTypes(env));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (method === "GET" && pathname === "/type/show") {
|
|
701
|
+
const pageType = url.searchParams.get("pageType");
|
|
702
|
+
if (!pageType) {
|
|
703
|
+
throw new AppError("pageType is required", "config");
|
|
704
|
+
}
|
|
705
|
+
writeJsonResponse(response, 200, showType(env, pageType));
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (method === "POST" && pathname === "/type/recommend") {
|
|
709
|
+
const body = await readJsonBody(request);
|
|
710
|
+
writeJsonResponse(response, 200, await recommendTypes(env, {
|
|
711
|
+
text: typeof body.text === "string" ? body.text : "",
|
|
712
|
+
keywords: typeof body.keywords === "string" ? body.keywords : undefined,
|
|
713
|
+
limit: typeof body.limit === "string" || typeof body.limit === "number" ? body.limit : undefined,
|
|
714
|
+
}));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (method === "GET" && pathname === "/template/list") {
|
|
718
|
+
writeJsonResponse(response, 200, listTemplates(env));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
if (method === "GET" && pathname === "/template/show") {
|
|
722
|
+
const pageType = url.searchParams.get("pageType");
|
|
723
|
+
if (!pageType) {
|
|
724
|
+
throw new AppError("pageType is required", "config");
|
|
725
|
+
}
|
|
726
|
+
writeJsonResponse(response, 200, showTemplate(env, pageType));
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (method === "POST" && pathname === "/template/create") {
|
|
730
|
+
const body = await readJsonBody(request);
|
|
731
|
+
const result = await runWriteTask("template-create", () => Promise.resolve(createTemplate(env, {
|
|
732
|
+
type: typeof body.type === "string" ? body.type : "",
|
|
733
|
+
title: typeof body.title === "string" ? body.title : "",
|
|
734
|
+
})));
|
|
735
|
+
writeJsonResponse(response, 200, result);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (method === "POST" && pathname === "/export/index") {
|
|
739
|
+
const body = await readJsonBody(request);
|
|
740
|
+
writeJsonResponse(response, 200, exportIndexContent(env, {
|
|
741
|
+
groupBy: typeof body.groupBy === "string" ? body.groupBy : undefined,
|
|
742
|
+
}));
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (method === "POST" && pathname === "/export/graph") {
|
|
746
|
+
writeJsonResponse(response, 200, exportGraphContent(env));
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (method === "GET" && (pathname === "/dashboard" || pathname.startsWith("/dashboard/"))) {
|
|
750
|
+
serveDashboardApp(pathname, response);
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
throw new AppError(`Unknown daemon route: ${method} ${pathname}`, "not_found");
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
const appError = asAppError(error);
|
|
757
|
+
const statusCode = appError.type === "config"
|
|
758
|
+
? 400
|
|
759
|
+
: appError.type === "not_found"
|
|
760
|
+
? 404
|
|
761
|
+
: isBusyError(appError)
|
|
762
|
+
? 409
|
|
763
|
+
: 500;
|
|
764
|
+
writeJsonResponse(response, statusCode, {
|
|
765
|
+
error: appError.message,
|
|
766
|
+
type: appError.type,
|
|
767
|
+
...(appError.details === undefined ? {} : { details: appError.details }),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
server = http.createServer((request, response) => {
|
|
772
|
+
void handleRequest(request, response);
|
|
773
|
+
});
|
|
774
|
+
await new Promise((resolve, reject) => {
|
|
775
|
+
server.once("error", reject);
|
|
776
|
+
server.listen(paths.daemonPort ?? 0, paths.daemonHost, () => resolve());
|
|
777
|
+
});
|
|
778
|
+
const address = server.address();
|
|
779
|
+
if (!address || typeof address === "string") {
|
|
780
|
+
throw new AppError("Failed to determine daemon listening address", "runtime");
|
|
781
|
+
}
|
|
782
|
+
state = createInitialDaemonState(paths, {
|
|
783
|
+
pid: process.pid,
|
|
784
|
+
port: address.port,
|
|
785
|
+
launchMode: options.launchMode,
|
|
786
|
+
});
|
|
787
|
+
writeDaemonPid(paths.daemonPidPath, process.pid);
|
|
788
|
+
persistState();
|
|
789
|
+
logInfo(`daemon listening on ${state.host}:${state.port} pid=${state.pid} mode=${state.launchMode}`);
|
|
790
|
+
const signalHandler = () => {
|
|
791
|
+
void beginShutdown();
|
|
792
|
+
};
|
|
793
|
+
process.on("SIGTERM", signalHandler);
|
|
794
|
+
process.on("SIGINT", signalHandler);
|
|
795
|
+
if (interval > 0) {
|
|
796
|
+
void runDefaultCycle("cycle").catch((error) => {
|
|
797
|
+
const appError = asAppError(error);
|
|
798
|
+
logError(`initial cycle failed: ${appError.message}`);
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
await new Promise((resolve) => {
|
|
802
|
+
resolveClosed = resolve;
|
|
803
|
+
});
|
|
804
|
+
process.off("SIGTERM", signalHandler);
|
|
805
|
+
process.off("SIGINT", signalHandler);
|
|
806
|
+
}
|
|
807
|
+
export { buildStatusPayload, renderLintResult };
|