@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,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
|
+
}
|