@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.
Files changed (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +167 -0
  3. package/README.zh-CN.md +167 -0
  4. package/SKILL.md +116 -0
  5. package/agents/openai.yaml +4 -0
  6. package/assets/config.example.env +18 -0
  7. package/assets/templates/achievement.md +32 -0
  8. package/assets/templates/bridge.md +33 -0
  9. package/assets/templates/concept.md +47 -0
  10. package/assets/templates/faq.md +31 -0
  11. package/assets/templates/lesson.md +31 -0
  12. package/assets/templates/method.md +31 -0
  13. package/assets/templates/misconception.md +35 -0
  14. package/assets/templates/person.md +31 -0
  15. package/assets/templates/research-note.md +34 -0
  16. package/assets/templates/resume.md +34 -0
  17. package/assets/templates/source-summary.md +35 -0
  18. package/assets/vllm/qwen3_5_openai_developer.jinja +182 -0
  19. package/assets/wiki.config.default.json +193 -0
  20. package/dist/commands/check-config.js +77 -0
  21. package/dist/commands/create.js +32 -0
  22. package/dist/commands/daemon.js +186 -0
  23. package/dist/commands/dashboard.js +112 -0
  24. package/dist/commands/doctor.js +22 -0
  25. package/dist/commands/export-graph.js +28 -0
  26. package/dist/commands/export-index.js +31 -0
  27. package/dist/commands/find.js +36 -0
  28. package/dist/commands/fts.js +32 -0
  29. package/dist/commands/graph.js +35 -0
  30. package/dist/commands/init.js +48 -0
  31. package/dist/commands/lint.js +35 -0
  32. package/dist/commands/list.js +28 -0
  33. package/dist/commands/page-info.js +24 -0
  34. package/dist/commands/search.js +32 -0
  35. package/dist/commands/setup.js +15 -0
  36. package/dist/commands/stat.js +20 -0
  37. package/dist/commands/sync.js +38 -0
  38. package/dist/commands/template.js +71 -0
  39. package/dist/commands/type.js +88 -0
  40. package/dist/commands/vault.js +64 -0
  41. package/dist/core/agent.js +201 -0
  42. package/dist/core/cli-env.js +129 -0
  43. package/dist/core/codex-workflow.js +233 -0
  44. package/dist/core/config.js +126 -0
  45. package/dist/core/db.js +292 -0
  46. package/dist/core/embedding.js +104 -0
  47. package/dist/core/frontmatter.js +287 -0
  48. package/dist/core/indexer.js +241 -0
  49. package/dist/core/onboarding.js +967 -0
  50. package/dist/core/page-files.js +91 -0
  51. package/dist/core/paths.js +161 -0
  52. package/dist/core/presenters.js +23 -0
  53. package/dist/core/query.js +58 -0
  54. package/dist/core/runtime.js +20 -0
  55. package/dist/core/sync.js +235 -0
  56. package/dist/core/synology.js +412 -0
  57. package/dist/core/template-evolution.js +38 -0
  58. package/dist/core/vault-processing.js +742 -0
  59. package/dist/core/vault.js +594 -0
  60. package/dist/core/workflow-context.js +188 -0
  61. package/dist/core/workflow-result.js +162 -0
  62. package/dist/core/workspace-bootstrap.js +30 -0
  63. package/dist/core/workspace-skills.js +220 -0
  64. package/dist/daemon/client.js +147 -0
  65. package/dist/daemon/server.js +807 -0
  66. package/dist/daemon/state.js +53 -0
  67. package/dist/dashboard/assets/index-1FgAUZ28.css +1 -0
  68. package/dist/dashboard/assets/index-6A0PWT4X.js +154 -0
  69. package/dist/dashboard/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  70. package/dist/dashboard/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  71. package/dist/dashboard/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  72. package/dist/dashboard/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  73. package/dist/dashboard/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  74. package/dist/dashboard/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  75. package/dist/dashboard/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  76. package/dist/dashboard/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  77. package/dist/dashboard/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  78. package/dist/dashboard/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  79. package/dist/dashboard/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  80. package/dist/dashboard/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  81. package/dist/dashboard/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  82. package/dist/dashboard/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  83. package/dist/dashboard/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  84. package/dist/dashboard/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  85. package/dist/dashboard/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  86. package/dist/dashboard/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  87. package/dist/dashboard/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  88. package/dist/dashboard/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  89. package/dist/dashboard/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  90. package/dist/dashboard/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  91. package/dist/dashboard/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  92. package/dist/dashboard/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  93. package/dist/dashboard/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  94. package/dist/dashboard/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  95. package/dist/dashboard/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  96. package/dist/dashboard/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
  97. package/dist/dashboard/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
  98. package/dist/dashboard/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  99. package/dist/dashboard/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  100. package/dist/dashboard/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  101. package/dist/dashboard/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  102. package/dist/dashboard/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
  103. package/dist/dashboard/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
  104. package/dist/dashboard/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
  105. package/dist/dashboard/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
  106. package/dist/dashboard/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  107. package/dist/dashboard/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  108. package/dist/dashboard/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
  109. package/dist/dashboard/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
  110. package/dist/dashboard/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
  111. package/dist/dashboard/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
  112. package/dist/dashboard/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  113. package/dist/dashboard/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  114. package/dist/dashboard/index.html +18 -0
  115. package/dist/index.js +86 -0
  116. package/dist/operations/dashboard.js +1231 -0
  117. package/dist/operations/export.js +110 -0
  118. package/dist/operations/query.js +649 -0
  119. package/dist/operations/type-template.js +210 -0
  120. package/dist/operations/write.js +143 -0
  121. package/dist/types/config.js +1 -0
  122. package/dist/types/page.js +1 -0
  123. package/dist/utils/case.js +22 -0
  124. package/dist/utils/errors.js +26 -0
  125. package/dist/utils/fs.js +77 -0
  126. package/dist/utils/output.js +33 -0
  127. package/dist/utils/process.js +60 -0
  128. package/dist/utils/segmenter.js +24 -0
  129. package/dist/utils/slug.js +10 -0
  130. package/dist/utils/time.js +24 -0
  131. package/package.json +64 -0
  132. package/references/cli-interface.md +312 -0
  133. package/references/env.md +122 -0
  134. package/references/template-design-guide.md +271 -0
  135. package/references/vault-to-wiki-instruction.md +110 -0
  136. package/references/wiki-maintenance-instruction.md +190 -0
@@ -0,0 +1,1231 @@
1
+ import path from "node:path";
2
+ import { getMeta } from "../core/db.js";
3
+ import { parsePage } from "../core/frontmatter.js";
4
+ import { buildDoctorReport } from "../core/onboarding.js";
5
+ import { resolveRuntimePaths } from "../core/paths.js";
6
+ import { compactPageSummary } from "../core/presenters.js";
7
+ import { listPageColumns, mapPageRow, selectPageById } from "../core/query.js";
8
+ import { openRuntimeDb } from "../core/runtime.js";
9
+ import { getSynologyCacheStatus, ensureLocalVaultFile, extractVaultText } from "../core/vault.js";
10
+ import { getVaultQueueItem, getVaultQueueSnapshot } from "../core/vault-processing.js";
11
+ import { getWorkflowArtifactSet } from "../core/workflow-context.js";
12
+ import { AppError } from "../utils/errors.js";
13
+ import { openTarget } from "../utils/process.js";
14
+ import { pathExistsSync, readTextFileSync } from "../utils/fs.js";
15
+ import { toOffsetIso } from "../utils/time.js";
16
+ import { ftsSearchPages, getWikiStat, runLint, searchPages } from "./query.js";
17
+ function parsePositiveLimit(value, fallback) {
18
+ const limit = Number.parseInt(String(value ?? fallback), 10);
19
+ if (!Number.isFinite(limit) || limit <= 0) {
20
+ throw new AppError(`Invalid limit value: ${value}`, "config");
21
+ }
22
+ return limit;
23
+ }
24
+ function normalizeOptionalString(value) {
25
+ if (typeof value !== "string") {
26
+ return null;
27
+ }
28
+ const normalized = value.trim();
29
+ return normalized ? normalized : null;
30
+ }
31
+ function normalizeStringArray(value) {
32
+ if (Array.isArray(value)) {
33
+ return value.map((entry) => String(entry ?? "").trim()).filter(Boolean);
34
+ }
35
+ if (typeof value === "string" && value.trim()) {
36
+ return [value.trim()];
37
+ }
38
+ return [];
39
+ }
40
+ function previewText(value, maxLength = 4_000) {
41
+ if (!value) {
42
+ return "";
43
+ }
44
+ const normalized = value.replace(/\r\n/g, "\n").trim();
45
+ if (normalized.length <= maxLength) {
46
+ return normalized;
47
+ }
48
+ return `${normalized.slice(0, maxLength)}…`;
49
+ }
50
+ function safeParseJson(rawText) {
51
+ if (!rawText || !rawText.trim()) {
52
+ return { parsed: null, error: null };
53
+ }
54
+ try {
55
+ return {
56
+ parsed: JSON.parse(rawText),
57
+ error: null,
58
+ };
59
+ }
60
+ catch (error) {
61
+ return {
62
+ parsed: null,
63
+ error: error instanceof Error ? error.message : String(error),
64
+ };
65
+ }
66
+ }
67
+ function readOptionalText(filePath) {
68
+ if (!pathExistsSync(filePath)) {
69
+ return null;
70
+ }
71
+ return readTextFileSync(filePath);
72
+ }
73
+ function normalizeDashboardPageId(input, wikiPath) {
74
+ if (input.endsWith(".md") || path.isAbsolute(input)) {
75
+ const relative = path.relative(wikiPath, path.resolve(wikiPath, input));
76
+ if (relative.startsWith("..")) {
77
+ throw new AppError(`Path is outside pages directory: ${input}`, "config");
78
+ }
79
+ return relative.split(path.sep).join("/");
80
+ }
81
+ return input;
82
+ }
83
+ function pageNodeKey(page) {
84
+ const nodeId = normalizeOptionalString(page.nodeId);
85
+ return nodeId ?? String(page.id);
86
+ }
87
+ function scoreRecency(updatedAt) {
88
+ if (!updatedAt) {
89
+ return 0;
90
+ }
91
+ const updatedAtMs = new Date(updatedAt).getTime();
92
+ if (Number.isNaN(updatedAtMs)) {
93
+ return 0;
94
+ }
95
+ const ageInDays = (Date.now() - updatedAtMs) / 86_400_000;
96
+ return Math.max(0, 365 - ageInDays);
97
+ }
98
+ function buildPageSummary(page, config) {
99
+ return compactPageSummary(page, config);
100
+ }
101
+ function getAllPageRows(db, config) {
102
+ const rows = db
103
+ .prepare(`SELECT ${listPageColumns(config).join(", ")} FROM pages ORDER BY updated_at DESC, title ASC`)
104
+ .all();
105
+ return rows.map((row) => mapPageRow(row, config));
106
+ }
107
+ function getAllEdges(db) {
108
+ return db.prepare(`
109
+ SELECT source, target, edge_type AS edgeType, source_page AS sourcePage
110
+ FROM edges
111
+ ORDER BY edge_type, source, target
112
+ `).all();
113
+ }
114
+ function createPageIndexes(pages) {
115
+ const aliasToNodeKey = new Map();
116
+ const nodeKeyToPage = new Map();
117
+ const pageIdToPage = new Map();
118
+ for (const page of pages) {
119
+ const key = pageNodeKey(page);
120
+ aliasToNodeKey.set(String(page.id), key);
121
+ const nodeId = normalizeOptionalString(page.nodeId);
122
+ if (nodeId) {
123
+ aliasToNodeKey.set(nodeId, key);
124
+ }
125
+ nodeKeyToPage.set(key, page);
126
+ pageIdToPage.set(String(page.id), page);
127
+ }
128
+ return {
129
+ aliasToNodeKey,
130
+ nodeKeyToPage,
131
+ pageIdToPage,
132
+ };
133
+ }
134
+ function normalizeEdges(edges, aliasToNodeKey) {
135
+ const normalized = [];
136
+ const seen = new Set();
137
+ for (const edge of edges) {
138
+ const source = aliasToNodeKey.get(edge.source);
139
+ const target = aliasToNodeKey.get(edge.target);
140
+ if (!source || !target || source === target) {
141
+ continue;
142
+ }
143
+ const key = `${source}::${target}::${edge.edgeType}::${edge.sourcePage ?? ""}`;
144
+ if (seen.has(key)) {
145
+ continue;
146
+ }
147
+ seen.add(key);
148
+ normalized.push({
149
+ source,
150
+ target,
151
+ edgeType: edge.edgeType,
152
+ sourcePage: edge.sourcePage,
153
+ });
154
+ }
155
+ return normalized;
156
+ }
157
+ function sampleOverviewNodeKeys(pages, edges, limit) {
158
+ if (pages.length <= limit) {
159
+ return new Set(pages.map((page) => pageNodeKey(page)));
160
+ }
161
+ const degreeMap = new Map();
162
+ const adjacency = new Map();
163
+ for (const page of pages) {
164
+ const key = pageNodeKey(page);
165
+ degreeMap.set(key, 0);
166
+ adjacency.set(key, new Set());
167
+ }
168
+ for (const edge of edges) {
169
+ degreeMap.set(edge.source, (degreeMap.get(edge.source) ?? 0) + 1);
170
+ degreeMap.set(edge.target, (degreeMap.get(edge.target) ?? 0) + 1);
171
+ adjacency.get(edge.source)?.add(edge.target);
172
+ adjacency.get(edge.target)?.add(edge.source);
173
+ }
174
+ const scored = pages
175
+ .map((page) => ({
176
+ page,
177
+ key: pageNodeKey(page),
178
+ degree: degreeMap.get(pageNodeKey(page)) ?? 0,
179
+ score: (degreeMap.get(pageNodeKey(page)) ?? 0) * 1_000 +
180
+ scoreRecency(normalizeOptionalString(page.updatedAt)) +
181
+ (page.status === "active" ? 100 : 0),
182
+ }))
183
+ .sort((left, right) => right.score - left.score || String(left.page.id).localeCompare(String(right.page.id)));
184
+ const byType = new Map();
185
+ const orphans = [];
186
+ const connected = [];
187
+ for (const item of scored) {
188
+ const pageType = String(item.page.pageType);
189
+ if (!byType.has(pageType)) {
190
+ byType.set(pageType, []);
191
+ }
192
+ byType.get(pageType).push(item);
193
+ if (item.degree === 0) {
194
+ orphans.push(item);
195
+ }
196
+ else {
197
+ connected.push(item);
198
+ }
199
+ }
200
+ const selected = new Set();
201
+ for (const bucket of byType.values()) {
202
+ if (selected.size >= limit) {
203
+ break;
204
+ }
205
+ const first = bucket[0];
206
+ if (first) {
207
+ selected.add(first.key);
208
+ }
209
+ }
210
+ const connectedTarget = Math.max(selected.size, Math.min(limit, Math.floor(limit * 0.82)));
211
+ let index = 0;
212
+ while (selected.size < connectedTarget && index < connected.length) {
213
+ const candidate = connected[index++];
214
+ if (selected.has(candidate.key)) {
215
+ continue;
216
+ }
217
+ selected.add(candidate.key);
218
+ if (selected.size >= connectedTarget) {
219
+ break;
220
+ }
221
+ const neighbors = [...(adjacency.get(candidate.key) ?? [])]
222
+ .map((key) => scored.find((item) => item.key === key))
223
+ .filter((item) => Boolean(item))
224
+ .sort((left, right) => right.score - left.score);
225
+ for (const neighbor of neighbors) {
226
+ if (selected.size >= connectedTarget) {
227
+ break;
228
+ }
229
+ selected.add(neighbor.key);
230
+ }
231
+ }
232
+ const orphanTarget = Math.min(Math.max(4, Math.floor(limit * 0.08)), orphans.length, limit - selected.size);
233
+ for (const orphan of orphans.slice(0, orphanTarget)) {
234
+ selected.add(orphan.key);
235
+ }
236
+ for (const item of scored) {
237
+ if (selected.size >= limit) {
238
+ break;
239
+ }
240
+ selected.add(item.key);
241
+ }
242
+ return selected;
243
+ }
244
+ function buildQueueTiming(item) {
245
+ const queuedAt = new Date(item.queuedAt).getTime();
246
+ const claimedAt = item.claimedAt ? new Date(item.claimedAt).getTime() : NaN;
247
+ const startedAt = item.startedAt ? new Date(item.startedAt).getTime() : NaN;
248
+ const processedAt = item.processedAt ? new Date(item.processedAt).getTime() : NaN;
249
+ const now = Date.now();
250
+ return {
251
+ queuedAt: item.queuedAt,
252
+ claimedAt: item.claimedAt ?? null,
253
+ startedAt: item.startedAt ?? null,
254
+ processedAt: item.processedAt,
255
+ lastErrorAt: item.lastErrorAt ?? null,
256
+ retryAfter: item.retryAfter ?? null,
257
+ queueAgeMs: Number.isFinite(queuedAt) ? now - queuedAt : null,
258
+ waitDurationMs: Number.isFinite(claimedAt) && Number.isFinite(queuedAt) ? claimedAt - queuedAt : null,
259
+ processingDurationMs: item.status === "processing" && Number.isFinite(startedAt)
260
+ ? now - startedAt
261
+ : Number.isFinite(startedAt) && Number.isFinite(processedAt)
262
+ ? processedAt - startedAt
263
+ : null,
264
+ totalDurationMs: Number.isFinite(queuedAt) && Number.isFinite(processedAt) ? processedAt - queuedAt : null,
265
+ };
266
+ }
267
+ function buildQueueListItem(item) {
268
+ return {
269
+ fileId: item.fileId,
270
+ status: item.status,
271
+ priority: item.priority,
272
+ attempts: item.attempts,
273
+ resultPageId: item.resultPageId,
274
+ errorMessage: item.errorMessage,
275
+ threadId: item.threadId ?? null,
276
+ decision: item.decision ?? null,
277
+ workflowVersion: item.workflowVersion ?? null,
278
+ resultManifestPath: item.resultManifestPath ?? null,
279
+ fileName: item.fileName ?? item.fileId.split("/").at(-1) ?? item.fileId,
280
+ fileExt: item.fileExt ?? null,
281
+ sourceType: item.sourceType ?? null,
282
+ fileSize: item.fileSize ?? null,
283
+ filePath: item.filePath ?? null,
284
+ createdPageIds: item.createdPageIds ?? [],
285
+ updatedPageIds: item.updatedPageIds ?? [],
286
+ appliedTypeNames: item.appliedTypeNames ?? [],
287
+ proposedTypeNames: item.proposedTypeNames ?? [],
288
+ skillsUsed: item.skillsUsed ?? [],
289
+ timing: buildQueueTiming(item),
290
+ };
291
+ }
292
+ function normalizeQueueSearch(item) {
293
+ return [
294
+ item.fileId,
295
+ item.fileName,
296
+ item.filePath,
297
+ item.resultPageId,
298
+ item.errorMessage,
299
+ item.threadId,
300
+ ...(item.createdPageIds ?? []),
301
+ ...(item.updatedPageIds ?? []),
302
+ ]
303
+ .filter((value) => typeof value === "string" && value.trim().length > 0)
304
+ .join(" ")
305
+ .toLowerCase();
306
+ }
307
+ function readArtifactBundle(fileId, env = process.env) {
308
+ const paths = resolveRuntimePaths(env);
309
+ const artifacts = getWorkflowArtifactSet(paths, fileId);
310
+ const queueItemText = readOptionalText(artifacts.queueItemPath);
311
+ const promptText = readOptionalText(artifacts.promptPath);
312
+ const resultText = readOptionalText(artifacts.resultPath);
313
+ const queueItemJson = safeParseJson(queueItemText);
314
+ const resultJson = safeParseJson(resultText);
315
+ return {
316
+ artifactId: artifacts.artifactId,
317
+ rootDir: artifacts.rootDir,
318
+ queueItemPath: artifacts.queueItemPath,
319
+ promptPath: artifacts.promptPath,
320
+ resultPath: artifacts.resultPath,
321
+ skillArtifactsPath: artifacts.skillArtifactsPath,
322
+ queueItem: {
323
+ exists: queueItemText !== null,
324
+ rawText: queueItemText,
325
+ parsed: queueItemJson.parsed,
326
+ parseError: queueItemJson.error,
327
+ },
328
+ prompt: {
329
+ exists: promptText !== null,
330
+ rawText: promptText,
331
+ preview: previewText(promptText, 6_000),
332
+ },
333
+ result: {
334
+ exists: resultText !== null,
335
+ rawText: resultText,
336
+ parsed: resultJson.parsed,
337
+ parseError: resultJson.error,
338
+ },
339
+ };
340
+ }
341
+ function fetchLinkedPageSummaries(db, config, identifiers) {
342
+ const cleaned = [...new Set(identifiers.filter(Boolean))];
343
+ if (cleaned.length === 0) {
344
+ return [];
345
+ }
346
+ const rows = db
347
+ .prepare(`
348
+ SELECT ${listPageColumns(config).join(", ")}
349
+ FROM pages
350
+ WHERE id IN (${cleaned.map(() => "?").join(", ")})
351
+ OR node_id IN (${cleaned.map(() => "?").join(", ")})
352
+ ORDER BY updated_at DESC, title ASC
353
+ `)
354
+ .all(...cleaned, ...cleaned);
355
+ return rows.map((row) => buildPageSummary(mapPageRow(row, config), config));
356
+ }
357
+ function normalizeQueueStatusFilter(status) {
358
+ if (!status) {
359
+ return undefined;
360
+ }
361
+ if (status === "pending" || status === "processing" || status === "done" || status === "skipped" || status === "error") {
362
+ return status;
363
+ }
364
+ throw new AppError(`Unsupported queue status: ${status}`, "config");
365
+ }
366
+ function normalizeLintLevel(level) {
367
+ if (!level || level === "all") {
368
+ return null;
369
+ }
370
+ if (level === "error" || level === "warning" || level === "info") {
371
+ return level;
372
+ }
373
+ throw new AppError(`Unsupported lint level: ${level}`, "config");
374
+ }
375
+ function normalizeGroupBy(value) {
376
+ if (!value || value === "flat") {
377
+ return "flat";
378
+ }
379
+ if (value === "page" || value === "rule") {
380
+ return value;
381
+ }
382
+ throw new AppError(`Unsupported groupBy value: ${value}`, "config");
383
+ }
384
+ async function buildVaultPreview(env, file) {
385
+ const { paths } = openRuntimeDb(env);
386
+ const cache = getSynologyCacheStatus(paths.vaultPath, file, env);
387
+ let localPath = cache.localPath;
388
+ let preview = "";
389
+ let previewError = null;
390
+ let previewAvailable = false;
391
+ try {
392
+ localPath = await ensureLocalVaultFile(file, paths.vaultPath, env);
393
+ preview = previewText(extractVaultText(localPath), 10_000);
394
+ previewAvailable = preview.length > 0;
395
+ }
396
+ catch (error) {
397
+ previewError = error instanceof Error ? error.message : String(error);
398
+ }
399
+ return {
400
+ cacheStatus: cache.kind,
401
+ localPath,
402
+ metadataPath: cache.metadataPath,
403
+ previewAvailable,
404
+ preview,
405
+ previewError,
406
+ };
407
+ }
408
+ async function resolvePageVaultSource(db, config, env, page, rawData) {
409
+ const vaultPath = normalizeOptionalString(rawData.vaultPath) ?? normalizeOptionalString(page.vaultPath);
410
+ if (!vaultPath) {
411
+ return null;
412
+ }
413
+ const row = db.prepare(`
414
+ SELECT
415
+ id,
416
+ file_name AS fileName,
417
+ file_ext AS fileExt,
418
+ source_type AS sourceType,
419
+ file_size AS fileSize,
420
+ file_path AS filePath,
421
+ content_hash AS contentHash,
422
+ file_mtime AS fileMtime,
423
+ indexed_at AS indexedAt
424
+ FROM vault_files
425
+ WHERE id = ?
426
+ `).get(vaultPath);
427
+ if (!row) {
428
+ return {
429
+ fileId: vaultPath,
430
+ missing: true,
431
+ previewAvailable: false,
432
+ preview: "",
433
+ previewError: "Vault file not found in index.",
434
+ };
435
+ }
436
+ const preview = await buildVaultPreview(env, row);
437
+ return {
438
+ fileId: row.id,
439
+ fileName: row.fileName,
440
+ fileExt: row.fileExt,
441
+ sourceType: row.sourceType,
442
+ fileSize: row.fileSize,
443
+ remotePath: row.filePath,
444
+ indexedAt: row.indexedAt,
445
+ ...preview,
446
+ };
447
+ }
448
+ function buildRelationLookup(db, config, pageRows, page) {
449
+ const { aliasToNodeKey, nodeKeyToPage, pageIdToPage } = createPageIndexes(pageRows);
450
+ const identifiers = [String(page.id)];
451
+ const nodeId = normalizeOptionalString(page.nodeId);
452
+ if (nodeId) {
453
+ identifiers.push(nodeId);
454
+ }
455
+ const outgoingRows = db.prepare(`
456
+ SELECT source, target, edge_type AS edgeType, source_page AS sourcePage
457
+ FROM edges
458
+ WHERE source_page = ?
459
+ ORDER BY edge_type, target
460
+ `).all(String(page.id));
461
+ const incomingRows = db.prepare(`
462
+ SELECT source, target, edge_type AS edgeType, source_page AS sourcePage
463
+ FROM edges
464
+ WHERE target IN (${identifiers.map(() => "?").join(", ")})
465
+ ORDER BY edge_type, source
466
+ `).all(...identifiers);
467
+ const lookupPage = (rawReference) => {
468
+ const key = aliasToNodeKey.get(rawReference) ?? rawReference;
469
+ const match = nodeKeyToPage.get(key) ?? pageIdToPage.get(rawReference) ?? null;
470
+ if (!match) {
471
+ return null;
472
+ }
473
+ return buildPageSummary(match, config);
474
+ };
475
+ const relations = [];
476
+ for (const edge of outgoingRows) {
477
+ relations.push({
478
+ direction: "outgoing",
479
+ edgeType: edge.edgeType,
480
+ source: buildPageSummary(page, config),
481
+ target: lookupPage(edge.target),
482
+ rawTarget: edge.target,
483
+ });
484
+ }
485
+ for (const edge of incomingRows) {
486
+ relations.push({
487
+ direction: "incoming",
488
+ edgeType: edge.edgeType,
489
+ source: lookupPage(edge.source),
490
+ target: buildPageSummary(page, config),
491
+ rawSource: edge.source,
492
+ });
493
+ }
494
+ return relations;
495
+ }
496
+ function buildLintIssueGroups(issues, groupBy) {
497
+ if (groupBy === "flat") {
498
+ return {
499
+ groupBy,
500
+ items: issues,
501
+ };
502
+ }
503
+ const groupMap = new Map();
504
+ for (const issue of issues) {
505
+ const key = groupBy === "page" ? issue.pageId : issue.check;
506
+ if (!groupMap.has(key)) {
507
+ groupMap.set(key, []);
508
+ }
509
+ groupMap.get(key).push(issue);
510
+ }
511
+ return {
512
+ groupBy,
513
+ groups: [...groupMap.entries()]
514
+ .map(([key, values]) => ({
515
+ key,
516
+ count: values.length,
517
+ levelCounts: values.reduce((accumulator, issue) => {
518
+ accumulator[issue.level] = (accumulator[issue.level] ?? 0) + 1;
519
+ return accumulator;
520
+ }, {}),
521
+ pageTitle: groupBy === "page" ? values[0]?.pageTitle ?? null : null,
522
+ pageType: groupBy === "page" ? values[0]?.pageType ?? null : null,
523
+ items: values,
524
+ }))
525
+ .sort((left, right) => right.count - left.count || left.key.localeCompare(right.key)),
526
+ };
527
+ }
528
+ function buildEnrichedLintIssues(env = process.env) {
529
+ const { db, config } = openRuntimeDb(env);
530
+ try {
531
+ const pageRows = getAllPageRows(db, config);
532
+ const pageSummaryById = new Map(pageRows.map((page) => [
533
+ String(page.id),
534
+ {
535
+ title: String(page.title),
536
+ pageType: String(page.pageType),
537
+ nodeId: normalizeOptionalString(page.nodeId),
538
+ filePath: normalizeOptionalString(page.filePath),
539
+ },
540
+ ]));
541
+ const lint = runLint(env, { level: "info" });
542
+ const enrich = (level, items) => items.map((item) => ({
543
+ level,
544
+ pageId: item.page,
545
+ check: item.check,
546
+ message: item.message,
547
+ pageTitle: pageSummaryById.get(item.page)?.title ?? null,
548
+ pageType: pageSummaryById.get(item.page)?.pageType ?? null,
549
+ nodeId: pageSummaryById.get(item.page)?.nodeId ?? null,
550
+ filePath: pageSummaryById.get(item.page)?.filePath ?? null,
551
+ }));
552
+ return [...enrich("error", lint.errors), ...enrich("warning", lint.warnings), ...enrich("info", lint.info)];
553
+ }
554
+ finally {
555
+ db.close();
556
+ }
557
+ }
558
+ async function fallbackTitleSearch(env, query, limit) {
559
+ const { db, config } = openRuntimeDb(env);
560
+ try {
561
+ const rows = db
562
+ .prepare(`
563
+ SELECT ${listPageColumns(config).join(", ")}
564
+ FROM pages
565
+ WHERE title LIKE @query OR summary_text LIKE @query OR file_path LIKE @query
566
+ ORDER BY updated_at DESC, title ASC
567
+ LIMIT @limit
568
+ `)
569
+ .all({
570
+ query: `%${query}%`,
571
+ limit,
572
+ });
573
+ return rows.map((row) => {
574
+ const mapped = mapPageRow(row, config);
575
+ return {
576
+ ...buildPageSummary(mapped, config),
577
+ summaryText: typeof mapped.summaryText === "string" ? mapped.summaryText : "",
578
+ };
579
+ });
580
+ }
581
+ finally {
582
+ db.close();
583
+ }
584
+ }
585
+ export async function getDashboardGraphOverview(env = process.env, options = {}) {
586
+ const { db, config } = openRuntimeDb(env);
587
+ try {
588
+ const limit = parsePositiveLimit(options.limit, 120);
589
+ const pageRows = getAllPageRows(db, config);
590
+ const { aliasToNodeKey, nodeKeyToPage } = createPageIndexes(pageRows);
591
+ const edges = normalizeEdges(getAllEdges(db), aliasToNodeKey);
592
+ const selectedKeys = sampleOverviewNodeKeys(pageRows, edges, limit);
593
+ const visibleNodes = [...selectedKeys].reduce((nodes, key) => {
594
+ const page = nodeKeyToPage.get(key);
595
+ if (!page) {
596
+ return nodes;
597
+ }
598
+ const degree = edges.filter((edge) => edge.source === key || edge.target === key).length;
599
+ nodes.push({
600
+ ...buildPageSummary(page, config),
601
+ nodeKey: key,
602
+ degree,
603
+ orphan: degree === 0,
604
+ embeddingStatus: page.embeddingStatus ?? null,
605
+ sourceType: page.sourceType ?? null,
606
+ });
607
+ return nodes;
608
+ }, [])
609
+ .sort((left, right) => String(left.title).localeCompare(String(right.title)));
610
+ const visibleEdges = edges.filter((edge) => selectedKeys.has(edge.source) && selectedKeys.has(edge.target));
611
+ return {
612
+ nodes: visibleNodes,
613
+ edges: visibleEdges,
614
+ totalNodes: pageRows.length,
615
+ visibleNodeCount: visibleNodes.length,
616
+ totalEdges: edges.length,
617
+ visibleEdgeCount: visibleEdges.length,
618
+ truncated: visibleNodes.length < pageRows.length,
619
+ sampleStrategy: {
620
+ limit,
621
+ priorities: ["degree", "recency", "pageType coverage", "orphan sampling"],
622
+ },
623
+ generatedAt: toOffsetIso(),
624
+ };
625
+ }
626
+ finally {
627
+ db.close();
628
+ }
629
+ }
630
+ export async function searchDashboardGraph(env = process.env, options) {
631
+ const query = options.query.trim();
632
+ const limit = parsePositiveLimit(options.limit, 20);
633
+ if (!query) {
634
+ return {
635
+ query,
636
+ mode: "empty",
637
+ results: [],
638
+ generatedAt: toOffsetIso(),
639
+ };
640
+ }
641
+ const merged = new Map();
642
+ let mode = "fts";
643
+ try {
644
+ for (const result of ftsSearchPages(env, {
645
+ query,
646
+ limit,
647
+ })) {
648
+ merged.set(String(result.id), {
649
+ ...result,
650
+ searchKind: "fts",
651
+ });
652
+ }
653
+ }
654
+ catch {
655
+ mode = "fallback";
656
+ }
657
+ if (merged.size === 0) {
658
+ for (const result of await fallbackTitleSearch(env, query, limit)) {
659
+ merged.set(String(result.id), {
660
+ ...result,
661
+ searchKind: "fallback",
662
+ });
663
+ }
664
+ }
665
+ if (merged.size < limit) {
666
+ try {
667
+ const semanticResults = await searchPages(env, {
668
+ query,
669
+ limit,
670
+ });
671
+ for (const result of semanticResults) {
672
+ if (merged.size >= limit) {
673
+ break;
674
+ }
675
+ const key = String(result.id);
676
+ if (!merged.has(key)) {
677
+ merged.set(key, {
678
+ ...result,
679
+ searchKind: merged.size > 0 ? "semantic" : "semantic-only",
680
+ });
681
+ }
682
+ }
683
+ if (merged.size > 0) {
684
+ mode = mode === "fallback" ? "fallback" : "hybrid";
685
+ }
686
+ }
687
+ catch {
688
+ // Semantic search is optional.
689
+ }
690
+ }
691
+ return {
692
+ query,
693
+ mode,
694
+ resultCount: merged.size,
695
+ results: [...merged.values()],
696
+ generatedAt: toOffsetIso(),
697
+ };
698
+ }
699
+ export function getDashboardQueueSummary(env = process.env) {
700
+ const snapshot = getVaultQueueSnapshot(env);
701
+ const items = snapshot.items.map(buildQueueListItem);
702
+ const processing = items
703
+ .filter((item) => item.status === "processing")
704
+ .sort((left, right) => Number(right.timing.processingDurationMs ?? 0) - Number(left.timing.processingDurationMs ?? 0))
705
+ .slice(0, 8);
706
+ const errors = items
707
+ .filter((item) => item.status === "error")
708
+ .sort((left, right) => String(right.timing.lastErrorAt ?? "").localeCompare(String(left.timing.lastErrorAt ?? "")))
709
+ .slice(0, 8);
710
+ const recentDone = items
711
+ .filter((item) => item.status === "done" || item.status === "skipped")
712
+ .sort((left, right) => String(right.timing.processedAt ?? "").localeCompare(String(left.timing.processedAt ?? "")))
713
+ .slice(0, 12);
714
+ return {
715
+ counts: {
716
+ pending: snapshot.totalPending,
717
+ processing: snapshot.totalProcessing,
718
+ done: snapshot.totalDone,
719
+ skipped: snapshot.totalSkipped,
720
+ error: snapshot.totalError,
721
+ total: snapshot.items.length,
722
+ },
723
+ processing,
724
+ errors,
725
+ recentDone,
726
+ generatedAt: toOffsetIso(),
727
+ };
728
+ }
729
+ export function listDashboardQueueItems(env = process.env, options = {}) {
730
+ const status = normalizeQueueStatusFilter(options.status);
731
+ const query = options.query?.trim().toLowerCase() ?? "";
732
+ const sourceType = options.sourceType?.trim().toLowerCase() ?? "";
733
+ const limit = parsePositiveLimit(options.limit, 200);
734
+ const snapshot = getVaultQueueSnapshot(env, status);
735
+ const filtered = snapshot.items
736
+ .filter((item) => (sourceType ? (item.sourceType ?? "").toLowerCase() === sourceType : true))
737
+ .filter((item) => (query ? normalizeQueueSearch(item).includes(query) : true))
738
+ .slice(0, limit)
739
+ .map(buildQueueListItem);
740
+ return {
741
+ total: filtered.length,
742
+ items: filtered,
743
+ generatedAt: toOffsetIso(),
744
+ };
745
+ }
746
+ export function getDashboardQueueItemDetail(env = process.env, fileId) {
747
+ const { db, config } = openRuntimeDb(env);
748
+ try {
749
+ const item = getVaultQueueItem(env, fileId);
750
+ if (!item) {
751
+ throw new AppError(`Queue item not found: ${fileId}`, "not_found");
752
+ }
753
+ const artifactBundle = readArtifactBundle(fileId, env);
754
+ const linkedPageIds = [
755
+ item.resultPageId,
756
+ ...(item.createdPageIds ?? []),
757
+ ...(item.updatedPageIds ?? []),
758
+ ].filter((value) => Boolean(value));
759
+ return {
760
+ item: buildQueueListItem(item),
761
+ artifacts: artifactBundle,
762
+ linkedPages: fetchLinkedPageSummaries(db, config, linkedPageIds),
763
+ generatedAt: toOffsetIso(),
764
+ };
765
+ }
766
+ finally {
767
+ db.close();
768
+ }
769
+ }
770
+ export function retryDashboardQueueItem(env = process.env, fileId) {
771
+ const { db } = openRuntimeDb(env);
772
+ try {
773
+ const item = getVaultQueueItem(env, fileId);
774
+ if (!item) {
775
+ throw new AppError(`Queue item not found: ${fileId}`, "not_found");
776
+ }
777
+ if (item.status === "processing") {
778
+ throw new AppError(`Queue item ${fileId} is currently processing and cannot be retried.`, "runtime", {
779
+ code: "busy",
780
+ });
781
+ }
782
+ db.prepare(`
783
+ UPDATE vault_processing_queue
784
+ SET
785
+ status = 'pending',
786
+ queued_at = @queued_at,
787
+ claimed_at = NULL,
788
+ started_at = NULL,
789
+ processed_at = NULL,
790
+ result_page_id = NULL,
791
+ error_message = NULL,
792
+ thread_id = NULL,
793
+ workflow_version = NULL,
794
+ decision = NULL,
795
+ result_manifest_path = NULL,
796
+ last_error_at = NULL,
797
+ retry_after = NULL,
798
+ created_page_ids = NULL,
799
+ updated_page_ids = NULL,
800
+ applied_type_names = NULL,
801
+ proposed_type_names = NULL,
802
+ skills_used = NULL
803
+ WHERE file_id = @file_id
804
+ `).run({
805
+ file_id: fileId,
806
+ queued_at: toOffsetIso(),
807
+ });
808
+ return {
809
+ status: "queued",
810
+ item: buildQueueListItem(getVaultQueueItem(env, fileId) ?? item),
811
+ };
812
+ }
813
+ finally {
814
+ db.close();
815
+ }
816
+ }
817
+ export function getDashboardPageDetail(env = process.env, inputPageId) {
818
+ const { db, config, paths } = openRuntimeDb(env);
819
+ try {
820
+ const pageId = normalizeDashboardPageId(inputPageId, paths.wikiPath);
821
+ const page = selectPageById(db, config, pageId);
822
+ if (!page) {
823
+ throw new AppError(`Page not found: ${pageId}`, "not_found");
824
+ }
825
+ const pageFilePath = path.join(paths.wikiPath, ...String(page.id).split("/"));
826
+ const parsed = parsePage(pageFilePath, paths.wikiPath, config);
827
+ const pageRows = getAllPageRows(db, config);
828
+ const relations = buildRelationLookup(db, config, pageRows, page);
829
+ const rawData = parsed.ok ? parsed.parsed.rawData : {};
830
+ return {
831
+ page: {
832
+ ...buildPageSummary(page, config),
833
+ nodeKey: pageNodeKey(page),
834
+ summaryText: page.summaryText ?? "",
835
+ embeddingStatus: page.embeddingStatus ?? null,
836
+ markdownPreview: parsed.ok ? previewText(parsed.parsed.body, 4_000) : "",
837
+ frontmatter: rawData,
838
+ unregisteredFields: parsed.ok ? parsed.parsed.unregisteredFields : [],
839
+ pagePath: pageFilePath,
840
+ },
841
+ relations,
842
+ relationCounts: {
843
+ outgoing: relations.filter((relation) => relation.direction === "outgoing").length,
844
+ incoming: relations.filter((relation) => relation.direction === "incoming").length,
845
+ },
846
+ generatedAt: toOffsetIso(),
847
+ };
848
+ }
849
+ finally {
850
+ db.close();
851
+ }
852
+ }
853
+ export async function getDashboardPageSource(env = process.env, inputPageId) {
854
+ const { db, config, paths } = openRuntimeDb(env);
855
+ try {
856
+ const pageId = normalizeDashboardPageId(inputPageId, paths.wikiPath);
857
+ const page = selectPageById(db, config, pageId);
858
+ if (!page) {
859
+ throw new AppError(`Page not found: ${pageId}`, "not_found");
860
+ }
861
+ const pageFilePath = path.join(paths.wikiPath, ...String(page.id).split("/"));
862
+ const parsed = parsePage(pageFilePath, paths.wikiPath, config);
863
+ const rawData = parsed.ok ? parsed.parsed.rawData : {};
864
+ return {
865
+ pageSource: {
866
+ pageId: String(page.id),
867
+ pagePath: pageFilePath,
868
+ rawMarkdown: readOptionalText(pageFilePath),
869
+ frontmatter: rawData,
870
+ },
871
+ vaultSource: await resolvePageVaultSource(db, config, env, page, rawData),
872
+ generatedAt: toOffsetIso(),
873
+ };
874
+ }
875
+ finally {
876
+ db.close();
877
+ }
878
+ }
879
+ export async function openDashboardPageSource(env = process.env, inputPageId, target = "vault") {
880
+ const { db, config, paths } = openRuntimeDb(env);
881
+ try {
882
+ const pageId = normalizeDashboardPageId(inputPageId, paths.wikiPath);
883
+ const page = selectPageById(db, config, pageId);
884
+ if (!page) {
885
+ throw new AppError(`Page not found: ${pageId}`, "not_found");
886
+ }
887
+ const pageFilePath = path.join(paths.wikiPath, ...String(page.id).split("/"));
888
+ if (target === "page") {
889
+ openTarget(pageFilePath);
890
+ return {
891
+ opened: true,
892
+ target: "page",
893
+ path: pageFilePath,
894
+ };
895
+ }
896
+ const parsed = parsePage(pageFilePath, paths.wikiPath, config);
897
+ const rawData = parsed.ok ? parsed.parsed.rawData : {};
898
+ const vaultPath = normalizeOptionalString(rawData.vaultPath) ?? normalizeOptionalString(page.vaultPath);
899
+ if (!vaultPath) {
900
+ openTarget(pageFilePath);
901
+ return {
902
+ opened: true,
903
+ target: "page",
904
+ path: pageFilePath,
905
+ };
906
+ }
907
+ const file = db.prepare(`
908
+ SELECT
909
+ id,
910
+ file_name AS fileName,
911
+ file_ext AS fileExt,
912
+ source_type AS sourceType,
913
+ file_size AS fileSize,
914
+ file_path AS filePath,
915
+ content_hash AS contentHash,
916
+ file_mtime AS fileMtime,
917
+ indexed_at AS indexedAt
918
+ FROM vault_files
919
+ WHERE id = ?
920
+ `).get(vaultPath);
921
+ if (!file) {
922
+ throw new AppError(`Vault file not found: ${vaultPath}`, "not_found");
923
+ }
924
+ const localPath = await ensureLocalVaultFile(file, paths.vaultPath, env);
925
+ openTarget(localPath);
926
+ return {
927
+ opened: true,
928
+ target: "vault",
929
+ path: localPath,
930
+ fileId: file.id,
931
+ };
932
+ }
933
+ finally {
934
+ db.close();
935
+ }
936
+ }
937
+ export function getDashboardVaultSummary(env = process.env) {
938
+ const { db, config, paths } = openRuntimeDb(env);
939
+ try {
940
+ const files = db.prepare(`
941
+ SELECT
942
+ id,
943
+ file_name AS fileName,
944
+ file_ext AS fileExt,
945
+ source_type AS sourceType,
946
+ file_size AS fileSize,
947
+ file_path AS filePath,
948
+ content_hash AS contentHash,
949
+ file_mtime AS fileMtime,
950
+ indexed_at AS indexedAt
951
+ FROM vault_files
952
+ ORDER BY id
953
+ `).all();
954
+ const queue = getVaultQueueSnapshot(env);
955
+ const queueByFileId = new Map(queue.items.map((item) => [item.fileId, item]));
956
+ const pages = getAllPageRows(db, config);
957
+ const pagesByVaultPath = new Map();
958
+ for (const page of pages) {
959
+ const vaultPath = normalizeOptionalString(page.vaultPath);
960
+ if (!vaultPath) {
961
+ continue;
962
+ }
963
+ if (!pagesByVaultPath.has(vaultPath)) {
964
+ pagesByVaultPath.set(vaultPath, []);
965
+ }
966
+ pagesByVaultPath.get(vaultPath).push(buildPageSummary(page, config));
967
+ }
968
+ const bySourceType = {};
969
+ const cacheStatusCounts = {};
970
+ let notQueued = 0;
971
+ let totalBytes = 0;
972
+ for (const file of files) {
973
+ const sourceKey = file.sourceType ?? file.fileExt ?? "unknown";
974
+ bySourceType[sourceKey] = bySourceType[sourceKey] ?? { count: 0, totalBytes: 0 };
975
+ bySourceType[sourceKey].count += 1;
976
+ bySourceType[sourceKey].totalBytes += file.fileSize;
977
+ totalBytes += file.fileSize;
978
+ const cacheStatus = getSynologyCacheStatus(paths.vaultPath, file, env).kind;
979
+ cacheStatusCounts[cacheStatus] = (cacheStatusCounts[cacheStatus] ?? 0) + 1;
980
+ if (!queueByFileId.has(file.id)) {
981
+ notQueued += 1;
982
+ }
983
+ }
984
+ return {
985
+ totalFiles: files.length,
986
+ totalBytes,
987
+ coverage: {
988
+ pending: queue.totalPending,
989
+ processing: queue.totalProcessing,
990
+ done: queue.totalDone,
991
+ skipped: queue.totalSkipped,
992
+ error: queue.totalError,
993
+ notQueued,
994
+ },
995
+ bySourceType,
996
+ cacheStatus: cacheStatusCounts,
997
+ mappedPages: [...pagesByVaultPath.values()].reduce((count, pagesForFile) => count + pagesForFile.length, 0),
998
+ generatedAt: toOffsetIso(),
999
+ };
1000
+ }
1001
+ finally {
1002
+ db.close();
1003
+ }
1004
+ }
1005
+ export function listDashboardVaultFiles(env = process.env, options = {}) {
1006
+ const { db, config, paths } = openRuntimeDb(env);
1007
+ try {
1008
+ const query = options.query?.trim().toLowerCase() ?? "";
1009
+ const sourceType = options.sourceType?.trim().toLowerCase() ?? "";
1010
+ const queueStatus = options.queueStatus?.trim().toLowerCase() ?? "";
1011
+ const limit = parsePositiveLimit(options.limit, 300);
1012
+ const files = db.prepare(`
1013
+ SELECT
1014
+ id,
1015
+ file_name AS fileName,
1016
+ file_ext AS fileExt,
1017
+ source_type AS sourceType,
1018
+ file_size AS fileSize,
1019
+ file_path AS filePath,
1020
+ content_hash AS contentHash,
1021
+ file_mtime AS fileMtime,
1022
+ indexed_at AS indexedAt
1023
+ FROM vault_files
1024
+ ORDER BY id
1025
+ `).all();
1026
+ const queue = getVaultQueueSnapshot(env);
1027
+ const queueByFileId = new Map(queue.items.map((item) => [item.fileId, item]));
1028
+ const pages = getAllPageRows(db, config);
1029
+ const pageCountByVaultPath = new Map();
1030
+ for (const page of pages) {
1031
+ const vaultPath = normalizeOptionalString(page.vaultPath);
1032
+ if (!vaultPath) {
1033
+ continue;
1034
+ }
1035
+ pageCountByVaultPath.set(vaultPath, (pageCountByVaultPath.get(vaultPath) ?? 0) + 1);
1036
+ }
1037
+ const items = files
1038
+ .map((file) => {
1039
+ const queueItem = queueByFileId.get(file.id) ?? null;
1040
+ const cache = getSynologyCacheStatus(paths.vaultPath, file, env);
1041
+ return {
1042
+ fileId: file.id,
1043
+ fileName: file.fileName,
1044
+ fileExt: file.fileExt,
1045
+ sourceType: file.sourceType,
1046
+ fileSize: file.fileSize,
1047
+ filePath: file.filePath,
1048
+ indexedAt: file.indexedAt,
1049
+ queueStatus: queueItem?.status ?? "not-queued",
1050
+ queueItem: queueItem ? buildQueueListItem(queueItem) : null,
1051
+ generatedPageCount: pageCountByVaultPath.get(file.id) ?? 0,
1052
+ cacheStatus: cache.kind,
1053
+ localPath: cache.localPath,
1054
+ };
1055
+ })
1056
+ .filter((item) => (sourceType ? (item.sourceType ?? "").toLowerCase() === sourceType : true))
1057
+ .filter((item) => (queueStatus ? item.queueStatus === queueStatus : true))
1058
+ .filter((item) => {
1059
+ if (!query) {
1060
+ return true;
1061
+ }
1062
+ return [item.fileId, item.fileName, item.filePath]
1063
+ .filter((value) => typeof value === "string")
1064
+ .join(" ")
1065
+ .toLowerCase()
1066
+ .includes(query);
1067
+ })
1068
+ .slice(0, limit);
1069
+ return {
1070
+ total: items.length,
1071
+ items,
1072
+ generatedAt: toOffsetIso(),
1073
+ };
1074
+ }
1075
+ finally {
1076
+ db.close();
1077
+ }
1078
+ }
1079
+ export async function getDashboardVaultFileDetail(env = process.env, fileId) {
1080
+ const { db, config } = openRuntimeDb(env);
1081
+ try {
1082
+ const file = db.prepare(`
1083
+ SELECT
1084
+ id,
1085
+ file_name AS fileName,
1086
+ file_ext AS fileExt,
1087
+ source_type AS sourceType,
1088
+ file_size AS fileSize,
1089
+ file_path AS filePath,
1090
+ content_hash AS contentHash,
1091
+ file_mtime AS fileMtime,
1092
+ indexed_at AS indexedAt
1093
+ FROM vault_files
1094
+ WHERE id = ?
1095
+ `).get(fileId);
1096
+ if (!file) {
1097
+ throw new AppError(`Vault file not found: ${fileId}`, "not_found");
1098
+ }
1099
+ const pageRows = getAllPageRows(db, config).filter((page) => normalizeOptionalString(page.vaultPath) === fileId);
1100
+ const relatedPages = pageRows.map((page) => buildPageSummary(page, config));
1101
+ const queueItem = getVaultQueueItem(env, fileId);
1102
+ return {
1103
+ file: {
1104
+ ...file,
1105
+ ...(await buildVaultPreview(env, file)),
1106
+ },
1107
+ queueItem: queueItem ? buildQueueListItem(queueItem) : null,
1108
+ relatedPages,
1109
+ generatedAt: toOffsetIso(),
1110
+ };
1111
+ }
1112
+ finally {
1113
+ db.close();
1114
+ }
1115
+ }
1116
+ export async function openDashboardVaultFile(env = process.env, fileId) {
1117
+ const { db, paths } = openRuntimeDb(env);
1118
+ try {
1119
+ const file = db.prepare(`
1120
+ SELECT
1121
+ id,
1122
+ file_name AS fileName,
1123
+ file_ext AS fileExt,
1124
+ source_type AS sourceType,
1125
+ file_size AS fileSize,
1126
+ file_path AS filePath,
1127
+ content_hash AS contentHash,
1128
+ file_mtime AS fileMtime,
1129
+ indexed_at AS indexedAt
1130
+ FROM vault_files
1131
+ WHERE id = ?
1132
+ `).get(fileId);
1133
+ if (!file) {
1134
+ throw new AppError(`Vault file not found: ${fileId}`, "not_found");
1135
+ }
1136
+ const localPath = await ensureLocalVaultFile(file, paths.vaultPath, env);
1137
+ openTarget(localPath);
1138
+ return {
1139
+ opened: true,
1140
+ fileId,
1141
+ path: localPath,
1142
+ };
1143
+ }
1144
+ finally {
1145
+ db.close();
1146
+ }
1147
+ }
1148
+ export function getDashboardLintSummary(env = process.env) {
1149
+ const issues = buildEnrichedLintIssues(env);
1150
+ const counts = {
1151
+ error: issues.filter((issue) => issue.level === "error").length,
1152
+ warning: issues.filter((issue) => issue.level === "warning").length,
1153
+ info: issues.filter((issue) => issue.level === "info").length,
1154
+ };
1155
+ const byRule = new Map();
1156
+ const byPage = new Map();
1157
+ for (const issue of issues) {
1158
+ byRule.set(issue.check, (byRule.get(issue.check) ?? 0) + 1);
1159
+ byPage.set(issue.pageId, (byPage.get(issue.pageId) ?? 0) + 1);
1160
+ }
1161
+ return {
1162
+ counts: {
1163
+ ...counts,
1164
+ total: counts.error + counts.warning + counts.info,
1165
+ },
1166
+ topRules: [...byRule.entries()]
1167
+ .map(([rule, count]) => ({ rule, count }))
1168
+ .sort((left, right) => right.count - left.count || left.rule.localeCompare(right.rule))
1169
+ .slice(0, 12),
1170
+ topPages: [...byPage.entries()]
1171
+ .map(([pageId, count]) => ({
1172
+ pageId,
1173
+ count,
1174
+ }))
1175
+ .sort((left, right) => right.count - left.count || left.pageId.localeCompare(right.pageId))
1176
+ .slice(0, 12),
1177
+ generatedAt: toOffsetIso(),
1178
+ };
1179
+ }
1180
+ export function listDashboardLintIssues(env = process.env, options = {}) {
1181
+ const level = normalizeLintLevel(options.level);
1182
+ const groupBy = normalizeGroupBy(options.groupBy);
1183
+ const issues = buildEnrichedLintIssues(env)
1184
+ .filter((issue) => (level ? issue.level === level : true))
1185
+ .filter((issue) => (options.rule ? issue.check === options.rule : true))
1186
+ .filter((issue) => (options.pageId ? issue.pageId === options.pageId : true))
1187
+ .sort((left, right) => left.pageId.localeCompare(right.pageId) || left.check.localeCompare(right.check));
1188
+ return {
1189
+ total: issues.length,
1190
+ ...buildLintIssueGroups(issues, groupBy),
1191
+ generatedAt: toOffsetIso(),
1192
+ };
1193
+ }
1194
+ export async function getDashboardStatus(env = process.env, daemonStatus, options = {}) {
1195
+ const { db, paths } = openRuntimeDb(env);
1196
+ try {
1197
+ const queue = getVaultQueueSnapshot(env);
1198
+ const stats = getWikiStat(env);
1199
+ const doctor = await buildDoctorReport(env, { probe: options.probe === true });
1200
+ const uptimeMs = daemonStatus.state?.startedAt && !Number.isNaN(new Date(daemonStatus.state.startedAt).getTime())
1201
+ ? Date.now() - new Date(daemonStatus.state.startedAt).getTime()
1202
+ : null;
1203
+ return {
1204
+ daemon: {
1205
+ ...daemonStatus,
1206
+ startedAt: daemonStatus.state?.startedAt ?? null,
1207
+ uptimeMs,
1208
+ },
1209
+ stats,
1210
+ queue: {
1211
+ pending: queue.totalPending,
1212
+ processing: queue.totalProcessing,
1213
+ done: queue.totalDone,
1214
+ skipped: queue.totalSkipped,
1215
+ error: queue.totalError,
1216
+ },
1217
+ runtime: {
1218
+ vaultSource: (env.VAULT_SOURCE ?? "local").trim().toLowerCase(),
1219
+ wikiPath: paths.wikiPath,
1220
+ vaultPath: paths.vaultPath,
1221
+ dbPath: paths.dbPath,
1222
+ },
1223
+ doctor,
1224
+ generatedAt: toOffsetIso(),
1225
+ lastSyncAt: getMeta(db, "last_sync_at"),
1226
+ };
1227
+ }
1228
+ finally {
1229
+ db.close();
1230
+ }
1231
+ }