@musnows/scriverse 0.9.5 → 0.9.7
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-protocol.js +6 -0
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai-skills.js +134 -0
- package/dist/ai-skills.js.map +1 -0
- package/dist/ai.js +1546 -62
- package/dist/ai.js.map +1 -1
- package/dist/app.js +143 -19
- package/dist/app.js.map +1 -1
- package/dist/chapter-title-numbering.js +56 -0
- package/dist/chapter-title-numbering.js.map +1 -0
- package/dist/database.js +142 -2
- package/dist/database.js.map +1 -1
- package/dist/public/ai-skill-menu.js +42 -0
- package/dist/public/ai-usage.d.ts +5 -2
- package/dist/public/ai-usage.js +50 -37
- package/dist/public/app.js +917 -242
- package/dist/public/chapter-line-id-tracker.d.ts +6 -0
- package/dist/public/chapter-line-id-tracker.js +24 -0
- package/dist/public/index.html +22 -11
- package/dist/public/model-config.d.ts +1 -0
- package/dist/public/model-config.js +9 -4
- package/dist/public/styles.css +135 -11
- package/dist/remote-mcp.js +496 -0
- package/dist/remote-mcp.js.map +1 -0
- package/dist/security.js +4 -0
- package/dist/security.js.map +1 -1
- package/dist/semantic-search.js +225 -0
- package/dist/semantic-search.js.map +1 -0
- package/dist/skills/continue-writing/SKILL.md +23 -0
- package/dist/skills/polish-writing/SKILL.md +23 -0
- package/dist/store.js +246 -33
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +11 -0
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -1
package/dist/store.js
CHANGED
|
@@ -4,17 +4,18 @@ import { ENTITY_VERSION_BASELINE_MIGRATION_VERSION, PLATFORM_AI_WORK_ID, SYSTEM_
|
|
|
4
4
|
import { exportWorkDocx } from "./docx-export.js";
|
|
5
5
|
import { createEpubArchive } from "./epub-export.js";
|
|
6
6
|
import { AppError, notFound } from "./errors.js";
|
|
7
|
-
import { documentParagraphLineRanges, normalizeWorkSearchQuery } from "./hybrid-search.js";
|
|
7
|
+
import { documentParagraphLineRanges, hybridSearchPermissionModule, normalizeWorkSearchQuery } from "./hybrid-search.js";
|
|
8
8
|
import { accountReference, logger } from "./logger.js";
|
|
9
9
|
import { paginated, paginationSql } from "./pagination.js";
|
|
10
10
|
import { currentRequestActor } from "./request-context.js";
|
|
11
|
-
import { canWriteWorkModule, classifyWorkModulePermissions, emptyWorkModulePermissions, fullWorkModulePermissions, storedWorkModulePermissions } from "./work-permissions.js";
|
|
11
|
+
import { canReadWorkModule, canWriteWorkModule, classifyWorkModulePermissions, emptyWorkModulePermissions, fullWorkModulePermissions, storedWorkModulePermissions } from "./work-permissions.js";
|
|
12
12
|
import { countWords, documentShortSearchTerms, escapeSqlLikePattern, id, json, normalizeDocumentSearchText, now, splitDocumentParagraphs } from "./utils.js";
|
|
13
13
|
import { buildWritingCalendar, writingDateKey } from "./writing-progress-time.js";
|
|
14
14
|
import { resolveMaxAgentToolCallLimit } from "./ai-tool-results.js";
|
|
15
15
|
import { DEFAULT_AI_STREAM_IDLE_TIMEOUT_SECONDS, normalizeAiStreamIdleTimeoutSeconds } from "./ai-stream-timeout.js";
|
|
16
16
|
import { normalizeRoleplayScenePin, roleplayUserTurnDisplayText, roleplayUserTurnTitleSource } from "./roleplay-turn.js";
|
|
17
17
|
import { chapterAnnotationLineHashes, createChapterLineIds, MAX_CHAPTER_LINE_IDS, parseChapterAnnotationLineIds, parseChapterAnnotationLineHashes, parseChapterLineIds, reconcileChapterLineIds, reanchorChapterAnnotations } from "./chapter-annotation-anchor.js";
|
|
18
|
+
import { renumberChapterTitle } from "./chapter-title-numbering.js";
|
|
18
19
|
import { normalizeRoleplayMemoryContent, roleplayMemoryCandidateIsSafe } from "./roleplay-memory.js";
|
|
19
20
|
const WORK_LIST_BATCH_SIZE = 500;
|
|
20
21
|
const ENTITY_LIST_BATCH_SIZE = 400;
|
|
@@ -22,18 +23,51 @@ export const RECYCLE_BIN_RETENTION_DAYS = 30;
|
|
|
22
23
|
function recycleBinExpiresAt(deletedAt) {
|
|
23
24
|
return new Date(new Date(deletedAt).getTime() + RECYCLE_BIN_RETENTION_DAYS * 24 * 60 * 60_000).toISOString();
|
|
24
25
|
}
|
|
26
|
+
function chapterAnnotationListFilter(kinds, filters = {}) {
|
|
27
|
+
const where = [];
|
|
28
|
+
const params = [];
|
|
29
|
+
if (kinds !== undefined) {
|
|
30
|
+
if (kinds.length > 0) {
|
|
31
|
+
where.push(`annotation.kind IN (${kinds.map(() => "?").join(",")})`);
|
|
32
|
+
params.push(...kinds);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
where.push("1 = 0");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (filters.chapterId) {
|
|
39
|
+
where.push("annotation.chapter_id = ?");
|
|
40
|
+
params.push(filters.chapterId);
|
|
41
|
+
}
|
|
42
|
+
const normalizedQuery = filters.query?.normalize("NFKC").trim().slice(0, 100) ?? "";
|
|
43
|
+
if (normalizedQuery) {
|
|
44
|
+
const pattern = `%${escapeSqlLikePattern(normalizedQuery)}%`;
|
|
45
|
+
where.push(`(
|
|
46
|
+
annotation.note LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
47
|
+
OR annotation.quote LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
48
|
+
OR chapter.title LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
49
|
+
OR volume.title LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
50
|
+
)`);
|
|
51
|
+
params.push(pattern, pattern, pattern, pattern);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
sql: where.map((clause) => ` AND ${clause}`).join(""),
|
|
55
|
+
params
|
|
56
|
+
};
|
|
57
|
+
}
|
|
25
58
|
export const attachmentPermissionModules = ["prose", "drafts", "settings", "characters", "races", "organizations", "ai-chat"];
|
|
26
59
|
export const WORK_AGENT_TOOL_IDS = [
|
|
27
60
|
"story_index",
|
|
28
61
|
"read_chapters",
|
|
29
62
|
"grep",
|
|
30
63
|
"search_story_entities",
|
|
64
|
+
"semantic_search_story",
|
|
31
65
|
"read_character_sections",
|
|
32
66
|
"search_drafts",
|
|
33
67
|
"image",
|
|
34
68
|
"calculate_time"
|
|
35
69
|
];
|
|
36
|
-
const DEFAULT_WORK_AGENT_TOOLS =
|
|
70
|
+
const DEFAULT_WORK_AGENT_TOOLS = WORK_AGENT_TOOL_IDS.filter((toolId) => toolId !== "semantic_search_story");
|
|
37
71
|
const LEGACY_DEFAULT_WORK_AGENT_TOOLS = [
|
|
38
72
|
"story_index",
|
|
39
73
|
"read_chapters",
|
|
@@ -219,7 +253,12 @@ const legacyFavoriteTables = {
|
|
|
219
253
|
organization: "organizations"
|
|
220
254
|
};
|
|
221
255
|
export const AI_CONVERSATION_STREAM_REQUEST_LEASE_MS = 3 * 60_000;
|
|
222
|
-
export const aiConversationTaskTypes = ["chat", "roleplay"
|
|
256
|
+
export const aiConversationTaskTypes = ["chat", "roleplay"];
|
|
257
|
+
function normalizeAiConversationTaskType(value, roleplayCharacterId) {
|
|
258
|
+
if (roleplayCharacterId || value === "roleplay")
|
|
259
|
+
return "roleplay";
|
|
260
|
+
return "chat";
|
|
261
|
+
}
|
|
223
262
|
export function defaultAiConversationTitle(prompt) {
|
|
224
263
|
const normalized = roleplayUserTurnTitleSource(prompt).replace(/\s+/gu, " ").trim();
|
|
225
264
|
return Array.from(normalized).slice(0, 15).join("") || "新对话";
|
|
@@ -1071,6 +1110,18 @@ export class Store {
|
|
|
1071
1110
|
titleGenerationModelId: row?.title_generation_model_id === null || row?.title_generation_model_id === undefined
|
|
1072
1111
|
? null
|
|
1073
1112
|
: String(row.title_generation_model_id),
|
|
1113
|
+
semanticSearchEnabled: Number(row?.semantic_search_enabled ?? 0) === 1,
|
|
1114
|
+
semanticEmbeddingModelId: row?.semantic_embedding_model_id === null || row?.semantic_embedding_model_id === undefined
|
|
1115
|
+
? null
|
|
1116
|
+
: String(row.semantic_embedding_model_id),
|
|
1117
|
+
semanticRerankModelId: row?.semantic_rerank_model_id === null || row?.semantic_rerank_model_id === undefined
|
|
1118
|
+
? null
|
|
1119
|
+
: String(row.semantic_rerank_model_id),
|
|
1120
|
+
semanticVectorDimension: Math.min(65_536, Math.max(1, Number(row?.semantic_vector_dimension ?? 1_024) || 1_024)),
|
|
1121
|
+
semanticRecallLimit: Math.min(200, Math.max(1, Number(row?.semantic_recall_limit ?? 20) || 20)),
|
|
1122
|
+
semanticResultLimit: Math.min(100, Math.max(1, Number(row?.semantic_result_limit ?? 12) || 12)),
|
|
1123
|
+
semanticBudgetTokens: Math.min(100_000, Math.max(256, Number(row?.semantic_budget_tokens ?? 4_000) || 4_000)),
|
|
1124
|
+
semanticChannelWeight: Math.min(5, Math.max(0.1, Number(row?.semantic_channel_weight ?? 1) || 1)),
|
|
1074
1125
|
updatedAt: String(row?.updated_at ?? "")
|
|
1075
1126
|
};
|
|
1076
1127
|
}
|
|
@@ -1155,6 +1206,95 @@ export class Store {
|
|
|
1155
1206
|
});
|
|
1156
1207
|
return this.getWorkAiSettings(workId);
|
|
1157
1208
|
}
|
|
1209
|
+
updateWorkSemanticSearchSettings(workId, input) {
|
|
1210
|
+
this.getWork(workId);
|
|
1211
|
+
const current = this.getWorkAiSettings(workId);
|
|
1212
|
+
const enabled = input.enabled ?? Boolean(current.semanticSearchEnabled);
|
|
1213
|
+
const embeddingModelId = input.embeddingModelId === undefined
|
|
1214
|
+
? current.semanticEmbeddingModelId ? String(current.semanticEmbeddingModelId) : null
|
|
1215
|
+
: input.embeddingModelId?.trim() || null;
|
|
1216
|
+
const rerankModelId = input.rerankModelId === undefined
|
|
1217
|
+
? current.semanticRerankModelId ? String(current.semanticRerankModelId) : null
|
|
1218
|
+
: input.rerankModelId?.trim() || null;
|
|
1219
|
+
const vectorDimension = Math.min(65_536, Math.max(1, Math.trunc(input.vectorDimension ?? Number(current.semanticVectorDimension))));
|
|
1220
|
+
const recallLimit = Math.min(200, Math.max(1, Math.trunc(input.recallLimit ?? Number(current.semanticRecallLimit))));
|
|
1221
|
+
const resultLimit = Math.min(100, Math.max(1, Math.trunc(input.resultLimit ?? Number(current.semanticResultLimit))));
|
|
1222
|
+
const budgetTokens = Math.min(100_000, Math.max(256, Math.trunc(input.budgetTokens ?? Number(current.semanticBudgetTokens))));
|
|
1223
|
+
const channelWeight = Math.min(5, Math.max(0.1, Number(input.channelWeight ?? current.semanticChannelWeight)));
|
|
1224
|
+
const timestamp = now();
|
|
1225
|
+
this.db.transaction(() => {
|
|
1226
|
+
this.db.run(`INSERT INTO work_ai_settings (
|
|
1227
|
+
work_id, semantic_search_enabled, semantic_embedding_model_id, semantic_rerank_model_id,
|
|
1228
|
+
semantic_vector_dimension, semantic_recall_limit, semantic_result_limit,
|
|
1229
|
+
semantic_budget_tokens, semantic_channel_weight, updated_at
|
|
1230
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1231
|
+
ON CONFLICT(work_id) DO UPDATE SET
|
|
1232
|
+
semantic_search_enabled = excluded.semantic_search_enabled,
|
|
1233
|
+
semantic_embedding_model_id = excluded.semantic_embedding_model_id,
|
|
1234
|
+
semantic_rerank_model_id = excluded.semantic_rerank_model_id,
|
|
1235
|
+
semantic_vector_dimension = excluded.semantic_vector_dimension,
|
|
1236
|
+
semantic_recall_limit = excluded.semantic_recall_limit,
|
|
1237
|
+
semantic_result_limit = excluded.semantic_result_limit,
|
|
1238
|
+
semantic_budget_tokens = excluded.semantic_budget_tokens,
|
|
1239
|
+
semantic_channel_weight = excluded.semantic_channel_weight,
|
|
1240
|
+
updated_at = excluded.updated_at`, workId, enabled ? 1 : 0, embeddingModelId, rerankModelId, vectorDimension, recallLimit, resultLimit, budgetTokens, channelWeight, timestamp);
|
|
1241
|
+
this.audit(workId, "semantic.settings.updated", "work-ai-settings", workId, {
|
|
1242
|
+
enabled,
|
|
1243
|
+
embeddingModelId,
|
|
1244
|
+
rerankModelId,
|
|
1245
|
+
vectorDimension,
|
|
1246
|
+
recallLimit,
|
|
1247
|
+
resultLimit,
|
|
1248
|
+
budgetTokens,
|
|
1249
|
+
channelWeight
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
return this.getWorkAiSettings(workId);
|
|
1253
|
+
}
|
|
1254
|
+
getSemanticContextSnapshot(snapshotId, workId) {
|
|
1255
|
+
const work = this.getWork(workId);
|
|
1256
|
+
const row = this.db.get("SELECT * FROM semantic_context_snapshots WHERE id = ? AND work_id = ?", snapshotId, workId);
|
|
1257
|
+
if (!row)
|
|
1258
|
+
throw notFound("语义上下文快照");
|
|
1259
|
+
const actor = currentRequestActor();
|
|
1260
|
+
const createdByUserId = row.created_by_user_id === null || row.created_by_user_id === undefined
|
|
1261
|
+
? null
|
|
1262
|
+
: String(row.created_by_user_id);
|
|
1263
|
+
if (actor?.userId && createdByUserId && actor.userId !== createdByUserId) {
|
|
1264
|
+
throw new AppError(403, "SEMANTIC_SNAPSHOT_ACCESS_DENIED", "不能读取其他用户创建的语义上下文快照");
|
|
1265
|
+
}
|
|
1266
|
+
const permissions = work.modulePermissions;
|
|
1267
|
+
const items = this.db.all("SELECT * FROM semantic_context_snapshot_items WHERE snapshot_id = ? ORDER BY position", snapshotId).flatMap((item) => {
|
|
1268
|
+
const module = hybridSearchPermissionModule(String(item.source_type));
|
|
1269
|
+
if (!module || !canReadWorkModule(permissions, module))
|
|
1270
|
+
return [];
|
|
1271
|
+
return [{
|
|
1272
|
+
position: Number(item.position),
|
|
1273
|
+
entryId: String(item.entry_id),
|
|
1274
|
+
sourceType: String(item.source_type),
|
|
1275
|
+
sourceId: String(item.source_id),
|
|
1276
|
+
sectionId: String(item.section_id) || null,
|
|
1277
|
+
sourceVersion: String(item.source_version),
|
|
1278
|
+
sourceTitle: String(item.source_title),
|
|
1279
|
+
startLine: Number(item.start_line),
|
|
1280
|
+
endLine: Number(item.end_line),
|
|
1281
|
+
content: String(item.content),
|
|
1282
|
+
estimatedTokens: Number(item.estimated_tokens),
|
|
1283
|
+
matchKinds: json(String(item.match_kinds_json), ["semantic"])
|
|
1284
|
+
}];
|
|
1285
|
+
});
|
|
1286
|
+
return {
|
|
1287
|
+
id: String(row.id),
|
|
1288
|
+
workId: String(row.work_id),
|
|
1289
|
+
conversationId: row.conversation_id === null ? null : String(row.conversation_id),
|
|
1290
|
+
query: String(row.query),
|
|
1291
|
+
scope: json(String(row.scope_json), {}),
|
|
1292
|
+
configFingerprint: String(row.config_fingerprint),
|
|
1293
|
+
createdByUserId,
|
|
1294
|
+
createdAt: String(row.created_at),
|
|
1295
|
+
items
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1158
1298
|
clearAutoRunPause(workId) {
|
|
1159
1299
|
this.getWork(workId);
|
|
1160
1300
|
const current = this.getWorkAiSettings(workId);
|
|
@@ -2537,7 +2677,8 @@ export class Store {
|
|
|
2537
2677
|
return this.db.all(`WITH RECURSIVE annotation_lines(line, end_line) AS (
|
|
2538
2678
|
SELECT annotation.start_line, annotation.end_line
|
|
2539
2679
|
FROM chapter_annotations annotation
|
|
2540
|
-
WHERE annotation.chapter_id = ? AND annotation.deleted_at IS NULL
|
|
2680
|
+
WHERE annotation.chapter_id = ? AND annotation.deleted_at IS NULL
|
|
2681
|
+
AND NOT (annotation.kind = 'todo' AND annotation.status = 'resolved')${kindFilter.sql}
|
|
2541
2682
|
UNION ALL
|
|
2542
2683
|
SELECT line + 1, end_line
|
|
2543
2684
|
FROM annotation_lines
|
|
@@ -2548,55 +2689,69 @@ export class Store {
|
|
|
2548
2689
|
GROUP BY line
|
|
2549
2690
|
ORDER BY line`, chapterId, ...kindFilter.params).map((row) => ({ line: Number(row.line), count: Number(row.count) }));
|
|
2550
2691
|
}
|
|
2551
|
-
listWorkChapterAnnotations(workId, kinds) {
|
|
2692
|
+
listWorkChapterAnnotations(workId, kinds, filters = {}) {
|
|
2552
2693
|
this.getWork(workId);
|
|
2553
|
-
const
|
|
2554
|
-
? { sql: "", params: [] }
|
|
2555
|
-
: kinds.length > 0
|
|
2556
|
-
? { sql: ` AND annotation.kind IN (${kinds.map(() => "?").join(",")})`, params: [...kinds] }
|
|
2557
|
-
: { sql: " AND 1 = 0", params: [] };
|
|
2694
|
+
const filter = chapterAnnotationListFilter(kinds, filters);
|
|
2558
2695
|
return this.db.all(`SELECT annotation.*, user.display_name AS actor_display_name, user.username AS actor_username,
|
|
2559
2696
|
chapter.title AS chapter_title, volume.title AS volume_title
|
|
2560
2697
|
FROM chapter_annotations annotation
|
|
2561
2698
|
JOIN chapters chapter ON chapter.id = annotation.chapter_id
|
|
2562
2699
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
2563
2700
|
LEFT JOIN users user ON user.id = annotation.updated_by_user_id
|
|
2564
|
-
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL${
|
|
2565
|
-
ORDER BY CASE
|
|
2701
|
+
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL${filter.sql}
|
|
2702
|
+
ORDER BY CASE
|
|
2703
|
+
WHEN annotation.kind = 'todo' AND annotation.status = 'resolved' THEN 2
|
|
2704
|
+
WHEN annotation.status = 'open' THEN 0
|
|
2705
|
+
ELSE 1
|
|
2706
|
+
END,
|
|
2566
2707
|
volume.sort_order, volume.created_at, chapter.sort_order, chapter.created_at,
|
|
2567
|
-
annotation.start_line, annotation.created_at`, workId, ...
|
|
2708
|
+
annotation.start_line, annotation.created_at`, workId, ...filter.params).map((row) => ({
|
|
2568
2709
|
...this.mapChapterAnnotation(row),
|
|
2569
2710
|
volumeTitle: requiredString(row, "volume_title"),
|
|
2570
2711
|
chapterTitle: requiredString(row, "chapter_title")
|
|
2571
2712
|
}));
|
|
2572
2713
|
}
|
|
2573
|
-
listWorkChapterAnnotationsPage(workId, pagination, kinds) {
|
|
2714
|
+
listWorkChapterAnnotationsPage(workId, pagination, kinds, filters = {}) {
|
|
2574
2715
|
this.getWork(workId);
|
|
2575
2716
|
const page = paginationSql(pagination);
|
|
2576
|
-
const
|
|
2577
|
-
|
|
2578
|
-
: kinds.length > 0
|
|
2579
|
-
? { sql: ` AND annotation.kind IN (${kinds.map(() => "?").join(",")})`, params: [...kinds] }
|
|
2580
|
-
: { sql: " AND 1 = 0", params: [] };
|
|
2717
|
+
const filter = chapterAnnotationListFilter(kinds, filters);
|
|
2718
|
+
const optionFilter = chapterAnnotationListFilter(kinds);
|
|
2581
2719
|
const rows = this.db.all(`SELECT annotation.*, user.display_name AS actor_display_name, user.username AS actor_username,
|
|
2582
2720
|
chapter.title AS chapter_title, volume.title AS volume_title
|
|
2583
2721
|
FROM chapter_annotations annotation
|
|
2584
2722
|
JOIN chapters chapter ON chapter.id = annotation.chapter_id
|
|
2585
2723
|
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
2586
2724
|
LEFT JOIN users user ON user.id = annotation.updated_by_user_id
|
|
2587
|
-
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL${
|
|
2588
|
-
ORDER BY CASE
|
|
2725
|
+
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL${filter.sql}
|
|
2726
|
+
ORDER BY CASE
|
|
2727
|
+
WHEN annotation.kind = 'todo' AND annotation.status = 'resolved' THEN 2
|
|
2728
|
+
WHEN annotation.status = 'open' THEN 0
|
|
2729
|
+
ELSE 1
|
|
2730
|
+
END,
|
|
2589
2731
|
volume.sort_order, volume.created_at, chapter.sort_order, chapter.created_at,
|
|
2590
|
-
annotation.start_line, annotation.created_at${page.sql}`, workId, ...
|
|
2732
|
+
annotation.start_line, annotation.created_at${page.sql}`, workId, ...filter.params, ...page.params);
|
|
2591
2733
|
const total = numberValue(this.db.get(`SELECT COUNT(*) AS count
|
|
2592
2734
|
FROM chapter_annotations annotation
|
|
2593
2735
|
JOIN chapters chapter ON chapter.id = annotation.chapter_id
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2736
|
+
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
2737
|
+
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL${filter.sql}`, workId, ...filter.params) ?? {}, "count");
|
|
2738
|
+
const chapterOptions = this.db.all(`SELECT DISTINCT chapter.id, chapter.title, volume.title AS volume_title,
|
|
2739
|
+
volume.sort_order AS volume_sort_order, volume.created_at AS volume_created_at,
|
|
2740
|
+
chapter.sort_order AS chapter_sort_order, chapter.created_at AS chapter_created_at
|
|
2741
|
+
FROM chapter_annotations annotation
|
|
2742
|
+
JOIN chapters chapter ON chapter.id = annotation.chapter_id
|
|
2743
|
+
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
2744
|
+
WHERE annotation.work_id = ? AND annotation.deleted_at IS NULL AND chapter.deleted_at IS NULL${optionFilter.sql}
|
|
2745
|
+
ORDER BY volume.sort_order, volume.created_at, chapter.sort_order, chapter.created_at`, workId, ...optionFilter.params).map((row) => ({
|
|
2746
|
+
id: requiredString(row, "id"),
|
|
2747
|
+
title: requiredString(row, "title"),
|
|
2748
|
+
volumeTitle: requiredString(row, "volume_title")
|
|
2749
|
+
}));
|
|
2750
|
+
return { ...paginated(rows.map((row) => ({
|
|
2751
|
+
...this.mapChapterAnnotation(row),
|
|
2752
|
+
volumeTitle: requiredString(row, "volume_title"),
|
|
2753
|
+
chapterTitle: requiredString(row, "chapter_title")
|
|
2754
|
+
})), pagination, total), chapterOptions };
|
|
2600
2755
|
}
|
|
2601
2756
|
createChapterAnnotation(chapterId, input) {
|
|
2602
2757
|
const chapter = this.getChapter(chapterId);
|
|
@@ -2662,6 +2817,64 @@ export class Store {
|
|
|
2662
2817
|
return chapter;
|
|
2663
2818
|
});
|
|
2664
2819
|
const timestamp = now();
|
|
2820
|
+
if (action.type === "renumberTitles") {
|
|
2821
|
+
if (action.startAt + currentChapters.length - 1 > 999_999) {
|
|
2822
|
+
throw new AppError(400, "CHAPTER_NUMBER_RANGE", "起始序号与所选章节数量超出最大序号 999999");
|
|
2823
|
+
}
|
|
2824
|
+
const currentById = new Map(currentChapters.map((chapter) => [String(chapter.id), chapter]));
|
|
2825
|
+
const placeholders = currentChapters.map(() => "?").join(", ");
|
|
2826
|
+
const orderedChapters = this.db.all(`SELECT chapter.id FROM chapters chapter
|
|
2827
|
+
JOIN volumes volume ON volume.id = chapter.volume_id
|
|
2828
|
+
WHERE chapter.work_id = ? AND chapter.deleted_at IS NULL
|
|
2829
|
+
AND volume.deleted_at IS NULL AND chapter.id IN (${placeholders})
|
|
2830
|
+
ORDER BY volume.sort_order, volume.created_at, volume.id,
|
|
2831
|
+
chapter.sort_order, chapter.created_at, chapter.id`, workId, ...currentChapters.map((chapter) => String(chapter.id))).map((row) => currentById.get(requiredString(row, "id"))).filter((chapter) => Boolean(chapter));
|
|
2832
|
+
const renumbered = orderedChapters.map((chapter, index) => ({
|
|
2833
|
+
chapter,
|
|
2834
|
+
title: renumberChapterTitle(String(chapter.title), action.startAt + index, action.template, action.numberStyle),
|
|
2835
|
+
sequence: action.startAt + index
|
|
2836
|
+
}));
|
|
2837
|
+
const oversized = renumbered.find((item) => item.title.length > 300);
|
|
2838
|
+
if (oversized) {
|
|
2839
|
+
throw new AppError(400, "CHAPTER_TITLE_TOO_LONG", `章节“${String(oversized.chapter.title).slice(0, 40)}”重排后的标题超过 300 个字符`);
|
|
2840
|
+
}
|
|
2841
|
+
let updated = 0;
|
|
2842
|
+
for (const item of renumbered) {
|
|
2843
|
+
const chapter = item.chapter;
|
|
2844
|
+
if (item.title === chapter.title)
|
|
2845
|
+
continue;
|
|
2846
|
+
const chapterId = String(chapter.id);
|
|
2847
|
+
const versionNo = Number(chapter.versionNo) + 1;
|
|
2848
|
+
this.db.run("UPDATE chapters SET title = ?, version_no = ?, analysis_status = 'expired', updated_at = ? WHERE id = ?", item.title, versionNo, timestamp, chapterId);
|
|
2849
|
+
this.syncChapterParagraphSearchVersion(chapterId, versionNo);
|
|
2850
|
+
this.insertChapterVersionRow({
|
|
2851
|
+
workId,
|
|
2852
|
+
chapterId,
|
|
2853
|
+
versionNo,
|
|
2854
|
+
title: item.title,
|
|
2855
|
+
content: String(chapter.content),
|
|
2856
|
+
volumeId: String(chapter.volumeId),
|
|
2857
|
+
sortOrder: Number(chapter.sortOrder),
|
|
2858
|
+
chapterType: String(chapter.chapterType),
|
|
2859
|
+
source: "manual",
|
|
2860
|
+
sourceRef: null,
|
|
2861
|
+
changeNote: "批量重排章节标题序号",
|
|
2862
|
+
timestamp
|
|
2863
|
+
});
|
|
2864
|
+
this.invalidateChapter(workId, chapterId, versionNo);
|
|
2865
|
+
this.audit(workId, "chapter.saved", "chapter", chapterId, {
|
|
2866
|
+
previousTitle: chapter.title,
|
|
2867
|
+
title: item.title,
|
|
2868
|
+
sequence: item.sequence,
|
|
2869
|
+
versionNo,
|
|
2870
|
+
batch: true,
|
|
2871
|
+
renumbered: true
|
|
2872
|
+
});
|
|
2873
|
+
updated += 1;
|
|
2874
|
+
}
|
|
2875
|
+
this.db.run("UPDATE works SET updated_at = ? WHERE id = ?", timestamp, workId);
|
|
2876
|
+
return { processed: currentChapters.length, updated, action: action.type };
|
|
2877
|
+
}
|
|
2665
2878
|
if (action.type === "move") {
|
|
2666
2879
|
const targetVolume = this.getVolume(action.volumeId);
|
|
2667
2880
|
if (targetVolume.workId !== workId)
|
|
@@ -6711,7 +6924,7 @@ export class Store {
|
|
|
6711
6924
|
const roleplayCharacterId = optionalString(conversation, "roleplay_character_id");
|
|
6712
6925
|
return {
|
|
6713
6926
|
workId,
|
|
6714
|
-
taskType: (optionalString(conversation, "task_type")
|
|
6927
|
+
taskType: normalizeAiConversationTaskType(optionalString(conversation, "task_type"), roleplayCharacterId),
|
|
6715
6928
|
roleplayCharacterId,
|
|
6716
6929
|
roleplayUserCharacterId: optionalString(conversation, "roleplay_user_character_id"),
|
|
6717
6930
|
roleplayMemories: roleplayCharacterId ? this.getRoleplayMemoryPromptItems(workId, roleplayCharacterId) : [],
|
|
@@ -6953,7 +7166,7 @@ export class Store {
|
|
|
6953
7166
|
throw notFound("AI 对话");
|
|
6954
7167
|
const workId = requiredString(conversation, "work_id");
|
|
6955
7168
|
const previousCharacterId = optionalString(conversation, "roleplay_character_id");
|
|
6956
|
-
const previousTaskType = optionalString(conversation, "task_type")
|
|
7169
|
+
const previousTaskType = normalizeAiConversationTaskType(optionalString(conversation, "task_type"), previousCharacterId);
|
|
6957
7170
|
if (previousTaskType === taskType)
|
|
6958
7171
|
return this.getAiConversationSummary(conversationId);
|
|
6959
7172
|
const messageCount = Number(this.db.get("SELECT COUNT(*) AS count FROM ai_conversation_messages WHERE conversation_id = ?", conversationId)?.count ?? 0);
|
|
@@ -7257,7 +7470,7 @@ export class Store {
|
|
|
7257
7470
|
const systemClockText = optionalString(conversation, "system_clock_text") ?? "";
|
|
7258
7471
|
const scenePinJson = optionalString(conversation, "scene_pin_json") ?? "{}";
|
|
7259
7472
|
this.db.transaction(() => {
|
|
7260
|
-
this.db.run("INSERT INTO ai_conversations (id, work_id, roleplay_character_id, roleplay_user_character_id, task_type, context_scope_json, title, compacted_summary, compacted_message_count, agent_tools_json, injected_entities_json, system_clock_text, scene_pin_json, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", forkId, workId, optionalString(conversation, "roleplay_character_id"), optionalString(conversation, "roleplay_user_character_id"), optionalString(conversation, "task_type"), optionalString(conversation, "context_scope_json"), title.slice(0, 200), forkSummary, forkCompactedCount, conversation.agent_tools_json == null
|
|
7473
|
+
this.db.run("INSERT INTO ai_conversations (id, work_id, roleplay_character_id, roleplay_user_character_id, task_type, context_scope_json, title, compacted_summary, compacted_message_count, agent_tools_json, injected_entities_json, system_clock_text, scene_pin_json, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", forkId, workId, optionalString(conversation, "roleplay_character_id"), optionalString(conversation, "roleplay_user_character_id"), normalizeAiConversationTaskType(optionalString(conversation, "task_type"), optionalString(conversation, "roleplay_character_id")), optionalString(conversation, "context_scope_json"), title.slice(0, 200), forkSummary, forkCompactedCount, conversation.agent_tools_json == null
|
|
7261
7474
|
? JSON.stringify(normalizeWorkAgentTools(this.getWorkAiSettings(workId).agentTools))
|
|
7262
7475
|
: String(conversation.agent_tools_json), injectedEntitiesJson, systemClockText, scenePinJson, timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
7263
7476
|
for (const message of messages.slice(0, targetIndex + 1)) {
|
|
@@ -7330,7 +7543,7 @@ export class Store {
|
|
|
7330
7543
|
compactedMessageCount: numberValue(row, "compacted_message_count"),
|
|
7331
7544
|
hasCompactedSummary: Boolean(requiredString(row, "compacted_summary")),
|
|
7332
7545
|
contextWarningPending: Boolean(optionalString(row, "context_warning_at")),
|
|
7333
|
-
taskType: optionalString(row, "task_type")
|
|
7546
|
+
taskType: normalizeAiConversationTaskType(optionalString(row, "task_type"), roleplayCharacterId),
|
|
7334
7547
|
...(lockedModelId ? { modelId: lockedModelId } : {}),
|
|
7335
7548
|
...(hasImageAttachments ? { hasImageAttachments: true, modelLockedByImage: true } : {}),
|
|
7336
7549
|
contextScope: json(optionalString(row, "context_scope_json") ?? "", { type: "none" }),
|