@musnows/scriverse 0.5.11 → 0.5.12
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 +23 -1
- package/dist/ai.js.map +1 -1
- package/dist/app.js +19 -1
- package/dist/app.js.map +1 -1
- package/dist/database.js +18 -0
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +280 -63
- package/dist/public/index.html +2 -2
- package/dist/public/module-request-cache.d.ts +17 -0
- package/dist/public/module-request-cache.js +35 -0
- package/dist/public/stream-typewriter.d.ts +1 -1
- package/dist/public/stream-typewriter.js +16 -12
- package/dist/public/styles.css +10 -5
- package/dist/store.js +56 -16
- package/dist/store.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/writing-progress-time.js +71 -0
- package/dist/writing-progress-time.js.map +1 -0
- package/package.json +1 -1
package/dist/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
12
|
<link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
|
|
13
|
-
<link rel="stylesheet" href="/styles.css?v=20260730-
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260730-drafts-stream-v1">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -884,6 +884,6 @@
|
|
|
884
884
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
885
885
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
886
886
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
887
|
-
<script type="module" src="/app.js?v=20260730-
|
|
887
|
+
<script type="module" src="/app.js?v=20260730-volume-lazy-module-cache-v1"></script>
|
|
888
888
|
</body>
|
|
889
889
|
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ModuleRequestOptions {
|
|
2
|
+
refresh?: boolean;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface ModuleRequestCache {
|
|
6
|
+
request<T>(
|
|
7
|
+
workId: string,
|
|
8
|
+
module: string,
|
|
9
|
+
requestKey: string,
|
|
10
|
+
loader: () => Promise<T> | T,
|
|
11
|
+
options?: ModuleRequestOptions
|
|
12
|
+
): Promise<T>;
|
|
13
|
+
invalidate(workId: string, module: string): void;
|
|
14
|
+
clear(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createModuleRequestCache(): ModuleRequestCache;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function createModuleRequestCache() {
|
|
2
|
+
const scopes = new Map();
|
|
3
|
+
|
|
4
|
+
const scopeKey = (workId, module) => `${String(workId)}\u0000${String(module)}`;
|
|
5
|
+
const cloneResult = (value) => structuredClone(value);
|
|
6
|
+
|
|
7
|
+
function request(workId, module, requestKey, loader, { refresh = false } = {}) {
|
|
8
|
+
const key = scopeKey(workId, module);
|
|
9
|
+
let scope = scopes.get(key);
|
|
10
|
+
if (!scope) {
|
|
11
|
+
scope = new Map();
|
|
12
|
+
scopes.set(key, scope);
|
|
13
|
+
}
|
|
14
|
+
if (refresh) scope.delete(requestKey);
|
|
15
|
+
if (scope.has(requestKey)) return scope.get(requestKey).then(cloneResult);
|
|
16
|
+
|
|
17
|
+
const pending = Promise.resolve().then(loader);
|
|
18
|
+
scope.set(requestKey, pending);
|
|
19
|
+
pending.catch(() => {
|
|
20
|
+
if (scope.get(requestKey) === pending) scope.delete(requestKey);
|
|
21
|
+
if (scope.size === 0) scopes.delete(key);
|
|
22
|
+
});
|
|
23
|
+
return pending.then(cloneResult);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function invalidate(workId, module) {
|
|
27
|
+
scopes.delete(scopeKey(workId, module));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function clear() {
|
|
31
|
+
scopes.clear();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { request, invalidate, clear };
|
|
35
|
+
}
|
|
@@ -9,7 +9,7 @@ export type StreamTypewriter = {
|
|
|
9
9
|
reveal(): string;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export function streamTypewriterBatchSize(pendingCharacters: number,
|
|
12
|
+
export function streamTypewriterBatchSize(pendingCharacters: number, finishing?: boolean): number;
|
|
13
13
|
|
|
14
14
|
export function createStreamTypewriter<FrameHandle = number>(options: {
|
|
15
15
|
onRender: (text: string, progress: StreamTypewriterProgress) => void;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
1
|
+
const STREAMING_CHARACTERS_PER_FRAME = 1;
|
|
2
|
+
const FINISHING_CHARACTERS_PER_FRAME = 2;
|
|
3
|
+
const FINISHING_ACCELERATION = 0.9;
|
|
4
|
+
const MAX_STREAMING_CHARACTERS_PER_FRAME = 12;
|
|
5
|
+
const MAX_FINISHING_CHARACTERS_PER_FRAME = 24;
|
|
3
6
|
|
|
4
|
-
export function streamTypewriterBatchSize(pendingCharacters,
|
|
7
|
+
export function streamTypewriterBatchSize(pendingCharacters, finishing = false) {
|
|
5
8
|
const pending = Math.max(0, Math.floor(Number(pendingCharacters) || 0));
|
|
6
9
|
if (pending === 0) return 0;
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const minimum = finishing ? FINISHING_CHARACTERS_PER_FRAME : STREAMING_CHARACTERS_PER_FRAME;
|
|
11
|
+
const maximum = finishing ? MAX_FINISHING_CHARACTERS_PER_FRAME : MAX_STREAMING_CHARACTERS_PER_FRAME;
|
|
12
|
+
const adaptive = finishing
|
|
13
|
+
? Math.ceil(Math.sqrt(pending) * FINISHING_ACCELERATION)
|
|
14
|
+
: Math.ceil(pending / 30);
|
|
15
|
+
return Math.min(pending, maximum, Math.max(minimum, adaptive));
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export function createStreamTypewriter({
|
|
@@ -22,7 +27,7 @@ export function createStreamTypewriter({
|
|
|
22
27
|
const pendingCharacters = [];
|
|
23
28
|
const idleResolvers = [];
|
|
24
29
|
let scheduledFrame = null;
|
|
25
|
-
let
|
|
30
|
+
let finishing = false;
|
|
26
31
|
|
|
27
32
|
const snapshot = () => visibleCharacters.join("");
|
|
28
33
|
const resolveIdle = () => {
|
|
@@ -42,9 +47,8 @@ export function createStreamTypewriter({
|
|
|
42
47
|
scheduledFrame = null;
|
|
43
48
|
const batchSize = reducedMotion
|
|
44
49
|
? pendingCharacters.length
|
|
45
|
-
: streamTypewriterBatchSize(pendingCharacters.length,
|
|
50
|
+
: streamTypewriterBatchSize(pendingCharacters.length, finishing);
|
|
46
51
|
visibleCharacters.push(...pendingCharacters.splice(0, batchSize));
|
|
47
|
-
if (finishingFrames !== null) finishingFrames = Math.max(1, finishingFrames - 1);
|
|
48
52
|
render();
|
|
49
53
|
if (pendingCharacters.length) schedule();
|
|
50
54
|
else resolveIdle();
|
|
@@ -60,7 +64,7 @@ export function createStreamTypewriter({
|
|
|
60
64
|
},
|
|
61
65
|
finish() {
|
|
62
66
|
if (!pendingCharacters.length && scheduledFrame === null) return Promise.resolve(snapshot());
|
|
63
|
-
|
|
67
|
+
finishing = true;
|
|
64
68
|
schedule();
|
|
65
69
|
return new Promise((resolve) => idleResolvers.push(resolve));
|
|
66
70
|
},
|
|
@@ -70,7 +74,7 @@ export function createStreamTypewriter({
|
|
|
70
74
|
scheduledFrame = null;
|
|
71
75
|
}
|
|
72
76
|
visibleCharacters.push(...pendingCharacters.splice(0));
|
|
73
|
-
|
|
77
|
+
finishing = false;
|
|
74
78
|
render();
|
|
75
79
|
resolveIdle();
|
|
76
80
|
return snapshot();
|
package/dist/public/styles.css
CHANGED
|
@@ -1100,7 +1100,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1100
1100
|
.module-nav .ai-analysis-entry,
|
|
1101
1101
|
.module-nav .ai-analysis-entry:hover,
|
|
1102
1102
|
.module-nav .ai-analysis-entry.active { background: transparent; color: var(--accent-dark); font-weight: 650; }
|
|
1103
|
-
#module-more-button { grid-column:
|
|
1103
|
+
#module-more-button { grid-column: 2; color: var(--accent-dark); text-align: center; }
|
|
1104
1104
|
.panel-heading { display: flex; align-items: center; gap: 8px; padding: 15px 0 9px 7px; color: var(--muted); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; }
|
|
1105
1105
|
.panel-heading-actions { display: flex; align-items: center; gap: 7px; margin-left: auto; }
|
|
1106
1106
|
.chapter-batch-button { display: grid; flex: none; place-items: center; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); }
|
|
@@ -1277,7 +1277,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1277
1277
|
.character-filter-toolbar-actions button:disabled { cursor: default; opacity: .45; }
|
|
1278
1278
|
.draft-filter-toolbar { display: flex; align-items: end; gap: 10px; margin-bottom: 18px; padding: 12px 14px; border: 1px solid var(--line); background: var(--surface-soft); }
|
|
1279
1279
|
.draft-filter-toolbar label { color: var(--muted); font-size: 11px; }
|
|
1280
|
-
.draft-filter-toolbar select { min-width: 180px; padding: 8px 10px; background: var(--surface); }
|
|
1280
|
+
.draft-filter-toolbar select { min-width: 180px; min-height: 38px; padding: 8px 10px; background: var(--surface); font-size: 11px; }
|
|
1281
1281
|
.draft-filter-toolbar span { margin-left: auto; align-self: center; color: var(--muted); font-size: 11px; }
|
|
1282
1282
|
.character-card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1283
1283
|
.character-duplicate-review { grid-column: 1 / -1; }
|
|
@@ -2038,7 +2038,6 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2038
2038
|
.assistant-message > .message-heading, .user-message > .message-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 5px; opacity: .65; font-size: 9px; }
|
|
2039
2039
|
.message-heading > span { letter-spacing: .12em; }.message-heading time { flex: 0 0 auto; font-variant-numeric: tabular-nums; letter-spacing: .03em; }
|
|
2040
2040
|
.message-body { min-width: 0; overflow-wrap: anywhere; }
|
|
2041
|
-
.is-streaming .message-body:empty::after, .is-streaming .message-body > :last-child::after { content: ""; display: inline-block; width: 2px; height: 1.05em; margin-left: 3px; border-radius: 1px; background: var(--accent-dark); vertical-align: -.16em; animation: ai-stream-cursor 850ms steps(1, end) infinite; }
|
|
2042
2041
|
.message-card-actions { position: absolute; right: 0; bottom: -31px; display: flex; align-items: center; gap: 12px; height: 24px; }
|
|
2043
2042
|
.message-card-actions button { display: inline-flex; align-items: center; gap: 4px; min-width: 0; padding: 3px 2px; border: 0; background: transparent; color: var(--muted); font-size: 9px; }
|
|
2044
2043
|
.message-card-actions button:hover, .message-card-actions button:focus-visible { color: var(--accent-dark); }
|
|
@@ -2087,8 +2086,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2087
2086
|
.ai-process-intermediate-step .ai-process-step-body { border-left-color: var(--muted); }
|
|
2088
2087
|
.ai-process-tool-step { padding-top: 1px; }
|
|
2089
2088
|
@keyframes ai-process-pulse { 0%, 100% { opacity: .45; transform: scale(.8); } 50% { opacity: 1; transform: scale(1); } }
|
|
2090
|
-
@
|
|
2091
|
-
@media (prefers-reduced-motion: reduce) { .is-streaming .ai-process-details[open] > summary::before, .is-streaming .message-body:empty::after, .is-streaming .message-body > :last-child::after { animation: none; } }
|
|
2089
|
+
@media (prefers-reduced-motion: reduce) { .is-streaming .ai-process-details[open] > summary::before { animation: none; } }
|
|
2092
2090
|
.ai-tool-call-list { display: grid; gap: 5px; margin-top: 10px; }
|
|
2093
2091
|
.ai-tool-call-summary { display: flex; align-items: center; justify-content: space-between; width: 100%; min-width: 0; padding: 7px 9px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface-soft); color: var(--muted); font: 9px/1.4 var(--font-latin), monospace; text-align: left; }
|
|
2094
2092
|
.ai-tool-call-summary::after { content: "查看详情"; flex: 0 0 auto; margin-left: 8px; color: var(--accent-dark); }
|
|
@@ -2278,6 +2276,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2278
2276
|
.trace-dialog .dialog-fields { max-height: 68vh; }
|
|
2279
2277
|
.large-dialog { width: min(1120px, 94vw); max-height: 94dvh; }
|
|
2280
2278
|
.large-dialog .dialog-fields { max-height: 72dvh; padding: 24px 28px; }
|
|
2279
|
+
.editor-dialog { width: min(1180px, 94vw); max-height: calc(100dvh - 16px); }
|
|
2280
|
+
.editor-dialog .dialog-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); max-height: 76dvh; padding: 24px 28px; }
|
|
2281
|
+
.editor-dialog .dialog-fields > .form-field-note, .editor-dialog .dialog-fields > .markdown-editor-field { grid-column: 1 / -1; }
|
|
2281
2282
|
.dialog::backdrop { background: rgba(34,30,25,.42); backdrop-filter: blur(3px); }
|
|
2282
2283
|
.dialog-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 22px 24px 16px; border-bottom: 1px solid var(--line); }
|
|
2283
2284
|
.dialog-header h2 { margin: 0; font-size: 23px; font-weight: 500; }
|
|
@@ -2555,6 +2556,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2555
2556
|
.markdown-editor-field { display: grid; gap: 10px; min-width: 0; }
|
|
2556
2557
|
.markdown-editor-field > span { color: var(--muted); font-size: 11px; }
|
|
2557
2558
|
.markdown-editor-field .vditor-editor-host { min-height: 360px; }
|
|
2559
|
+
.editor-dialog .markdown-editor-field .vditor-editor-host.vditor { min-height: clamp(420px, 56dvh, 640px) !important; }
|
|
2558
2560
|
.vditor-editor-host { width: 100%; height: 100%; min-width: 0; min-height: 0; }
|
|
2559
2561
|
.vditor-editor-host.vditor { height: 100% !important; min-height: 0; overflow: hidden; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: var(--ink); }
|
|
2560
2562
|
.vditor-editor-host.vditor:not(.vditor--dark) { --border-color: var(--line); --panel-background-color: var(--surface); --toolbar-background-color: var(--surface-soft); --toolbar-icon-color: var(--muted); --toolbar-icon-hover-color: var(--accent-dark); --textarea-background-color: var(--surface); --textarea-text-color: var(--ink); --heading-border-color: var(--line); --blockquote-color: var(--muted); }
|
|
@@ -2613,6 +2615,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2613
2615
|
.character-editor-section-fields textarea { min-height: 120px; resize: vertical; line-height: 1.65; }
|
|
2614
2616
|
.character-editor-section-fields .section-list-row textarea { min-height: 180px; }
|
|
2615
2617
|
.character-editor-section-fields .member-chip { position: relative; display: inline-flex; width: auto; cursor: pointer; }
|
|
2618
|
+
.character-editor-section-fields .member-chip input[type="checkbox"] { position: absolute; width: 1px !important; min-width: 1px; height: 1px; padding: 0; border: 0; opacity: 0; }
|
|
2616
2619
|
.character-editor-section-fields .item-list-row button, .character-editor-section-fields .structured-list-row button { min-width: 54px; }
|
|
2617
2620
|
.knowledge-markdown-sections { display: grid; grid-column: 1 / -1; gap: 14px; min-width: 0; }
|
|
2618
2621
|
.knowledge-markdown-list-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
@@ -2814,6 +2817,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2814
2817
|
.setting-markdown-field { height: 70dvh; }
|
|
2815
2818
|
.setting-markdown-heading, .markdown-editor-field-heading { align-items: flex-start; flex-direction: column; gap: 4px; }
|
|
2816
2819
|
.setting-markdown-field > .vditor-editor-host, .markdown-editor-field .vditor-editor-host { min-height: 42dvh; }
|
|
2820
|
+
.editor-dialog .dialog-fields { grid-template-columns: minmax(0, 1fr); max-height: 76dvh; }
|
|
2821
|
+
.editor-dialog .markdown-editor-field .vditor-editor-host.vditor { min-height: 52dvh !important; }
|
|
2817
2822
|
.character-editor-workspace, .character-editor-workspace.history-open { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
|
|
2818
2823
|
.character-editor-nav { flex-direction: row; overflow-x: auto; padding: 8px 10px; border-right: 0; border-bottom: 1px solid var(--line); }
|
|
2819
2824
|
.character-editor-nav button { flex: 0 0 130px; }
|
package/dist/store.js
CHANGED
|
@@ -6,6 +6,7 @@ import { paginated, paginationSql } from "./pagination.js";
|
|
|
6
6
|
import { currentRequestActor } from "./request-context.js";
|
|
7
7
|
import { classifyWorkModulePermissions, emptyWorkModulePermissions, fullWorkModulePermissions, storedWorkModulePermissions } from "./work-permissions.js";
|
|
8
8
|
import { countWords, documentShortSearchTerms, id, json, normalizeDocumentSearchText, normalizeParagraphSpacing, now, splitDocumentParagraphs } from "./utils.js";
|
|
9
|
+
import { buildWritingCalendar, writingDateKey } from "./writing-progress-time.js";
|
|
9
10
|
const defaultPlatformPageSizes = {
|
|
10
11
|
drafts: 30,
|
|
11
12
|
settings: 30,
|
|
@@ -891,6 +892,41 @@ export class Store {
|
|
|
891
892
|
}));
|
|
892
893
|
return { ...work, volumes };
|
|
893
894
|
}
|
|
895
|
+
getWorkVolumeDirectory(workId) {
|
|
896
|
+
const work = this.getWork(workId);
|
|
897
|
+
const permissions = work.modulePermissions;
|
|
898
|
+
if (permissions.prose === "none")
|
|
899
|
+
return { ...work, volumes: [] };
|
|
900
|
+
const volumeRows = this.db.all(`SELECT volume.*,
|
|
901
|
+
(SELECT COUNT(*) FROM chapters chapter WHERE chapter.volume_id = volume.id AND chapter.deleted_at IS NULL) AS chapter_count
|
|
902
|
+
FROM volumes volume WHERE volume.work_id = ? ORDER BY volume.sort_order, volume.created_at`, workId);
|
|
903
|
+
const volumes = volumeRows.map((row) => ({
|
|
904
|
+
...this.mapVolume(row),
|
|
905
|
+
chapterCount: numberValue(row, "chapter_count"),
|
|
906
|
+
chapters: []
|
|
907
|
+
}));
|
|
908
|
+
return { ...work, volumes };
|
|
909
|
+
}
|
|
910
|
+
listVolumeChapters(volumeId) {
|
|
911
|
+
const volume = this.getVolume(volumeId);
|
|
912
|
+
const work = this.getWork(String(volume.workId));
|
|
913
|
+
if (work.modulePermissions.prose === "none")
|
|
914
|
+
return [];
|
|
915
|
+
return this.db.all(`SELECT id, work_id, volume_id, title, chapter_type, sort_order, word_count, version_no,
|
|
916
|
+
analysis_status, excluded_from_analysis, created_at, updated_at
|
|
917
|
+
FROM chapters WHERE volume_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at`, volumeId).map((row) => this.mapChapterDirectoryEntry(row));
|
|
918
|
+
}
|
|
919
|
+
listVolumeChaptersPage(volumeId, pagination) {
|
|
920
|
+
const volume = this.getVolume(volumeId);
|
|
921
|
+
const work = this.getWork(String(volume.workId));
|
|
922
|
+
if (work.modulePermissions.prose === "none")
|
|
923
|
+
return paginated([], pagination);
|
|
924
|
+
const page = paginationSql(pagination);
|
|
925
|
+
const rows = this.db.all(`SELECT id, work_id, volume_id, title, chapter_type, sort_order, word_count, version_no,
|
|
926
|
+
analysis_status, excluded_from_analysis, created_at, updated_at
|
|
927
|
+
FROM chapters WHERE volume_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at${page.sql}`, volumeId, ...page.params);
|
|
928
|
+
return paginated(rows.map((row) => this.mapChapterDirectoryEntry(row)), pagination);
|
|
929
|
+
}
|
|
894
930
|
getWorkDirectoryPage(workId, pagination) {
|
|
895
931
|
const work = this.getWork(workId);
|
|
896
932
|
const permissions = work.modulePermissions;
|
|
@@ -4125,16 +4161,26 @@ export class Store {
|
|
|
4125
4161
|
const conversation = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
|
|
4126
4162
|
if (!conversation)
|
|
4127
4163
|
throw notFound("AI 对话");
|
|
4164
|
+
const requestId = input.requestId?.trim() || null;
|
|
4165
|
+
if (requestId) {
|
|
4166
|
+
const existing = this.db.get("SELECT * FROM ai_conversation_messages WHERE conversation_id = ? AND request_id = ?", conversationId, requestId);
|
|
4167
|
+
if (existing)
|
|
4168
|
+
return this.mapAiConversationMessage(existing);
|
|
4169
|
+
}
|
|
4128
4170
|
const messageId = id("message");
|
|
4129
4171
|
const timestamp = now();
|
|
4130
4172
|
const title = requiredString(conversation, "title") === "新对话" && input.role === "user"
|
|
4131
4173
|
? input.content.replace(/\s+/gu, " ").trim().slice(0, 36) || "新对话"
|
|
4132
4174
|
: requiredString(conversation, "title");
|
|
4133
4175
|
this.db.transaction(() => {
|
|
4134
|
-
this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", messageId, conversationId, input.role, input.content, JSON.stringify(input.citations ?? []), JSON.stringify(input.metadata ?? {}), timestamp, currentRequestActor()?.userId ?? null);
|
|
4135
|
-
this.db.
|
|
4176
|
+
this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, request_id, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(conversation_id, request_id) WHERE request_id IS NOT NULL DO NOTHING", messageId, conversationId, input.role, input.content, JSON.stringify(input.citations ?? []), JSON.stringify(input.metadata ?? {}), requestId, timestamp, currentRequestActor()?.userId ?? null);
|
|
4177
|
+
const inserted = this.db.get("SELECT id FROM ai_conversation_messages WHERE id = ?", messageId);
|
|
4178
|
+
if (inserted)
|
|
4179
|
+
this.db.run("UPDATE ai_conversations SET title = ?, updated_at = ? WHERE id = ?", title, timestamp, conversationId);
|
|
4136
4180
|
});
|
|
4137
|
-
const message =
|
|
4181
|
+
const message = requestId
|
|
4182
|
+
? this.db.get("SELECT * FROM ai_conversation_messages WHERE conversation_id = ? AND request_id = ?", conversationId, requestId)
|
|
4183
|
+
: this.db.get("SELECT * FROM ai_conversation_messages WHERE id = ?", messageId);
|
|
4138
4184
|
if (!message)
|
|
4139
4185
|
throw notFound("AI 对话消息");
|
|
4140
4186
|
return this.mapAiConversationMessage(message);
|
|
@@ -4157,7 +4203,7 @@ export class Store {
|
|
|
4157
4203
|
this.db.transaction(() => {
|
|
4158
4204
|
this.db.run("INSERT INTO ai_conversations (id, work_id, title, compacted_summary, compacted_message_count, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", forkId, requiredString(conversation, "work_id"), title.slice(0, 200), forkSummary, forkCompactedCount, timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
4159
4205
|
for (const message of messages.slice(0, targetIndex + 1)) {
|
|
4160
|
-
this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", id("message"), forkId, requiredString(message, "role"), requiredString(message, "content"), requiredString(message, "citations_json"), requiredString(message, "metadata_json"), requiredString(message, "created_at"), currentRequestActor()?.userId ?? null);
|
|
4206
|
+
this.db.run("INSERT INTO ai_conversation_messages (id, conversation_id, role, content, citations_json, metadata_json, request_id, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", id("message"), forkId, requiredString(message, "role"), requiredString(message, "content"), requiredString(message, "citations_json"), requiredString(message, "metadata_json"), optionalString(message, "request_id"), requiredString(message, "created_at"), currentRequestActor()?.userId ?? null);
|
|
4161
4207
|
}
|
|
4162
4208
|
});
|
|
4163
4209
|
return this.getAiConversation(forkId);
|
|
@@ -4184,6 +4230,7 @@ export class Store {
|
|
|
4184
4230
|
content: requiredString(row, "content"),
|
|
4185
4231
|
citations: json(requiredString(row, "citations_json"), []),
|
|
4186
4232
|
metadata: json(requiredString(row, "metadata_json"), {}),
|
|
4233
|
+
requestId: optionalString(row, "request_id"),
|
|
4187
4234
|
createdAt: requiredString(row, "created_at")
|
|
4188
4235
|
};
|
|
4189
4236
|
}
|
|
@@ -5615,18 +5662,14 @@ export class Store {
|
|
|
5615
5662
|
const goal = this.db.get("SELECT * FROM writing_goals WHERE work_id = ?", workId);
|
|
5616
5663
|
const dailyGoal = goal ? numberValue(goal, "daily_goal") : 1000;
|
|
5617
5664
|
const targetTotal = goal ? numberValue(goal, "target_total") : 100000;
|
|
5618
|
-
const
|
|
5619
|
-
today.setUTCHours(0, 0, 0, 0);
|
|
5620
|
-
const start = new Date(today);
|
|
5621
|
-
start.setUTCDate(start.getUTCDate() - days + 1);
|
|
5622
|
-
const startKey = start.toISOString().slice(0, 10);
|
|
5665
|
+
const calendar = buildWritingCalendar(new Date(), days);
|
|
5623
5666
|
const versions = this.db.all(`SELECT chapter_id, content, source, created_at FROM chapter_versions
|
|
5624
|
-
WHERE work_id = ? AND created_at
|
|
5667
|
+
WHERE work_id = ? AND created_at < ? ORDER BY created_at, version_no, id`, workId, calendar.endExclusive);
|
|
5625
5668
|
const chapterWords = new Map();
|
|
5626
5669
|
const events = new Map();
|
|
5627
5670
|
for (const version of versions) {
|
|
5628
|
-
const day = requiredString(version, "created_at")
|
|
5629
|
-
if (day < startKey) {
|
|
5671
|
+
const day = writingDateKey(new Date(requiredString(version, "created_at")), calendar.timeZone);
|
|
5672
|
+
if (day < calendar.startKey) {
|
|
5630
5673
|
chapterWords.set(requiredString(version, "chapter_id"), requiredString(version, "source") === "delete" ? 0 : countWords(requiredString(version, "content")));
|
|
5631
5674
|
}
|
|
5632
5675
|
else {
|
|
@@ -5637,10 +5680,7 @@ export class Store {
|
|
|
5637
5680
|
}
|
|
5638
5681
|
let previousTotal = [...chapterWords.values()].reduce((sum, value) => sum + value, 0);
|
|
5639
5682
|
const trend = [];
|
|
5640
|
-
for (
|
|
5641
|
-
const date = new Date(start);
|
|
5642
|
-
date.setUTCDate(start.getUTCDate() + index);
|
|
5643
|
-
const day = date.toISOString().slice(0, 10);
|
|
5683
|
+
for (const day of calendar.dateKeys) {
|
|
5644
5684
|
for (const version of events.get(day) ?? []) {
|
|
5645
5685
|
chapterWords.set(requiredString(version, "chapter_id"), requiredString(version, "source") === "delete" ? 0 : countWords(requiredString(version, "content")));
|
|
5646
5686
|
}
|