@musnows/scriverse 0.7.4 → 0.7.6
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/dist/ai.js +49 -7
- package/dist/ai.js.map +1 -1
- package/dist/app.js +44 -45
- package/dist/app.js.map +1 -1
- package/dist/epub-export.js +42 -38
- package/dist/epub-export.js.map +1 -1
- package/dist/public/app.js +127 -54
- package/dist/public/index.html +6 -6
- package/dist/public/outline-board.d.ts +16 -10
- package/dist/public/outline-board.js +11 -105
- package/dist/public/reading-preview.d.ts +5 -1
- package/dist/public/reading-preview.js +17 -2
- package/dist/public/styles.css +11 -8
- package/dist/store.js +239 -63
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/zip-stream.js +149 -0
- package/dist/zip-stream.js.map +1 -0
- package/package.json +1 -1
package/dist/store.js
CHANGED
|
@@ -155,6 +155,9 @@ function settingsFromKnowledgeSections(sections) {
|
|
|
155
155
|
return sections.map((section) => section.contentMarkdown).filter((content) => content.trim());
|
|
156
156
|
}
|
|
157
157
|
const CHAPTER_OUTLINE_BOARD_PREVIEW_LENGTH = 600;
|
|
158
|
+
function chapterOutlineBoardLikePattern(value) {
|
|
159
|
+
return `%${value.normalize("NFKC").toLocaleLowerCase("zh-CN").replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
|
|
160
|
+
}
|
|
158
161
|
export const versionedEntityTypes = [
|
|
159
162
|
"work",
|
|
160
163
|
"volume",
|
|
@@ -1013,20 +1016,24 @@ export class Store {
|
|
|
1013
1016
|
return this.getWork(workId);
|
|
1014
1017
|
}
|
|
1015
1018
|
deleteWork(workId, expectedVersionNo) {
|
|
1016
|
-
this.db.transaction(() => {
|
|
1019
|
+
return this.db.transaction(() => {
|
|
1017
1020
|
const current = this.getWork(workId);
|
|
1018
1021
|
this.assertExpectedVersion("work", workId, expectedVersionNo, "作品", Number(current.versionNo));
|
|
1019
1022
|
const timestamp = now();
|
|
1023
|
+
const activeTaskIds = this.db.all("SELECT id FROM analysis_tasks WHERE work_id = ? AND status IN ('pending', 'running') ORDER BY id", workId).map((row) => requiredString(row, "id"));
|
|
1020
1024
|
const versionNo = this.recordEntityVersion("work", workId, "delete", null, "删除作品(可恢复)", timestamp);
|
|
1021
1025
|
this.db.run("UPDATE works SET version_no = ?, deleted_at = ?, updated_at = ? WHERE id = ?", versionNo, timestamp, timestamp, workId);
|
|
1026
|
+
this.db.run(`UPDATE analysis_tasks SET status = 'expired', next_attempt_at = NULL, updated_at = ?
|
|
1027
|
+
WHERE work_id = ? AND status IN ('pending', 'running')`, timestamp, workId);
|
|
1028
|
+
this.db.run("DELETE FROM relationship_source_index_queue WHERE work_id = ?", workId);
|
|
1022
1029
|
this.audit(workId, "work.deleted", "work", workId, {
|
|
1023
1030
|
title: current.title,
|
|
1024
1031
|
versionNo,
|
|
1025
1032
|
recoverable: true,
|
|
1026
1033
|
expiresAt: recycleBinExpiresAt(timestamp)
|
|
1027
1034
|
});
|
|
1035
|
+
return activeTaskIds;
|
|
1028
1036
|
});
|
|
1029
|
-
return [];
|
|
1030
1037
|
}
|
|
1031
1038
|
restoreWork(workId, expectedVersionNo) {
|
|
1032
1039
|
const deleted = this.db.get("SELECT * FROM works WHERE id = ? AND deleted_at IS NOT NULL", workId);
|
|
@@ -2520,10 +2527,101 @@ export class Store {
|
|
|
2520
2527
|
return null;
|
|
2521
2528
|
return this.mapChapterOutline(row, chapter);
|
|
2522
2529
|
}
|
|
2523
|
-
getChapterOutlineBoard(workId) {
|
|
2530
|
+
getChapterOutlineBoard(workId, filters, pagination) {
|
|
2524
2531
|
this.getWork(workId);
|
|
2525
2532
|
const previewLength = CHAPTER_OUTLINE_BOARD_PREVIEW_LENGTH;
|
|
2526
|
-
const
|
|
2533
|
+
const where = [
|
|
2534
|
+
"chapter.work_id = ?",
|
|
2535
|
+
"chapter.deleted_at IS NULL",
|
|
2536
|
+
"volume.deleted_at IS NULL"
|
|
2537
|
+
];
|
|
2538
|
+
const whereParams = [workId];
|
|
2539
|
+
if (filters.volumeId) {
|
|
2540
|
+
where.push("chapter.volume_id = ?");
|
|
2541
|
+
whereParams.push(filters.volumeId);
|
|
2542
|
+
}
|
|
2543
|
+
if (filters.outlineStatus === "empty") {
|
|
2544
|
+
where.push("outline.chapter_id IS NULL");
|
|
2545
|
+
}
|
|
2546
|
+
else if (filters.outlineStatus !== "all") {
|
|
2547
|
+
where.push("outline.status = ?");
|
|
2548
|
+
whereParams.push(filters.outlineStatus);
|
|
2549
|
+
}
|
|
2550
|
+
const associatedForeshadow = (statusSql = "") => `(
|
|
2551
|
+
EXISTS (
|
|
2552
|
+
SELECT 1 FROM foreshadow_occurrences occurrence
|
|
2553
|
+
JOIN foreshadows foreshadow ON foreshadow.id = occurrence.foreshadow_id
|
|
2554
|
+
WHERE occurrence.chapter_id = chapter.id AND foreshadow.work_id = chapter.work_id${statusSql}
|
|
2555
|
+
) OR EXISTS (
|
|
2556
|
+
SELECT 1 FROM foreshadows foreshadow
|
|
2557
|
+
WHERE foreshadow.work_id = chapter.work_id
|
|
2558
|
+
AND foreshadow.planned_payoff_chapter_id = chapter.id${statusSql}
|
|
2559
|
+
)
|
|
2560
|
+
)`;
|
|
2561
|
+
if (filters.foreshadowStatus === "none") {
|
|
2562
|
+
where.push(`NOT ${associatedForeshadow()}`);
|
|
2563
|
+
}
|
|
2564
|
+
else if (filters.foreshadowStatus !== "all") {
|
|
2565
|
+
const statusSql = filters.foreshadowStatus === "unresolved"
|
|
2566
|
+
? " AND foreshadow.status IN ('planned', 'planted')"
|
|
2567
|
+
: filters.foreshadowStatus === "resolved"
|
|
2568
|
+
? " AND foreshadow.status = 'resolved'"
|
|
2569
|
+
: " AND foreshadow.status = 'abandoned'";
|
|
2570
|
+
where.push(associatedForeshadow(statusSql));
|
|
2571
|
+
}
|
|
2572
|
+
const trimmedQuery = filters.query.trim();
|
|
2573
|
+
if (trimmedQuery) {
|
|
2574
|
+
const pattern = chapterOutlineBoardLikePattern(trimmedQuery);
|
|
2575
|
+
where.push(`(
|
|
2576
|
+
lower(chapter.title) LIKE ? ESCAPE '\\'
|
|
2577
|
+
OR lower(chapter.chapter_type) LIKE ? ESCAPE '\\'
|
|
2578
|
+
OR lower(outline.goal) LIKE ? ESCAPE '\\'
|
|
2579
|
+
OR lower(outline.conflict) LIKE ? ESCAPE '\\'
|
|
2580
|
+
OR lower(outline.turning_point) LIKE ? ESCAPE '\\'
|
|
2581
|
+
OR lower(outline.notes) LIKE ? ESCAPE '\\'
|
|
2582
|
+
OR EXISTS (
|
|
2583
|
+
SELECT 1 FROM foreshadow_occurrences occurrence
|
|
2584
|
+
JOIN foreshadows foreshadow ON foreshadow.id = occurrence.foreshadow_id
|
|
2585
|
+
WHERE occurrence.chapter_id = chapter.id AND foreshadow.work_id = chapter.work_id
|
|
2586
|
+
AND lower(foreshadow.title) LIKE ? ESCAPE '\\'
|
|
2587
|
+
)
|
|
2588
|
+
OR EXISTS (
|
|
2589
|
+
SELECT 1 FROM foreshadows foreshadow
|
|
2590
|
+
WHERE foreshadow.work_id = chapter.work_id
|
|
2591
|
+
AND foreshadow.planned_payoff_chapter_id = chapter.id
|
|
2592
|
+
AND lower(foreshadow.title) LIKE ? ESCAPE '\\'
|
|
2593
|
+
)
|
|
2594
|
+
)`);
|
|
2595
|
+
whereParams.push(...Array.from({ length: 8 }, () => pattern));
|
|
2596
|
+
}
|
|
2597
|
+
const whereSql = where.join(" AND ");
|
|
2598
|
+
const associationCount = (statusSql = "") => `(
|
|
2599
|
+
SELECT COUNT(*) FROM foreshadows sorted_foreshadow
|
|
2600
|
+
WHERE sorted_foreshadow.work_id = chapter.work_id${statusSql}
|
|
2601
|
+
AND (
|
|
2602
|
+
sorted_foreshadow.planned_payoff_chapter_id = chapter.id
|
|
2603
|
+
OR EXISTS (
|
|
2604
|
+
SELECT 1 FROM foreshadow_occurrences sorted_occurrence
|
|
2605
|
+
WHERE sorted_occurrence.foreshadow_id = sorted_foreshadow.id
|
|
2606
|
+
AND sorted_occurrence.chapter_id = chapter.id
|
|
2607
|
+
)
|
|
2608
|
+
)
|
|
2609
|
+
)`;
|
|
2610
|
+
const chapterTreeOrder = "chapter.sort_order, chapter.created_at, chapter.id";
|
|
2611
|
+
const chapterOrder = filters.sort === "status"
|
|
2612
|
+
? `CASE WHEN outline.chapter_id IS NULL THEN 0 WHEN outline.status = 'draft' THEN 1 WHEN outline.status = 'ready' THEN 2 ELSE 3 END, ${chapterTreeOrder}`
|
|
2613
|
+
: filters.sort === "foreshadows"
|
|
2614
|
+
? `${associationCount(" AND sorted_foreshadow.status IN ('planned', 'planted')")} DESC, ${associationCount()} DESC, ${chapterTreeOrder}`
|
|
2615
|
+
: filters.sort === "title"
|
|
2616
|
+
? `chapter.title COLLATE NOCASE, ${chapterTreeOrder}`
|
|
2617
|
+
: chapterTreeOrder;
|
|
2618
|
+
const orderSql = `volume.sort_order, volume.created_at, volume.id, ${chapterOrder}`;
|
|
2619
|
+
const total = numberValue(this.db.get(`SELECT COUNT(*) AS count
|
|
2620
|
+
FROM chapters chapter
|
|
2621
|
+
JOIN volumes volume ON volume.id = chapter.volume_id AND volume.work_id = chapter.work_id
|
|
2622
|
+
LEFT JOIN chapter_outlines outline ON outline.chapter_id = chapter.id
|
|
2623
|
+
WHERE ${whereSql}`, ...whereParams) ?? {}, "count");
|
|
2624
|
+
const pageRows = this.db.all(`SELECT volume.id AS volume_id, volume.title AS volume_title, volume.sort_order AS volume_order,
|
|
2527
2625
|
chapter.id AS chapter_id, chapter.title AS chapter_title, chapter.chapter_type,
|
|
2528
2626
|
chapter.sort_order AS chapter_order,
|
|
2529
2627
|
outline.chapter_id AS outline_chapter_id,
|
|
@@ -2532,34 +2630,59 @@ export class Store {
|
|
|
2532
2630
|
substr(outline.turning_point, 1, ?) AS turning_point, length(outline.turning_point) > ? AS turning_point_truncated,
|
|
2533
2631
|
substr(outline.notes, 1, ?) AS notes, length(outline.notes) > ? AS notes_truncated,
|
|
2534
2632
|
outline.status, outline.updated_at AS outline_updated_at
|
|
2633
|
+
FROM chapters chapter
|
|
2634
|
+
JOIN volumes volume ON volume.id = chapter.volume_id AND volume.work_id = chapter.work_id
|
|
2635
|
+
LEFT JOIN chapter_outlines outline ON outline.chapter_id = chapter.id
|
|
2636
|
+
WHERE ${whereSql}
|
|
2637
|
+
ORDER BY ${orderSql}
|
|
2638
|
+
LIMIT ? OFFSET ?`, previewLength, previewLength, previewLength, previewLength, previewLength, previewLength, previewLength, previewLength, ...whereParams, pagination.limit + 1, pagination.offset);
|
|
2639
|
+
const chapterPage = paginated(pageRows, pagination, total);
|
|
2640
|
+
const volumeRows = this.db.all(`SELECT volume.id, volume.title, volume.sort_order,
|
|
2641
|
+
COUNT(chapter.id) AS chapter_count
|
|
2535
2642
|
FROM volumes volume
|
|
2536
2643
|
LEFT JOIN chapters chapter
|
|
2537
2644
|
ON chapter.volume_id = volume.id AND chapter.work_id = volume.work_id AND chapter.deleted_at IS NULL
|
|
2538
|
-
LEFT JOIN chapter_outlines outline ON outline.chapter_id = chapter.id
|
|
2539
2645
|
WHERE volume.work_id = ? AND volume.deleted_at IS NULL
|
|
2540
|
-
|
|
2541
|
-
|
|
2646
|
+
GROUP BY volume.id
|
|
2647
|
+
ORDER BY volume.sort_order, volume.created_at, volume.id`, workId);
|
|
2648
|
+
const filteredVolumeRows = this.db.all(`SELECT chapter.volume_id, COUNT(*) AS chapter_count
|
|
2649
|
+
FROM chapters chapter
|
|
2650
|
+
JOIN volumes volume ON volume.id = chapter.volume_id AND volume.work_id = chapter.work_id
|
|
2651
|
+
LEFT JOIN chapter_outlines outline ON outline.chapter_id = chapter.id
|
|
2652
|
+
WHERE ${whereSql}
|
|
2653
|
+
GROUP BY chapter.volume_id`, ...whereParams);
|
|
2654
|
+
const filteredCountByVolume = new Map(filteredVolumeRows.map((row) => [
|
|
2655
|
+
requiredString(row, "volume_id"),
|
|
2656
|
+
numberValue(row, "chapter_count")
|
|
2657
|
+
]));
|
|
2658
|
+
const volumeOptions = volumeRows.map((row) => ({
|
|
2659
|
+
id: requiredString(row, "id"),
|
|
2660
|
+
title: requiredString(row, "title"),
|
|
2661
|
+
sortOrder: numberValue(row, "sort_order"),
|
|
2662
|
+
chapterCount: numberValue(row, "chapter_count"),
|
|
2663
|
+
filteredChapterCount: filteredCountByVolume.get(requiredString(row, "id")) ?? 0
|
|
2664
|
+
}));
|
|
2665
|
+
const volumeOptionById = new Map(volumeOptions.map((volume) => [volume.id, volume]));
|
|
2666
|
+
const volumeOrderById = new Map(volumeOptions.map((volume, index) => [volume.id, index]));
|
|
2542
2667
|
const volumeById = new Map();
|
|
2543
2668
|
const chapterById = new Map();
|
|
2544
|
-
|
|
2545
|
-
for (const row of rows) {
|
|
2669
|
+
for (const row of chapterPage.items) {
|
|
2546
2670
|
const volumeId = requiredString(row, "volume_id");
|
|
2547
2671
|
let volume = volumeById.get(volumeId);
|
|
2548
2672
|
if (!volume) {
|
|
2673
|
+
const summary = volumeOptionById.get(volumeId);
|
|
2549
2674
|
volume = {
|
|
2550
2675
|
id: volumeId,
|
|
2551
2676
|
title: requiredString(row, "volume_title"),
|
|
2552
2677
|
sortOrder: numberValue(row, "volume_order"),
|
|
2678
|
+
chapterCount: summary?.chapterCount ?? 0,
|
|
2679
|
+
filteredChapterCount: summary?.filteredChapterCount ?? 0,
|
|
2553
2680
|
chapters: []
|
|
2554
2681
|
};
|
|
2555
2682
|
volumeById.set(volumeId, volume);
|
|
2556
2683
|
}
|
|
2557
|
-
const chapterId =
|
|
2558
|
-
if (!chapterId)
|
|
2559
|
-
continue;
|
|
2684
|
+
const chapterId = requiredString(row, "chapter_id");
|
|
2560
2685
|
const hasOutline = optionalString(row, "outline_chapter_id") !== null;
|
|
2561
|
-
if (hasOutline)
|
|
2562
|
-
outlinedChapterCount += 1;
|
|
2563
2686
|
const chapter = {
|
|
2564
2687
|
id: chapterId,
|
|
2565
2688
|
title: requiredString(row, "chapter_title"),
|
|
@@ -2580,6 +2703,7 @@ export class Store {
|
|
|
2580
2703
|
volume.chapters.push(chapter);
|
|
2581
2704
|
chapterById.set(chapterId, chapter);
|
|
2582
2705
|
}
|
|
2706
|
+
const chapterIds = [...chapterById.keys()];
|
|
2583
2707
|
const associations = new Map();
|
|
2584
2708
|
const associate = (chapterId, source, role, plannedPayoff = false) => {
|
|
2585
2709
|
if (!chapterId || !chapterById.has(chapterId))
|
|
@@ -2597,28 +2721,29 @@ export class Store {
|
|
|
2597
2721
|
byForeshadow.set(source.id, summary);
|
|
2598
2722
|
associations.set(chapterId, byForeshadow);
|
|
2599
2723
|
};
|
|
2600
|
-
const
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2724
|
+
const placeholders = chapterIds.map(() => "?").join(", ");
|
|
2725
|
+
const foreshadowRows = chapterIds.length === 0 ? [] : this.db.all(`SELECT association.* FROM (
|
|
2726
|
+
SELECT chapter.id AS chapter_id, foreshadow.id, foreshadow.title, foreshadow.status,
|
|
2727
|
+
foreshadow.importance, foreshadow.created_at AS foreshadow_created_at,
|
|
2728
|
+
occurrence.role, 0 AS planned_payoff, occurrence.created_at AS association_created_at,
|
|
2729
|
+
occurrence.id AS association_id
|
|
2730
|
+
FROM chapters chapter
|
|
2731
|
+
JOIN foreshadow_occurrences occurrence ON occurrence.chapter_id = chapter.id
|
|
2732
|
+
JOIN foreshadows foreshadow ON foreshadow.id = occurrence.foreshadow_id
|
|
2733
|
+
WHERE chapter.id IN (${placeholders}) AND chapter.work_id = ? AND chapter.deleted_at IS NULL
|
|
2734
|
+
AND foreshadow.work_id = ?
|
|
2735
|
+
UNION ALL
|
|
2736
|
+
SELECT chapter.id AS chapter_id, foreshadow.id, foreshadow.title, foreshadow.status,
|
|
2737
|
+
foreshadow.importance, foreshadow.created_at AS foreshadow_created_at,
|
|
2738
|
+
NULL AS role, 1 AS planned_payoff, foreshadow.created_at AS association_created_at,
|
|
2739
|
+
foreshadow.id AS association_id
|
|
2740
|
+
FROM chapters chapter
|
|
2741
|
+
JOIN foreshadows foreshadow ON foreshadow.planned_payoff_chapter_id = chapter.id
|
|
2742
|
+
WHERE chapter.id IN (${placeholders}) AND chapter.work_id = ? AND chapter.deleted_at IS NULL
|
|
2743
|
+
AND foreshadow.work_id = ?
|
|
2744
|
+
) association
|
|
2745
|
+
ORDER BY CASE association.importance WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END,
|
|
2746
|
+
association.foreshadow_created_at, association.id, association.association_created_at, association.association_id`, ...chapterIds, workId, workId, ...chapterIds, workId, workId);
|
|
2622
2747
|
for (const row of foreshadowRows) {
|
|
2623
2748
|
const source = {
|
|
2624
2749
|
id: requiredString(row, "id"),
|
|
@@ -2626,15 +2751,8 @@ export class Store {
|
|
|
2626
2751
|
status: requiredString(row, "status"),
|
|
2627
2752
|
importance: requiredString(row, "importance")
|
|
2628
2753
|
};
|
|
2629
|
-
foreshadowIds.add(source.id);
|
|
2630
|
-
if (source.status === "planned" || source.status === "planted")
|
|
2631
|
-
unresolvedForeshadowIds.add(source.id);
|
|
2632
|
-
const occurrenceChapterId = optionalString(row, "occurrence_volume_id")
|
|
2633
|
-
? optionalString(row, "occurrence_chapter_id")
|
|
2634
|
-
: null;
|
|
2635
2754
|
const role = optionalString(row, "role");
|
|
2636
|
-
associate(
|
|
2637
|
-
associate(optionalString(row, "payoff_volume_id") ? optionalString(row, "payoff_chapter_id") : null, source, undefined, true);
|
|
2755
|
+
associate(requiredString(row, "chapter_id"), source, role === "setup" || role === "reminder" || role === "payoff" ? role : undefined, booleanValue(row, "planned_payoff"));
|
|
2638
2756
|
}
|
|
2639
2757
|
for (const [chapterId, byForeshadow] of associations) {
|
|
2640
2758
|
const chapter = chapterById.get(chapterId);
|
|
@@ -2645,14 +2763,47 @@ export class Store {
|
|
|
2645
2763
|
roles: [...summary.roles]
|
|
2646
2764
|
}));
|
|
2647
2765
|
}
|
|
2766
|
+
const chapterFiltersActive = Boolean(trimmedQuery || filters.outlineStatus !== "all" || filters.foreshadowStatus !== "all");
|
|
2767
|
+
if (pagination.page === 1) {
|
|
2768
|
+
for (const option of volumeOptions) {
|
|
2769
|
+
if (option.chapterCount !== 0)
|
|
2770
|
+
continue;
|
|
2771
|
+
if (filters.volumeId && option.id !== filters.volumeId)
|
|
2772
|
+
continue;
|
|
2773
|
+
if (chapterFiltersActive && filters.volumeId !== option.id)
|
|
2774
|
+
continue;
|
|
2775
|
+
volumeById.set(option.id, { ...option, chapters: [] });
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
const volumes = [...volumeById.values()].sort((left, right) => ((volumeOrderById.get(left.id) ?? Number.MAX_SAFE_INTEGER) - (volumeOrderById.get(right.id) ?? Number.MAX_SAFE_INTEGER)));
|
|
2779
|
+
const statsRow = this.db.get(`SELECT
|
|
2780
|
+
(SELECT COUNT(*) FROM chapters chapter
|
|
2781
|
+
JOIN volumes volume ON volume.id = chapter.volume_id AND volume.work_id = chapter.work_id
|
|
2782
|
+
WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL) AS chapter_count,
|
|
2783
|
+
(SELECT COUNT(*) FROM chapter_outlines outline
|
|
2784
|
+
JOIN chapters chapter ON chapter.id = outline.chapter_id
|
|
2785
|
+
JOIN volumes volume ON volume.id = chapter.volume_id AND volume.work_id = chapter.work_id
|
|
2786
|
+
WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL AND volume.deleted_at IS NULL) AS outlined_chapter_count,
|
|
2787
|
+
(SELECT COUNT(*) FROM foreshadows foreshadow WHERE foreshadow.work_id = ?) AS foreshadow_count,
|
|
2788
|
+
(SELECT COUNT(*) FROM foreshadows foreshadow
|
|
2789
|
+
WHERE foreshadow.work_id = ? AND foreshadow.status IN ('planned', 'planted')) AS unresolved_foreshadow_count`, workId, workId, workId, workId) ?? {};
|
|
2648
2790
|
return {
|
|
2649
2791
|
workId,
|
|
2650
|
-
volumes
|
|
2792
|
+
volumes,
|
|
2793
|
+
volumeOptions,
|
|
2794
|
+
filters: { ...filters, query: trimmedQuery },
|
|
2795
|
+
page: chapterPage.page,
|
|
2796
|
+
limit: chapterPage.limit,
|
|
2797
|
+
itemCount: chapterById.size,
|
|
2798
|
+
total,
|
|
2799
|
+
pageCount: Math.max(1, Math.ceil(total / pagination.limit)),
|
|
2800
|
+
hasMore: chapterPage.hasMore,
|
|
2801
|
+
nextPage: chapterPage.nextPage,
|
|
2651
2802
|
stats: {
|
|
2652
|
-
chapterCount:
|
|
2653
|
-
outlinedChapterCount,
|
|
2654
|
-
foreshadowCount:
|
|
2655
|
-
unresolvedForeshadowCount:
|
|
2803
|
+
chapterCount: numberValue(statsRow, "chapter_count"),
|
|
2804
|
+
outlinedChapterCount: numberValue(statsRow, "outlined_chapter_count"),
|
|
2805
|
+
foreshadowCount: numberValue(statsRow, "foreshadow_count"),
|
|
2806
|
+
unresolvedForeshadowCount: numberValue(statsRow, "unresolved_foreshadow_count")
|
|
2656
2807
|
}
|
|
2657
2808
|
};
|
|
2658
2809
|
}
|
|
@@ -5812,7 +5963,10 @@ export class Store {
|
|
|
5812
5963
|
return numberValue(row ?? {}, "value");
|
|
5813
5964
|
}
|
|
5814
5965
|
listAutoRunWorkIds() {
|
|
5815
|
-
return this.db.all(
|
|
5966
|
+
return this.db.all(`SELECT settings.work_id FROM work_ai_settings settings
|
|
5967
|
+
JOIN works work ON work.id = settings.work_id
|
|
5968
|
+
WHERE settings.auto_run_enabled = 1 AND work.deleted_at IS NULL
|
|
5969
|
+
ORDER BY settings.work_id`).map((row) => requiredString(row, "work_id"));
|
|
5816
5970
|
}
|
|
5817
5971
|
claimPendingTask(taskId, runningLimit) {
|
|
5818
5972
|
return this.db.transaction(() => {
|
|
@@ -7185,24 +7339,46 @@ export class Store {
|
|
|
7185
7339
|
});
|
|
7186
7340
|
}
|
|
7187
7341
|
async exportEpub(workId, volumeId) {
|
|
7188
|
-
const
|
|
7189
|
-
const
|
|
7190
|
-
const selectedVolume = volumeId ?
|
|
7342
|
+
const work = this.getWork(workId);
|
|
7343
|
+
const allVolumeRows = this.db.all("SELECT id, title FROM volumes WHERE work_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at", workId);
|
|
7344
|
+
const selectedVolume = volumeId ? allVolumeRows.find((volume) => requiredString(volume, "id") === volumeId) : undefined;
|
|
7191
7345
|
if (volumeId && !selectedVolume)
|
|
7192
7346
|
throw notFound("分卷");
|
|
7193
|
-
const
|
|
7194
|
-
const
|
|
7347
|
+
const sourceVolumeRows = selectedVolume ? [selectedVolume] : allVolumeRows;
|
|
7348
|
+
const chapterRows = volumeId
|
|
7349
|
+
? this.db.all(`SELECT id, volume_id, title, version_no FROM chapters
|
|
7350
|
+
WHERE work_id = ? AND volume_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at`, workId, volumeId)
|
|
7351
|
+
: this.db.all(`SELECT id, volume_id, title, version_no FROM chapters
|
|
7352
|
+
WHERE work_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at`, workId);
|
|
7353
|
+
const chaptersByVolume = new Map();
|
|
7354
|
+
for (const chapter of chapterRows) {
|
|
7355
|
+
const chapterVolumeId = requiredString(chapter, "volume_id");
|
|
7356
|
+
const chapters = chaptersByVolume.get(chapterVolumeId) ?? [];
|
|
7357
|
+
chapters.push(chapter);
|
|
7358
|
+
chaptersByVolume.set(chapterVolumeId, chapters);
|
|
7359
|
+
}
|
|
7360
|
+
const title = selectedVolume ? `${String(work.title)} - ${requiredString(selectedVolume, "title")}` : String(work.title);
|
|
7195
7361
|
const cover = this.findWorkCover(workId);
|
|
7196
7362
|
const archive = await createEpubArchive({
|
|
7197
7363
|
title,
|
|
7198
|
-
author: String(
|
|
7199
|
-
description: String(
|
|
7200
|
-
language: String(
|
|
7201
|
-
volumes:
|
|
7202
|
-
title:
|
|
7203
|
-
chapters: volume.
|
|
7204
|
-
title:
|
|
7205
|
-
content:
|
|
7364
|
+
author: String(work.author ?? ""),
|
|
7365
|
+
description: String(work.description ?? ""),
|
|
7366
|
+
language: String(work.language ?? "zh-CN"),
|
|
7367
|
+
volumes: sourceVolumeRows.map((volume) => ({
|
|
7368
|
+
title: requiredString(volume, "title"),
|
|
7369
|
+
chapters: (chaptersByVolume.get(requiredString(volume, "id")) ?? []).map((chapter) => ({
|
|
7370
|
+
title: requiredString(chapter, "title"),
|
|
7371
|
+
content: () => {
|
|
7372
|
+
const chapterId = requiredString(chapter, "id");
|
|
7373
|
+
const versionNo = numberValue(chapter, "version_no");
|
|
7374
|
+
const current = this.db.get("SELECT content FROM chapters WHERE id = ? AND work_id = ? AND version_no = ? AND deleted_at IS NULL", chapterId, workId, versionNo);
|
|
7375
|
+
if (current)
|
|
7376
|
+
return requiredString(current, "content");
|
|
7377
|
+
const historical = this.db.get("SELECT content FROM chapter_versions WHERE chapter_id = ? AND work_id = ? AND version_no = ?", chapterId, workId, versionNo);
|
|
7378
|
+
if (!historical)
|
|
7379
|
+
throw new AppError(409, "EPUB_EXPORT_SOURCE_CHANGED", "导出过程中章节版本已不可用,请重新导出");
|
|
7380
|
+
return requiredString(historical, "content");
|
|
7381
|
+
}
|
|
7206
7382
|
}))
|
|
7207
7383
|
})),
|
|
7208
7384
|
cover: cover ? { mimeType: cover.mimeType, content: cover.content } : null
|