@biaoo/tiangong-wiki 0.2.2 → 0.3.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/README.md +106 -1
- package/README.zh-CN.md +106 -1
- package/dist/commands/create.js +3 -0
- package/dist/commands/sync.js +3 -0
- package/dist/commands/template.js +3 -0
- package/dist/core/codex-workflow.js +37 -15
- package/dist/core/db.js +19 -0
- package/dist/core/onboarding.js +35 -1
- package/dist/core/page-source.js +25 -0
- package/dist/core/paths.js +10 -0
- package/dist/core/workflow-result.js +5 -0
- package/dist/daemon/audit-log.js +18 -0
- package/dist/daemon/client.js +1 -1
- package/dist/daemon/git-journal.js +114 -0
- package/dist/daemon/server.js +446 -124
- package/dist/daemon/write-actor.js +60 -0
- package/dist/daemon/write-queue.js +360 -0
- package/dist/operations/dashboard.js +4 -9
- package/dist/operations/write.js +93 -5
- package/mcp-server/dist/daemon-client.js +90 -0
- package/mcp-server/dist/index.js +26 -0
- package/mcp-server/dist/server.js +525 -0
- package/package.json +11 -5
- package/references/centralized-service-deployment.md +482 -0
- package/references/examples/centralized-service/centralized.env.example +25 -0
- package/references/examples/centralized-service/nginx-centralized-wiki.conf +68 -0
- package/references/examples/centralized-service/tiangong-wiki-daemon.service +17 -0
- package/references/examples/centralized-service/tiangong-wiki-mcp.service +18 -0
- package/references/troubleshooting.md +22 -0
package/dist/daemon/server.js
CHANGED
|
@@ -2,13 +2,20 @@ import http from "node:http";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { getMeta } from "../core/db.js";
|
|
5
|
-
import {
|
|
5
|
+
import { normalizePageId } from "../core/paths.js";
|
|
6
|
+
import { readCanonicalPageSourceById } from "../core/page-source.js";
|
|
7
|
+
import { selectPageById } from "../core/query.js";
|
|
8
|
+
import { loadRuntimeConfig, openRuntimeDb } from "../core/runtime.js";
|
|
9
|
+
import { DaemonWriteQueue } from "./write-queue.js";
|
|
10
|
+
import { appendAuditEvent } from "./audit-log.js";
|
|
11
|
+
import { commitWriteJournal, GitPushScheduler } from "./git-journal.js";
|
|
12
|
+
import { buildCliWriteActor, buildSystemWriteActor, resolveWriteActor } from "./write-actor.js";
|
|
6
13
|
import { exportGraphContent, exportIndexContent } from "../operations/export.js";
|
|
7
14
|
import { getDashboardGraphOverview, getDashboardLintSummary, getDashboardPageDetail, getDashboardPageSource, getDashboardQueueItemDetail, getDashboardQueueSummary, getDashboardStatus, getDashboardVaultFileDetail, getDashboardVaultSummary, listDashboardLintIssues, listDashboardQueueItems, listDashboardVaultFiles, openDashboardPageSource, openDashboardVaultFile, retryDashboardQueueItem, searchDashboardGraph, } from "../operations/dashboard.js";
|
|
8
15
|
import { diffVaultFiles, findPages, ftsSearchPages, getPageInfo, getVaultQueue, getWikiStat, listPages, listVaultFiles, renderLintResult, runLint, searchPages, traverseGraph, } from "../operations/query.js";
|
|
9
16
|
import { createTemplate, listTemplates, listTypes, recommendTypes, showTemplate, showType, } from "../operations/type-template.js";
|
|
10
17
|
import { runTemplateLint } from "../operations/template-lint.js";
|
|
11
|
-
import { createPage, runSync, runSyncCommand } from "../operations/write.js";
|
|
18
|
+
import { createPage, runSync, runSyncCommand, updatePage } from "../operations/write.js";
|
|
12
19
|
import { AppError, asAppError } from "../utils/errors.js";
|
|
13
20
|
import { pathExistsSync } from "../utils/fs.js";
|
|
14
21
|
import { addSeconds, toOffsetIso } from "../utils/time.js";
|
|
@@ -114,11 +121,19 @@ async function readJsonBody(request) {
|
|
|
114
121
|
throw new AppError(`Failed to parse daemon request body: ${error instanceof Error ? error.message : String(error)}`, "config");
|
|
115
122
|
}
|
|
116
123
|
}
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
function getErrorDetailsCode(error) {
|
|
125
|
+
if (typeof error.details !== "object" || error.details === null || !("code" in error.details)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const code = error.details.code;
|
|
129
|
+
return typeof code === "string" ? code : null;
|
|
130
|
+
}
|
|
131
|
+
function isConflictError(error) {
|
|
132
|
+
const code = getErrorDetailsCode(error);
|
|
133
|
+
return code === "busy" || code === "revision_conflict";
|
|
134
|
+
}
|
|
135
|
+
function isServiceUnavailableError(error) {
|
|
136
|
+
return getErrorDetailsCode(error) === "queue_full";
|
|
122
137
|
}
|
|
123
138
|
async function buildStatusPayload(env, state) {
|
|
124
139
|
let lastSyncAt = null;
|
|
@@ -156,7 +171,6 @@ export async function runDaemonServer(options) {
|
|
|
156
171
|
let cycleTimer = null;
|
|
157
172
|
let queuedCycle = false;
|
|
158
173
|
let stopping = false;
|
|
159
|
-
let currentWrite = null;
|
|
160
174
|
let server;
|
|
161
175
|
let resolveClosed = null;
|
|
162
176
|
let nextDashboardLogId = 1;
|
|
@@ -200,6 +214,7 @@ export async function runDaemonServer(options) {
|
|
|
200
214
|
broadcastDashboardLog(entry);
|
|
201
215
|
console.error(entry.line);
|
|
202
216
|
};
|
|
217
|
+
const gitPushScheduler = new GitPushScheduler(paths, logInfo, env);
|
|
203
218
|
const serveDashboardApp = (requestPath, response) => {
|
|
204
219
|
if (!pathExistsSync(dashboardDistPath)) {
|
|
205
220
|
throw new AppError(`Dashboard assets not found at ${dashboardDistPath}. Build the dashboard bundle before opening /dashboard.`, "not_found");
|
|
@@ -273,6 +288,44 @@ export async function runDaemonServer(options) {
|
|
|
273
288
|
writeDaemonState(paths.daemonStatePath, state);
|
|
274
289
|
}
|
|
275
290
|
};
|
|
291
|
+
const afterWriteComplete = (task) => {
|
|
292
|
+
if (queuedCycle && !stopping) {
|
|
293
|
+
queuedCycle = false;
|
|
294
|
+
void enqueueWriteTask("cycle", () => runCycleTransaction(buildSystemWriteActor("daemon"), "cycle"), {
|
|
295
|
+
summarizeResult: summarizeCycleResult,
|
|
296
|
+
}).catch((error) => {
|
|
297
|
+
const appError = asAppError(error);
|
|
298
|
+
logError(`queued cycle failed: ${appError.message}`);
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (task === "cycle" || task === "sync-trigger") {
|
|
303
|
+
scheduleNextCycle();
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
persistState();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const writeQueue = new DaemonWriteQueue(env, {
|
|
310
|
+
onJobStart: (job) => {
|
|
311
|
+
if (state) {
|
|
312
|
+
state.currentTask = job.taskType;
|
|
313
|
+
if (job.taskType === "cycle" || job.taskType === "sync-trigger") {
|
|
314
|
+
state.nextRunAt = null;
|
|
315
|
+
}
|
|
316
|
+
persistState();
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
onJobFinish: (job) => {
|
|
320
|
+
if (state) {
|
|
321
|
+
state.lastRunAt = toOffsetIso();
|
|
322
|
+
state.lastResult = job.status === "succeeded" ? "ok" : "error";
|
|
323
|
+
state.lastError = job.errorMessage;
|
|
324
|
+
state.currentTask = "idle";
|
|
325
|
+
}
|
|
326
|
+
afterWriteComplete(job.taskType);
|
|
327
|
+
},
|
|
328
|
+
});
|
|
276
329
|
const clearTimer = () => {
|
|
277
330
|
if (cycleTimer) {
|
|
278
331
|
clearTimeout(cycleTimer);
|
|
@@ -296,7 +349,7 @@ export async function runDaemonServer(options) {
|
|
|
296
349
|
if (stopping) {
|
|
297
350
|
return;
|
|
298
351
|
}
|
|
299
|
-
if (
|
|
352
|
+
if (writeQueue.hasWork()) {
|
|
300
353
|
queuedCycle = true;
|
|
301
354
|
if (state) {
|
|
302
355
|
state.nextRunAt = null;
|
|
@@ -304,113 +357,305 @@ export async function runDaemonServer(options) {
|
|
|
304
357
|
}
|
|
305
358
|
return;
|
|
306
359
|
}
|
|
307
|
-
void
|
|
360
|
+
void enqueueWriteTask("cycle", () => runCycleTransaction(buildSystemWriteActor("daemon"), "cycle"), {
|
|
361
|
+
summarizeResult: summarizeCycleResult,
|
|
362
|
+
}).catch((error) => {
|
|
308
363
|
const appError = asAppError(error);
|
|
309
364
|
logError(`scheduled cycle failed: ${appError.message}`);
|
|
310
365
|
});
|
|
311
366
|
}, interval * 1000);
|
|
312
367
|
};
|
|
313
|
-
const
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
void runDefaultCycle("cycle").catch((error) => {
|
|
317
|
-
const appError = asAppError(error);
|
|
318
|
-
logError(`queued cycle failed: ${appError.message}`);
|
|
319
|
-
});
|
|
320
|
-
return;
|
|
368
|
+
const enqueueWriteTask = async (task, run, options = {}) => {
|
|
369
|
+
if (stopping) {
|
|
370
|
+
throw new AppError("Wiki daemon is shutting down.", "runtime");
|
|
321
371
|
}
|
|
322
|
-
|
|
323
|
-
|
|
372
|
+
return writeQueue.enqueue(task, run, options);
|
|
373
|
+
};
|
|
374
|
+
const enrichWriteResult = (result, actor, git) => {
|
|
375
|
+
return {
|
|
376
|
+
...result,
|
|
377
|
+
writeMeta: {
|
|
378
|
+
requestId: actor.requestId,
|
|
379
|
+
actorId: actor.actorId,
|
|
380
|
+
actorType: actor.actorType,
|
|
381
|
+
auditLogPath: paths.auditLogPath,
|
|
382
|
+
git,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
const resolveCanonicalPageId = (inputPageId) => {
|
|
387
|
+
const { db, config } = openRuntimeDb(env);
|
|
388
|
+
try {
|
|
389
|
+
const normalizedPageId = normalizePageId(inputPageId, paths.wikiPath);
|
|
390
|
+
const page = selectPageById(db, config, normalizedPageId);
|
|
391
|
+
if (!page) {
|
|
392
|
+
throw new AppError(`Page not found: ${normalizedPageId}`, "not_found");
|
|
393
|
+
}
|
|
394
|
+
return String(page.id);
|
|
324
395
|
}
|
|
325
|
-
|
|
326
|
-
|
|
396
|
+
finally {
|
|
397
|
+
db.close();
|
|
327
398
|
}
|
|
328
399
|
};
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
400
|
+
const readPageRevision = (pageId) => {
|
|
401
|
+
return readCanonicalPageSourceById(pageId, paths.wikiPath, loadRuntimeConfig(env).config).revision;
|
|
402
|
+
};
|
|
403
|
+
const buildErrorDetails = (error) => {
|
|
404
|
+
const details = typeof error.details === "object" && error.details !== null && !Array.isArray(error.details)
|
|
405
|
+
? { ...error.details }
|
|
406
|
+
: {};
|
|
407
|
+
if (!("message" in details)) {
|
|
408
|
+
details.message = error.message;
|
|
409
|
+
}
|
|
410
|
+
return details;
|
|
411
|
+
};
|
|
412
|
+
const recordSyncFailureAndThrow = (actor, input) => {
|
|
413
|
+
appendAuditEvent(paths, actor, {
|
|
414
|
+
operation: input.operation,
|
|
415
|
+
resourceId: input.resourceId,
|
|
416
|
+
status: "sync_failed",
|
|
417
|
+
revisionBefore: input.revisionBefore,
|
|
418
|
+
revisionAfter: input.revisionAfter,
|
|
419
|
+
commitHash: null,
|
|
420
|
+
details: buildErrorDetails(input.error),
|
|
421
|
+
});
|
|
422
|
+
throw new AppError(input.error.message, input.error.type, {
|
|
423
|
+
...buildErrorDetails(input.error),
|
|
424
|
+
requestId: actor.requestId,
|
|
425
|
+
actorId: actor.actorId,
|
|
426
|
+
actorType: actor.actorType,
|
|
427
|
+
auditLogPath: paths.auditLogPath,
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
const finalizeJournaledWrite = async (actor, input) => {
|
|
431
|
+
appendAuditEvent(paths, actor, {
|
|
432
|
+
operation: input.operation,
|
|
433
|
+
resourceId: input.resourceId,
|
|
434
|
+
status: "write_applied",
|
|
435
|
+
revisionBefore: input.revisionBefore,
|
|
436
|
+
revisionAfter: input.revisionAfter,
|
|
437
|
+
commitHash: null,
|
|
438
|
+
details: input.details ?? null,
|
|
439
|
+
});
|
|
440
|
+
try {
|
|
441
|
+
const gitResult = commitWriteJournal(paths, actor, {
|
|
442
|
+
operation: input.operation,
|
|
443
|
+
resourceId: input.resourceId,
|
|
444
|
+
});
|
|
445
|
+
const pushScheduled = gitResult.status === "committed" ? gitPushScheduler.schedule(actor) : false;
|
|
446
|
+
appendAuditEvent(paths, actor, {
|
|
447
|
+
operation: input.operation,
|
|
448
|
+
resourceId: input.resourceId,
|
|
449
|
+
status: gitResult.status === "committed" ? "git_commit_succeeded" : "git_commit_skipped",
|
|
450
|
+
revisionBefore: input.revisionBefore,
|
|
451
|
+
revisionAfter: input.revisionAfter,
|
|
452
|
+
commitHash: gitResult.commitHash,
|
|
453
|
+
details: gitResult.status === "committed" ? { pushScheduled } : { reason: "no_staged_changes" },
|
|
454
|
+
});
|
|
455
|
+
return enrichWriteResult(input.result, actor, {
|
|
456
|
+
status: gitResult.status,
|
|
457
|
+
commitHash: gitResult.commitHash,
|
|
458
|
+
pushScheduled,
|
|
459
|
+
});
|
|
332
460
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
461
|
+
catch (error) {
|
|
462
|
+
const appError = asAppError(error);
|
|
463
|
+
appendAuditEvent(paths, actor, {
|
|
464
|
+
operation: input.operation,
|
|
465
|
+
resourceId: input.resourceId,
|
|
466
|
+
status: "git_commit_failed",
|
|
467
|
+
revisionBefore: input.revisionBefore,
|
|
468
|
+
revisionAfter: input.revisionAfter,
|
|
469
|
+
commitHash: null,
|
|
470
|
+
details: buildErrorDetails(appError),
|
|
471
|
+
});
|
|
472
|
+
throw new AppError("Git commit failed after write succeeded.", "runtime", {
|
|
473
|
+
code: "git_commit_failed",
|
|
474
|
+
degraded: true,
|
|
475
|
+
requestId: actor.requestId,
|
|
476
|
+
actorId: actor.actorId,
|
|
477
|
+
actorType: actor.actorType,
|
|
478
|
+
resourceId: input.resourceId,
|
|
479
|
+
revisionBefore: input.revisionBefore,
|
|
480
|
+
revisionAfter: input.revisionAfter,
|
|
481
|
+
auditLogPath: paths.auditLogPath,
|
|
482
|
+
writeResult: input.result,
|
|
483
|
+
gitError: appError.message,
|
|
337
484
|
});
|
|
338
485
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
486
|
+
};
|
|
487
|
+
const summarizeCycleResult = (result) => ({
|
|
488
|
+
status: result.status,
|
|
489
|
+
task: result.task,
|
|
490
|
+
sync: {
|
|
491
|
+
mode: result.sync.mode,
|
|
492
|
+
inserted: result.sync.inserted,
|
|
493
|
+
updated: result.sync.updated,
|
|
494
|
+
deleted: result.sync.deleted,
|
|
495
|
+
vaultChanges: result.sync.vault.changes,
|
|
496
|
+
},
|
|
497
|
+
queue: result.queue,
|
|
498
|
+
});
|
|
499
|
+
const runCycleTask = async (task) => {
|
|
500
|
+
logInfo(`${task}: start`);
|
|
501
|
+
const syncResult = await runSync(env);
|
|
502
|
+
logInfo(`${task}: sync ok mode=${syncResult.mode} inserted=${syncResult.inserted} updated=${syncResult.updated} deleted=${syncResult.deleted} vaultChanges=${syncResult.vault.changes}`);
|
|
503
|
+
const queueResult = {
|
|
504
|
+
enabled: false,
|
|
505
|
+
processed: 0,
|
|
506
|
+
done: 0,
|
|
507
|
+
skipped: 0,
|
|
508
|
+
errored: 0,
|
|
509
|
+
batches: 0,
|
|
510
|
+
};
|
|
511
|
+
while (!stopping) {
|
|
512
|
+
const batchResult = await processVaultQueueBatch(env, {
|
|
513
|
+
log: (message) => logInfo(`queue ${message}`),
|
|
514
|
+
});
|
|
515
|
+
if (!batchResult.enabled) {
|
|
516
|
+
break;
|
|
356
517
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
state.lastRunAt = toOffsetIso();
|
|
361
|
-
state.lastResult = "error";
|
|
362
|
-
state.lastError = appError.message;
|
|
363
|
-
state.currentTask = "idle";
|
|
364
|
-
}
|
|
365
|
-
throw appError;
|
|
518
|
+
queueResult.enabled = true;
|
|
519
|
+
if (batchResult.processed === 0) {
|
|
520
|
+
break;
|
|
366
521
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
522
|
+
queueResult.processed += batchResult.processed;
|
|
523
|
+
queueResult.done += batchResult.done;
|
|
524
|
+
queueResult.skipped += batchResult.skipped;
|
|
525
|
+
queueResult.errored += batchResult.errored;
|
|
526
|
+
queueResult.batches += 1;
|
|
527
|
+
}
|
|
528
|
+
if (queueResult.enabled) {
|
|
529
|
+
logInfo(`${task}: queue summary processed=${queueResult.processed} done=${queueResult.done} skipped=${queueResult.skipped} errored=${queueResult.errored} batches=${queueResult.batches}`);
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
status: "started",
|
|
533
|
+
task,
|
|
534
|
+
sync: syncResult,
|
|
535
|
+
queue: queueResult,
|
|
536
|
+
};
|
|
537
|
+
};
|
|
538
|
+
const runCreateTransaction = async (actor, input) => {
|
|
539
|
+
try {
|
|
540
|
+
const result = await createPage(env, input);
|
|
541
|
+
const revisionAfter = readPageRevision(result.created);
|
|
542
|
+
return await finalizeJournaledWrite(actor, {
|
|
543
|
+
operation: "create",
|
|
544
|
+
resourceId: result.created,
|
|
545
|
+
revisionBefore: null,
|
|
546
|
+
revisionAfter,
|
|
547
|
+
result,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
const appError = asAppError(error);
|
|
552
|
+
if (getErrorDetailsCode(appError) === "sync_failed") {
|
|
553
|
+
const details = buildErrorDetails(appError);
|
|
554
|
+
recordSyncFailureAndThrow(actor, {
|
|
555
|
+
operation: "create",
|
|
556
|
+
resourceId: typeof details.pageId === "string" ? details.pageId : null,
|
|
557
|
+
revisionBefore: null,
|
|
558
|
+
revisionAfter: typeof details.revisionAfter === "string" ? details.revisionAfter : null,
|
|
559
|
+
error: appError,
|
|
560
|
+
});
|
|
370
561
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return promise;
|
|
562
|
+
throw appError;
|
|
563
|
+
}
|
|
374
564
|
};
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
565
|
+
const runUpdateTransaction = async (actor, input) => {
|
|
566
|
+
const canonicalPageId = resolveCanonicalPageId(input.pageId);
|
|
567
|
+
const revisionBefore = readPageRevision(canonicalPageId);
|
|
568
|
+
try {
|
|
569
|
+
const result = await updatePage(env, input);
|
|
570
|
+
return await finalizeJournaledWrite(actor, {
|
|
571
|
+
operation: "update",
|
|
572
|
+
resourceId: result.pageId,
|
|
573
|
+
revisionBefore,
|
|
574
|
+
revisionAfter: result.revision,
|
|
575
|
+
result,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
catch (error) {
|
|
579
|
+
const appError = asAppError(error);
|
|
580
|
+
if (getErrorDetailsCode(appError) === "sync_failed") {
|
|
581
|
+
const details = buildErrorDetails(appError);
|
|
582
|
+
recordSyncFailureAndThrow(actor, {
|
|
583
|
+
operation: "update",
|
|
584
|
+
resourceId: typeof details.pageId === "string" ? details.pageId : canonicalPageId,
|
|
585
|
+
revisionBefore: typeof details.revisionBefore === "string" ? details.revisionBefore : revisionBefore,
|
|
586
|
+
revisionAfter: typeof details.revisionAfter === "string" ? details.revisionAfter : null,
|
|
587
|
+
error: appError,
|
|
391
588
|
});
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
589
|
+
}
|
|
590
|
+
throw appError;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
const runSyncTransaction = async (actor, input) => {
|
|
594
|
+
try {
|
|
595
|
+
const result = await runSyncCommand(env, input);
|
|
596
|
+
const resourceId = input.targetPaths?.[0] ?? input.vaultFileId ?? "*";
|
|
597
|
+
return await finalizeJournaledWrite(actor, {
|
|
598
|
+
operation: "sync",
|
|
599
|
+
resourceId,
|
|
600
|
+
revisionBefore: null,
|
|
601
|
+
revisionAfter: null,
|
|
602
|
+
result,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
const appError = asAppError(error);
|
|
607
|
+
return recordSyncFailureAndThrow(actor, {
|
|
608
|
+
operation: "sync",
|
|
609
|
+
resourceId: input.targetPaths?.[0] ?? input.vaultFileId ?? "*",
|
|
610
|
+
revisionBefore: null,
|
|
611
|
+
revisionAfter: null,
|
|
612
|
+
error: appError,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
const runCycleTransaction = async (actor, task) => {
|
|
617
|
+
try {
|
|
618
|
+
const result = await runCycleTask(task);
|
|
619
|
+
return await finalizeJournaledWrite(actor, {
|
|
620
|
+
operation: task,
|
|
621
|
+
resourceId: "*",
|
|
622
|
+
revisionBefore: null,
|
|
623
|
+
revisionAfter: null,
|
|
624
|
+
result,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
const appError = asAppError(error);
|
|
629
|
+
return recordSyncFailureAndThrow(actor, {
|
|
630
|
+
operation: task,
|
|
631
|
+
resourceId: "*",
|
|
632
|
+
revisionBefore: null,
|
|
633
|
+
revisionAfter: null,
|
|
634
|
+
error: appError,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const runTemplateCreateTransaction = async (actor, input) => {
|
|
639
|
+
const result = Promise.resolve(createTemplate(env, {
|
|
640
|
+
type: input.type,
|
|
641
|
+
title: input.title,
|
|
642
|
+
}));
|
|
643
|
+
return await finalizeJournaledWrite(actor, {
|
|
644
|
+
operation: "template-create",
|
|
645
|
+
resourceId: `template:${input.type}`,
|
|
646
|
+
revisionBefore: null,
|
|
647
|
+
revisionAfter: null,
|
|
648
|
+
result: await result,
|
|
649
|
+
});
|
|
650
|
+
};
|
|
651
|
+
const runQueueRetryTransaction = async (actor, fileId) => {
|
|
652
|
+
const result = await retryDashboardQueueItem(env, fileId);
|
|
653
|
+
return await finalizeJournaledWrite(actor, {
|
|
654
|
+
operation: "queue-retry",
|
|
655
|
+
resourceId: `queue:${fileId}`,
|
|
656
|
+
revisionBefore: null,
|
|
657
|
+
revisionAfter: null,
|
|
658
|
+
result,
|
|
414
659
|
});
|
|
415
660
|
};
|
|
416
661
|
const beginShutdown = async () => {
|
|
@@ -425,7 +670,7 @@ export async function runDaemonServer(options) {
|
|
|
425
670
|
persistState();
|
|
426
671
|
}
|
|
427
672
|
try {
|
|
428
|
-
await
|
|
673
|
+
await writeQueue.waitForIdle();
|
|
429
674
|
}
|
|
430
675
|
finally {
|
|
431
676
|
await new Promise((resolve) => {
|
|
@@ -464,36 +709,51 @@ export async function runDaemonServer(options) {
|
|
|
464
709
|
}
|
|
465
710
|
if (method === "POST" && pathname === "/sync") {
|
|
466
711
|
const body = await readJsonBody(request);
|
|
712
|
+
const actor = resolveWriteActor(request, body, buildCliWriteActor(env));
|
|
467
713
|
const pathValue = typeof body.path === "string" && body.path.trim()
|
|
468
714
|
? body.path.trim()
|
|
469
715
|
: Array.isArray(body.targetPaths) && typeof body.targetPaths[0] === "string"
|
|
470
716
|
? String(body.targetPaths[0])
|
|
471
717
|
: undefined;
|
|
472
|
-
const result = await
|
|
718
|
+
const result = await enqueueWriteTask("sync", async () => runSyncTransaction(actor, {
|
|
473
719
|
targetPaths: pathValue ? [pathValue] : undefined,
|
|
474
720
|
force: body.force === true,
|
|
475
721
|
skipEmbedding: body.skipEmbedding === true,
|
|
476
722
|
process: body.process === true,
|
|
477
723
|
vaultFileId: typeof body.vaultFileId === "string" && body.vaultFileId.trim() ? body.vaultFileId.trim() : undefined,
|
|
478
|
-
})
|
|
724
|
+
}), {
|
|
725
|
+
summarizeResult: (payload) => ({
|
|
726
|
+
mode: payload.mode,
|
|
727
|
+
inserted: payload.inserted,
|
|
728
|
+
updated: payload.updated,
|
|
729
|
+
deleted: payload.deleted,
|
|
730
|
+
queueProcessed: payload.queueProcess?.processed ?? 0,
|
|
731
|
+
}),
|
|
732
|
+
});
|
|
479
733
|
writeJsonResponse(response, 200, result);
|
|
480
734
|
return;
|
|
481
735
|
}
|
|
482
736
|
if (method === "POST" && pathname === "/sync/trigger") {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
});
|
|
488
|
-
}
|
|
489
|
-
void runDefaultCycle("sync-trigger").catch((error) => {
|
|
490
|
-
const appError = asAppError(error);
|
|
491
|
-
logError(`sync-trigger failed: ${appError.message}`);
|
|
492
|
-
});
|
|
493
|
-
writeJsonResponse(response, 200, {
|
|
494
|
-
status: "started",
|
|
495
|
-
currentTask: "sync-trigger",
|
|
737
|
+
const body = await readJsonBody(request);
|
|
738
|
+
const actor = resolveWriteActor(request, body, buildCliWriteActor(env));
|
|
739
|
+
const result = await enqueueWriteTask("sync-trigger", () => runCycleTransaction(actor, "sync-trigger"), {
|
|
740
|
+
summarizeResult: summarizeCycleResult,
|
|
496
741
|
});
|
|
742
|
+
writeJsonResponse(response, 200, result);
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (method === "GET" && pathname === "/write-queue/summary") {
|
|
746
|
+
writeJsonResponse(response, 200, writeQueue.getSummary());
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const writeQueueJobMatch = pathname.match(/^\/write-queue\/jobs\/(.+)$/);
|
|
750
|
+
if (method === "GET" && writeQueueJobMatch) {
|
|
751
|
+
const jobId = decodePathParam(writeQueueJobMatch[1]);
|
|
752
|
+
const job = writeQueue.getJob(jobId);
|
|
753
|
+
if (!job) {
|
|
754
|
+
throw new AppError(`Write queue job not found: ${jobId}`, "not_found");
|
|
755
|
+
}
|
|
756
|
+
writeJsonResponse(response, 200, job);
|
|
497
757
|
return;
|
|
498
758
|
}
|
|
499
759
|
if (method === "GET" && pathname === "/api/dashboard/status") {
|
|
@@ -535,7 +795,14 @@ export async function runDaemonServer(options) {
|
|
|
535
795
|
const queueRetryMatch = pathname.match(/^\/api\/dashboard\/queue\/items\/(.+)\/retry$/);
|
|
536
796
|
if (method === "POST" && queueRetryMatch) {
|
|
537
797
|
const fileId = decodePathParam(queueRetryMatch[1]);
|
|
538
|
-
|
|
798
|
+
const body = await readJsonBody(request);
|
|
799
|
+
const actor = resolveWriteActor(request, body, buildCliWriteActor(env));
|
|
800
|
+
writeJsonResponse(response, 200, await enqueueWriteTask("queue-retry", async () => runQueueRetryTransaction(actor, fileId), {
|
|
801
|
+
summarizeResult: (payload) => ({
|
|
802
|
+
fileId,
|
|
803
|
+
status: typeof payload.status === "string" ? payload.status : "unknown",
|
|
804
|
+
}),
|
|
805
|
+
}));
|
|
539
806
|
return;
|
|
540
807
|
}
|
|
541
808
|
const queueItemMatch = pathname.match(/^\/api\/dashboard\/queue\/items\/(.+)$/);
|
|
@@ -642,6 +909,28 @@ export async function runDaemonServer(options) {
|
|
|
642
909
|
writeJsonResponse(response, 200, getPageInfo(env, pageId));
|
|
643
910
|
return;
|
|
644
911
|
}
|
|
912
|
+
if (method === "GET" && pathname === "/page-read") {
|
|
913
|
+
const pageId = url.searchParams.get("pageId");
|
|
914
|
+
if (!pageId) {
|
|
915
|
+
throw new AppError("pageId is required", "config", {
|
|
916
|
+
code: "invalid_request",
|
|
917
|
+
field: "pageId",
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
const { db, config, paths } = openRuntimeDb(env);
|
|
921
|
+
try {
|
|
922
|
+
const normalizedPageId = normalizePageId(pageId, paths.wikiPath);
|
|
923
|
+
const page = selectPageById(db, config, normalizedPageId);
|
|
924
|
+
if (!page) {
|
|
925
|
+
throw new AppError(`Page not found: ${normalizedPageId}`, "not_found");
|
|
926
|
+
}
|
|
927
|
+
writeJsonResponse(response, 200, readCanonicalPageSourceById(String(page.id), paths.wikiPath, config));
|
|
928
|
+
}
|
|
929
|
+
finally {
|
|
930
|
+
db.close();
|
|
931
|
+
}
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
645
934
|
if (method === "GET" && pathname === "/list") {
|
|
646
935
|
writeJsonResponse(response, 200, listPages(env, {
|
|
647
936
|
type: url.searchParams.get("type") ?? undefined,
|
|
@@ -676,14 +965,37 @@ export async function runDaemonServer(options) {
|
|
|
676
965
|
}
|
|
677
966
|
if (method === "POST" && pathname === "/create") {
|
|
678
967
|
const body = await readJsonBody(request);
|
|
968
|
+
const actor = resolveWriteActor(request, body, buildCliWriteActor(env));
|
|
679
969
|
const type = typeof body.type === "string" ? body.type : "";
|
|
680
970
|
const title = typeof body.title === "string" ? body.title : "";
|
|
681
971
|
const nodeId = typeof body.nodeId === "string" ? body.nodeId : undefined;
|
|
682
|
-
const result = await
|
|
972
|
+
const result = await enqueueWriteTask("create", () => runCreateTransaction(actor, {
|
|
683
973
|
type,
|
|
684
974
|
title,
|
|
685
975
|
nodeId,
|
|
686
|
-
})
|
|
976
|
+
}), {
|
|
977
|
+
summarizeResult: (payload) => ({
|
|
978
|
+
created: payload.created,
|
|
979
|
+
filePath: payload.filePath,
|
|
980
|
+
}),
|
|
981
|
+
});
|
|
982
|
+
writeJsonResponse(response, 200, result);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (method === "POST" && pathname === "/page-update") {
|
|
986
|
+
const body = await readJsonBody(request);
|
|
987
|
+
const actor = resolveWriteActor(request, body, buildCliWriteActor(env));
|
|
988
|
+
const result = await enqueueWriteTask("update", () => runUpdateTransaction(actor, {
|
|
989
|
+
pageId: typeof body.pageId === "string" ? body.pageId : "",
|
|
990
|
+
bodyMarkdown: typeof body.bodyMarkdown === "string" ? body.bodyMarkdown : undefined,
|
|
991
|
+
frontmatterPatch: body.frontmatterPatch,
|
|
992
|
+
ifRevision: typeof body.ifRevision === "string" ? body.ifRevision : undefined,
|
|
993
|
+
}), {
|
|
994
|
+
summarizeResult: (payload) => ({
|
|
995
|
+
pageId: payload.pageId,
|
|
996
|
+
revision: payload.revision,
|
|
997
|
+
}),
|
|
998
|
+
});
|
|
687
999
|
writeJsonResponse(response, 200, result);
|
|
688
1000
|
return;
|
|
689
1001
|
}
|
|
@@ -736,10 +1048,16 @@ export async function runDaemonServer(options) {
|
|
|
736
1048
|
}
|
|
737
1049
|
if (method === "POST" && pathname === "/template/create") {
|
|
738
1050
|
const body = await readJsonBody(request);
|
|
739
|
-
const
|
|
1051
|
+
const actor = resolveWriteActor(request, body, buildCliWriteActor(env));
|
|
1052
|
+
const result = await enqueueWriteTask("template-create", () => runTemplateCreateTransaction(actor, {
|
|
740
1053
|
type: typeof body.type === "string" ? body.type : "",
|
|
741
1054
|
title: typeof body.title === "string" ? body.title : "",
|
|
742
|
-
})
|
|
1055
|
+
}), {
|
|
1056
|
+
summarizeResult: (payload) => ({
|
|
1057
|
+
pageType: typeof payload.pageType === "string" ? payload.pageType : String(body.type ?? ""),
|
|
1058
|
+
templatePath: typeof payload.templatePath === "string" ? payload.templatePath : null,
|
|
1059
|
+
}),
|
|
1060
|
+
});
|
|
743
1061
|
writeJsonResponse(response, 200, result);
|
|
744
1062
|
return;
|
|
745
1063
|
}
|
|
@@ -766,9 +1084,11 @@ export async function runDaemonServer(options) {
|
|
|
766
1084
|
? 400
|
|
767
1085
|
: appError.type === "not_found"
|
|
768
1086
|
? 404
|
|
769
|
-
:
|
|
770
|
-
?
|
|
771
|
-
:
|
|
1087
|
+
: isServiceUnavailableError(appError)
|
|
1088
|
+
? 503
|
|
1089
|
+
: isConflictError(appError)
|
|
1090
|
+
? 409
|
|
1091
|
+
: 500;
|
|
772
1092
|
writeJsonResponse(response, statusCode, {
|
|
773
1093
|
error: appError.message,
|
|
774
1094
|
type: appError.type,
|
|
@@ -801,7 +1121,9 @@ export async function runDaemonServer(options) {
|
|
|
801
1121
|
process.on("SIGTERM", signalHandler);
|
|
802
1122
|
process.on("SIGINT", signalHandler);
|
|
803
1123
|
if (interval > 0) {
|
|
804
|
-
void
|
|
1124
|
+
void enqueueWriteTask("cycle", () => runCycleTransaction(buildSystemWriteActor("daemon"), "cycle"), {
|
|
1125
|
+
summarizeResult: summarizeCycleResult,
|
|
1126
|
+
}).catch((error) => {
|
|
805
1127
|
const appError = asAppError(error);
|
|
806
1128
|
logError(`initial cycle failed: ${appError.message}`);
|
|
807
1129
|
});
|