@musnows/scriverse 0.5.11 → 0.6.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/dist/ai-protocol.js +6 -1
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai.js +358 -21
- package/dist/ai.js.map +1 -1
- package/dist/app.js +33 -5
- package/dist/app.js.map +1 -1
- package/dist/database.js +35 -0
- package/dist/database.js.map +1 -1
- package/dist/public/ai-context-meter.js +32 -0
- package/dist/public/app.js +459 -100
- package/dist/public/index.html +7 -3
- 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 +55 -11
- package/dist/store.js +96 -23
- package/dist/store.js.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +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-ai-error-drafts-filter-align-model-availability-token-distribution-v1">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -309,7 +309,11 @@
|
|
|
309
309
|
<div id="ai-prompt" class="ai-prompt" contenteditable="true" role="textbox" aria-multiline="true" aria-keyshortcuts="Enter" title="Enter 发送,Shift+Enter 换行" data-placeholder="告诉 AI 你想讨论或修改什么……"></div>
|
|
310
310
|
<div id="ai-mention-menu" class="ai-mention-menu hidden" role="listbox" aria-label="引用角色、设定或章节"></div>
|
|
311
311
|
<div class="prompt-composer-actions">
|
|
312
|
-
<
|
|
312
|
+
<button id="ai-context-meter" class="ai-context-meter is-empty" type="button" aria-haspopup="dialog" aria-expanded="false" aria-controls="ai-context-popover" aria-live="polite" aria-label="当前上下文用量"><b>—</b></button>
|
|
313
|
+
<section id="ai-context-popover" class="ai-context-popover hidden" role="dialog" aria-labelledby="ai-context-popover-title" aria-describedby="ai-context-popover-description">
|
|
314
|
+
<header class="ai-context-popover-header"><div><strong id="ai-context-popover-title">Token 分布</strong><small id="ai-context-popover-description">按当前模型上下文窗口计算</small></div><button id="ai-context-popover-close" class="ai-context-popover-close" type="button" aria-label="关闭 Token 分布">×</button></header>
|
|
315
|
+
<div id="ai-context-distribution" class="ai-context-distribution" role="list" aria-label="当前 Token 分布"></div>
|
|
316
|
+
</section>
|
|
313
317
|
<button id="ai-send" class="ai-send-button" type="button" aria-label="发送消息">发送</button>
|
|
314
318
|
</div>
|
|
315
319
|
</div>
|
|
@@ -884,6 +888,6 @@
|
|
|
884
888
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
885
889
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
886
890
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
887
|
-
<script type="module" src="/app.js?v=20260730-
|
|
891
|
+
<script type="module" src="/app.js?v=20260730-ai-error-model-availability-token-distribution-conversation-title-v1"></script>
|
|
888
892
|
</body>
|
|
889
893
|
</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); }
|
|
@@ -1275,9 +1275,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1275
1275
|
.character-filter-toolbar-actions { display: flex; align-items: center; justify-content: flex-end; gap: 12px; min-height: 38px; }
|
|
1276
1276
|
.character-filter-result-count { color: var(--muted); font-size: 11px; white-space: nowrap; }
|
|
1277
1277
|
.character-filter-toolbar-actions button:disabled { cursor: default; opacity: .45; }
|
|
1278
|
-
.draft-filter-toolbar { display: flex; align-items:
|
|
1278
|
+
.draft-filter-toolbar { display: flex; align-items: center; 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; }
|
|
@@ -1301,9 +1301,24 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1301
1301
|
.card-actions .primary-button:hover, .card-actions .primary-button:focus-visible { background: var(--accent-dark); }
|
|
1302
1302
|
.provider-card-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 420px), 1fr)); }
|
|
1303
1303
|
.provider-card { min-width: 0; }
|
|
1304
|
+
.provider-card.is-disabled { border-style: dashed; border-color: color-mix(in srgb, var(--accent) 68%, var(--line)); background: color-mix(in srgb, var(--accent) 5%, var(--surface-soft)); }
|
|
1305
|
+
.provider-card-meta { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
|
|
1306
|
+
.provider-card-meta small { min-width: 0; }
|
|
1307
|
+
.provider-status-badge, .model-status-badge { display: inline-flex; flex: 0 0 auto; align-items: center; min-height: 22px; padding: 3px 8px; border: 1px solid var(--line); border-radius: 999px; font-size: 9px; font-weight: 700; line-height: 1.1; white-space: nowrap; }
|
|
1308
|
+
.provider-status-badge.is-enabled { border-color: color-mix(in srgb, var(--green) 48%, var(--line)); background: color-mix(in srgb, var(--green) 10%, var(--surface)); color: var(--green); }
|
|
1309
|
+
.provider-status-badge.is-disabled, .model-status-badge.is-disabled { border-color: color-mix(in srgb, var(--accent) 68%, var(--line)); background: color-mix(in srgb, var(--accent) 18%, var(--surface)); color: var(--accent-dark); }
|
|
1310
|
+
.provider-status-badge.is-error, .model-status-badge.is-unavailable { border-color: color-mix(in srgb, var(--accent) 46%, var(--line)); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); color: var(--accent-dark); }
|
|
1311
|
+
.provider-disabled-notice { display: flex; align-items: baseline; gap: 8px; margin: 12px 0; padding: 9px 11px; border-left: 3px solid var(--accent); background: color-mix(in srgb, var(--accent) 10%, var(--surface)); }
|
|
1312
|
+
.provider-disabled-notice strong { color: var(--accent-dark); font-size: 12px; }
|
|
1313
|
+
.provider-disabled-notice span { color: var(--muted); font-size: 10px; line-height: 1.4; }
|
|
1304
1314
|
.provider-card p, .provider-card .model-pill { overflow-wrap: anywhere; word-break: break-word; }
|
|
1305
1315
|
.provider-models { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
|
|
1306
|
-
.provider-
|
|
1316
|
+
.provider-model-row { display: flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; }
|
|
1317
|
+
.provider-model-row.is-unavailable { padding: 4px 6px; border: 1px dashed color-mix(in srgb, var(--accent) 42%, var(--line)); border-radius: 4px; background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
1318
|
+
.provider-card .model-pill { flex: 1 1 auto; max-width: 100%; margin: 0; border: 0; text-align: left; white-space: normal; cursor: pointer; }
|
|
1319
|
+
.provider-model-row .model-pill { min-width: 0; }
|
|
1320
|
+
.provider-model-row.is-unavailable .model-pill { color: var(--muted); opacity: .72; }
|
|
1321
|
+
.provider-model-row .model-test-button { flex: 0 0 auto; min-height: 30px; padding: 6px 9px; }
|
|
1307
1322
|
.provider-card .model-pill:hover, .provider-card .model-pill:focus-visible { background: var(--surface-hover); color: var(--ink); }
|
|
1308
1323
|
.provider-card .card-actions { flex-wrap: wrap; }
|
|
1309
1324
|
.pill { display: inline-block; padding: 3px 7px; margin: 0 4px 4px 0; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); color: var(--muted); font-size: 9px; }
|
|
@@ -2038,7 +2053,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2038
2053
|
.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
2054
|
.message-heading > span { letter-spacing: .12em; }.message-heading time { flex: 0 0 auto; font-variant-numeric: tabular-nums; letter-spacing: .03em; }
|
|
2040
2055
|
.message-body { min-width: 0; overflow-wrap: anywhere; }
|
|
2041
|
-
.
|
|
2056
|
+
.assistant-message.is-error .message-body { font-family: inherit; font-size: inherit; line-height: inherit; }
|
|
2057
|
+
.assistant-message.is-error .ai-error-text { margin: 0; white-space: pre-wrap; font: inherit; }
|
|
2042
2058
|
.message-card-actions { position: absolute; right: 0; bottom: -31px; display: flex; align-items: center; gap: 12px; height: 24px; }
|
|
2043
2059
|
.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
2060
|
.message-card-actions button:hover, .message-card-actions button:focus-visible { color: var(--accent-dark); }
|
|
@@ -2087,8 +2103,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2087
2103
|
.ai-process-intermediate-step .ai-process-step-body { border-left-color: var(--muted); }
|
|
2088
2104
|
.ai-process-tool-step { padding-top: 1px; }
|
|
2089
2105
|
@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; } }
|
|
2106
|
+
@media (prefers-reduced-motion: reduce) { .is-streaming .ai-process-details[open] > summary::before { animation: none; } }
|
|
2092
2107
|
.ai-tool-call-list { display: grid; gap: 5px; margin-top: 10px; }
|
|
2093
2108
|
.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
2109
|
.ai-tool-call-summary::after { content: "查看详情"; flex: 0 0 auto; margin-left: 8px; color: var(--accent-dark); }
|
|
@@ -2141,11 +2156,33 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2141
2156
|
.ai-prompt-reference button { width: 16px; height: 16px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: inherit; font: 14px/1 var(--font-latin), monospace; }
|
|
2142
2157
|
.ai-prompt-reference button:hover, .ai-prompt-reference button:focus-visible { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
|
|
2143
2158
|
.prompt-composer-actions { position: absolute; right: 8px; bottom: 8px; display: flex; gap: 6px; align-items: center; }
|
|
2144
|
-
.ai-context-meter { --context-usage: 0; --context-meter-color: var(--green); position: relative; display: grid; flex: 0 0 32px; place-items: center; width: 32px; height: 32px; overflow: visible; border: 0; border-radius: 50%; background: conic-gradient(var(--context-meter-color) calc(var(--context-usage) * 1%), rgba(143,132,116,.2) 0); color: var(--ink); font-family: var(--font-latin); line-height: 1; }
|
|
2159
|
+
.ai-context-meter { --context-usage: 0; --context-meter-color: var(--green); position: relative; display: grid; flex: 0 0 32px; place-items: center; width: 32px; height: 32px; overflow: visible; border: 0; border-radius: 50%; background: conic-gradient(var(--context-meter-color) calc(var(--context-usage) * 1%), rgba(143,132,116,.2) 0); color: var(--ink); font-family: var(--font-latin); line-height: 1; cursor: pointer; }
|
|
2145
2160
|
.ai-context-meter::before { content: ""; position: absolute; inset: 3px; border-radius: 50%; background: var(--paper); box-shadow: inset 0 0 0 1px rgba(143,132,116,.12); }
|
|
2146
|
-
.ai-context-meter
|
|
2147
|
-
.ai-context-meter:
|
|
2161
|
+
.ai-context-meter:hover, .ai-context-meter:focus-visible, .ai-context-meter[aria-expanded="true"] { outline: 0; filter: brightness(.96); }
|
|
2162
|
+
.ai-context-meter:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
|
|
2148
2163
|
.ai-context-meter b { position: relative; z-index: 1; font-size: 9px; font-weight: 600; }.ai-context-meter.is-warning { --context-meter-color: #bf6a35; }.ai-context-meter.is-danger { --context-meter-color: #ad463c; }.ai-context-meter.is-empty { opacity: .62; }
|
|
2164
|
+
.ai-context-popover { position: absolute; right: 0; bottom: calc(100% + 10px); z-index: 40; display: grid; width: min(320px, calc(100vw - 32px)); gap: 12px; padding: 14px; border: 1px solid var(--line); border-radius: 7px; background: var(--paper); color: var(--ink); box-shadow: 0 14px 38px rgba(36, 30, 24, .18); }
|
|
2165
|
+
.ai-context-popover.hidden { display: none; }
|
|
2166
|
+
.ai-context-popover::after { position: absolute; right: 12px; bottom: -6px; width: 10px; height: 10px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--paper); content: ""; transform: rotate(45deg); }
|
|
2167
|
+
.ai-context-popover-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--line); }
|
|
2168
|
+
.ai-context-popover-header > div { display: grid; gap: 3px; min-width: 0; }
|
|
2169
|
+
.ai-context-popover-header strong { font-size: 12px; }
|
|
2170
|
+
.ai-context-popover-header small { color: var(--muted); font: 9px/1.4 var(--font-latin), var(--font-cjk), sans-serif; }
|
|
2171
|
+
.ai-context-popover-close { flex: 0 0 auto; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); font: 17px/1 var(--font-latin), sans-serif; }
|
|
2172
|
+
.ai-context-popover-close:hover, .ai-context-popover-close:focus-visible { border-color: var(--accent); color: var(--accent-dark); outline: 0; }
|
|
2173
|
+
.ai-context-distribution { display: grid; gap: 10px; }
|
|
2174
|
+
.ai-context-distribution-row { display: grid; gap: 5px; }
|
|
2175
|
+
.ai-context-distribution-label { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; font: 10px/1.3 var(--font-latin), sans-serif; }
|
|
2176
|
+
.ai-context-distribution-label > span { min-width: 0; color: var(--ink); }
|
|
2177
|
+
.ai-context-distribution-label small { color: var(--muted); font: 9px/1.3 var(--font-cjk), sans-serif; }
|
|
2178
|
+
.ai-context-distribution-label strong { flex: 0 0 auto; color: var(--muted); font: 10px/1.3 var(--font-latin), monospace; font-weight: 500; }
|
|
2179
|
+
.ai-context-distribution-track { height: 5px; overflow: hidden; border-radius: 3px; background: color-mix(in srgb, var(--muted) 14%, transparent); }
|
|
2180
|
+
.ai-context-distribution-track > span { display: block; width: calc(var(--distribution-percent) * 1%); height: 100%; min-width: 0; border-radius: inherit; background: var(--distribution-color, var(--accent)); transition: width .16s ease; }
|
|
2181
|
+
.ai-context-distribution-row[data-key="system-prompt"] { --distribution-color: var(--accent); }
|
|
2182
|
+
.ai-context-distribution-row[data-key="function"] { --distribution-color: #9a7651; }
|
|
2183
|
+
.ai-context-distribution-row[data-key="skills"] { --distribution-color: var(--muted); }
|
|
2184
|
+
.ai-context-distribution-row[data-key="context"] { --distribution-color: var(--green); }
|
|
2185
|
+
.ai-context-distribution-row[data-key="left"] { --distribution-color: color-mix(in srgb, var(--muted) 55%, transparent); }
|
|
2149
2186
|
.ai-send-button { min-width: 54px; height: 32px; padding: 0 11px; border: 0; border-radius: 4px; background: var(--accent); color: #fff; font-size: 10px; font-weight: 600; box-shadow: 0 4px 12px rgba(139,61,44,.2); }
|
|
2150
2187
|
.ai-send-button:hover { background: var(--accent-dark); }.ai-send-button:disabled { cursor: wait; opacity: .62; }
|
|
2151
2188
|
.ai-citations { display: grid; gap: 6px; max-height: 190px; overflow-y: auto; margin-bottom: 8px; }
|
|
@@ -2278,6 +2315,9 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2278
2315
|
.trace-dialog .dialog-fields { max-height: 68vh; }
|
|
2279
2316
|
.large-dialog { width: min(1120px, 94vw); max-height: 94dvh; }
|
|
2280
2317
|
.large-dialog .dialog-fields { max-height: 72dvh; padding: 24px 28px; }
|
|
2318
|
+
.editor-dialog { width: min(1180px, 94vw); max-height: calc(100dvh - 16px); }
|
|
2319
|
+
.editor-dialog .dialog-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); max-height: 76dvh; padding: 24px 28px; }
|
|
2320
|
+
.editor-dialog .dialog-fields > .form-field-note, .editor-dialog .dialog-fields > .markdown-editor-field { grid-column: 1 / -1; }
|
|
2281
2321
|
.dialog::backdrop { background: rgba(34,30,25,.42); backdrop-filter: blur(3px); }
|
|
2282
2322
|
.dialog-header { display: flex; justify-content: space-between; align-items: flex-start; padding: 22px 24px 16px; border-bottom: 1px solid var(--line); }
|
|
2283
2323
|
.dialog-header h2 { margin: 0; font-size: 23px; font-weight: 500; }
|
|
@@ -2555,6 +2595,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2555
2595
|
.markdown-editor-field { display: grid; gap: 10px; min-width: 0; }
|
|
2556
2596
|
.markdown-editor-field > span { color: var(--muted); font-size: 11px; }
|
|
2557
2597
|
.markdown-editor-field .vditor-editor-host { min-height: 360px; }
|
|
2598
|
+
.editor-dialog .markdown-editor-field .vditor-editor-host.vditor { min-height: clamp(420px, 56dvh, 640px) !important; }
|
|
2558
2599
|
.vditor-editor-host { width: 100%; height: 100%; min-width: 0; min-height: 0; }
|
|
2559
2600
|
.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
2601
|
.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 +2654,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2613
2654
|
.character-editor-section-fields textarea { min-height: 120px; resize: vertical; line-height: 1.65; }
|
|
2614
2655
|
.character-editor-section-fields .section-list-row textarea { min-height: 180px; }
|
|
2615
2656
|
.character-editor-section-fields .member-chip { position: relative; display: inline-flex; width: auto; cursor: pointer; }
|
|
2657
|
+
.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
2658
|
.character-editor-section-fields .item-list-row button, .character-editor-section-fields .structured-list-row button { min-width: 54px; }
|
|
2617
2659
|
.knowledge-markdown-sections { display: grid; grid-column: 1 / -1; gap: 14px; min-width: 0; }
|
|
2618
2660
|
.knowledge-markdown-list-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
@@ -2814,6 +2856,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2814
2856
|
.setting-markdown-field { height: 70dvh; }
|
|
2815
2857
|
.setting-markdown-heading, .markdown-editor-field-heading { align-items: flex-start; flex-direction: column; gap: 4px; }
|
|
2816
2858
|
.setting-markdown-field > .vditor-editor-host, .markdown-editor-field .vditor-editor-host { min-height: 42dvh; }
|
|
2859
|
+
.editor-dialog .dialog-fields { grid-template-columns: minmax(0, 1fr); max-height: 76dvh; }
|
|
2860
|
+
.editor-dialog .markdown-editor-field .vditor-editor-host.vditor { min-height: 52dvh !important; }
|
|
2817
2861
|
.character-editor-workspace, .character-editor-workspace.history-open { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
|
|
2818
2862
|
.character-editor-nav { flex-direction: row; overflow-x: auto; padding: 8px 10px; border-right: 0; border-bottom: 1px solid var(--line); }
|
|
2819
2863
|
.character-editor-nav button { flex: 0 0 130px; }
|
|
@@ -2965,7 +3009,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2965
3009
|
.app-shell.shelf-mode .main-panel { grid-column: 1; grid-row: 2; min-height: 0; }
|
|
2966
3010
|
.ai-panel {
|
|
2967
3011
|
position: fixed;
|
|
2968
|
-
z-index:
|
|
3012
|
+
z-index: 35;
|
|
2969
3013
|
right: 0;
|
|
2970
3014
|
bottom: 0;
|
|
2971
3015
|
left: 0;
|
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,
|
|
@@ -122,6 +123,10 @@ export const versionedEntityTypes = [
|
|
|
122
123
|
"chapter-outline",
|
|
123
124
|
"foreshadow"
|
|
124
125
|
];
|
|
126
|
+
export function defaultAiConversationTitle(prompt) {
|
|
127
|
+
const normalized = prompt.replace(/\s+/gu, " ").trim();
|
|
128
|
+
return Array.from(normalized).slice(0, 15).join("") || "新对话";
|
|
129
|
+
}
|
|
125
130
|
function isRecord(value) {
|
|
126
131
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
127
132
|
}
|
|
@@ -688,6 +693,9 @@ export class Store {
|
|
|
688
693
|
agentTools: json(String(row?.agent_tools_json ?? '["story_index","read_chapters","search_story_entities","grep","read_character_sections","search_drafts"]'), ["story_index", "read_chapters", "search_story_entities", "grep", "read_character_sections", "search_drafts"])
|
|
689
694
|
.map((tool) => tool === "query_story_knowledge" ? "search_story_entities" : tool)
|
|
690
695
|
.filter((tool, index, tools) => tools.indexOf(tool) === index),
|
|
696
|
+
titleGenerationModelId: row?.title_generation_model_id === null || row?.title_generation_model_id === undefined
|
|
697
|
+
? null
|
|
698
|
+
: String(row.title_generation_model_id),
|
|
691
699
|
updatedAt: String(row?.updated_at ?? "")
|
|
692
700
|
};
|
|
693
701
|
}
|
|
@@ -704,12 +712,15 @@ export class Store {
|
|
|
704
712
|
const nextBookSummaryContextPercent = input.bookSummaryContextPercent ?? Number(current.bookSummaryContextPercent);
|
|
705
713
|
const nextContextCompactThreshold = input.contextCompactThreshold ?? Number(current.contextCompactThreshold);
|
|
706
714
|
const nextAgentTools = input.agentTools ?? current.agentTools;
|
|
715
|
+
const nextTitleGenerationModelId = input.titleGenerationModelId === undefined
|
|
716
|
+
? (current.titleGenerationModelId ? String(current.titleGenerationModelId) : null)
|
|
717
|
+
: input.titleGenerationModelId?.trim() || null;
|
|
707
718
|
this.db.run(`INSERT INTO work_ai_settings (
|
|
708
719
|
work_id, system_prompt, auto_run_enabled, auto_run_concurrency, auto_run_batch_limit,
|
|
709
720
|
auto_run_daily_task_limit, auto_run_failure_threshold, auto_run_paused, auto_run_pause_reason,
|
|
710
721
|
auto_run_resume_at, auto_run_consecutive_failures, book_summary_context_percent,
|
|
711
|
-
context_compact_threshold, agent_tools_json, updated_at
|
|
712
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
722
|
+
context_compact_threshold, agent_tools_json, title_generation_model_id, updated_at
|
|
723
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
713
724
|
ON CONFLICT(work_id) DO UPDATE SET
|
|
714
725
|
system_prompt = excluded.system_prompt,
|
|
715
726
|
auto_run_enabled = excluded.auto_run_enabled,
|
|
@@ -724,7 +735,8 @@ export class Store {
|
|
|
724
735
|
book_summary_context_percent = excluded.book_summary_context_percent,
|
|
725
736
|
context_compact_threshold = excluded.context_compact_threshold,
|
|
726
737
|
agent_tools_json = excluded.agent_tools_json,
|
|
727
|
-
|
|
738
|
+
title_generation_model_id = excluded.title_generation_model_id,
|
|
739
|
+
updated_at = excluded.updated_at`, workId, nextPrompt, nextEnabled ? 1 : 0, Math.min(8, Math.max(1, nextConcurrency)), Math.min(200, Math.max(1, nextBatchLimit)), Math.min(10_000, Math.max(0, nextDailyTaskLimit)), Math.min(10, Math.max(1, nextFailureThreshold)), current.autoRunPaused ? 1 : 0, String(current.autoRunPauseReason ?? ""), current.autoRunResumeAt === null ? null : String(current.autoRunResumeAt), Math.max(0, Number(current.autoRunConsecutiveFailures) || 0), Math.min(90, Math.max(1, nextBookSummaryContextPercent)), Math.min(90, Math.max(50, nextContextCompactThreshold)), JSON.stringify(nextAgentTools), nextTitleGenerationModelId, timestamp);
|
|
728
740
|
this.audit(workId, "work.ai-settings.updated", "work-ai-settings", workId, {
|
|
729
741
|
systemPromptChanged: input.systemPrompt !== undefined,
|
|
730
742
|
autoRunEnabled: nextEnabled,
|
|
@@ -734,7 +746,8 @@ export class Store {
|
|
|
734
746
|
autoRunFailureThreshold: Math.min(10, Math.max(1, nextFailureThreshold)),
|
|
735
747
|
bookSummaryContextPercent: Math.min(90, Math.max(1, nextBookSummaryContextPercent)),
|
|
736
748
|
contextCompactThreshold: Math.min(90, Math.max(50, nextContextCompactThreshold)),
|
|
737
|
-
agentTools: nextAgentTools
|
|
749
|
+
agentTools: nextAgentTools,
|
|
750
|
+
titleGenerationModelId: nextTitleGenerationModelId
|
|
738
751
|
});
|
|
739
752
|
return this.getWorkAiSettings(workId);
|
|
740
753
|
}
|
|
@@ -891,6 +904,41 @@ export class Store {
|
|
|
891
904
|
}));
|
|
892
905
|
return { ...work, volumes };
|
|
893
906
|
}
|
|
907
|
+
getWorkVolumeDirectory(workId) {
|
|
908
|
+
const work = this.getWork(workId);
|
|
909
|
+
const permissions = work.modulePermissions;
|
|
910
|
+
if (permissions.prose === "none")
|
|
911
|
+
return { ...work, volumes: [] };
|
|
912
|
+
const volumeRows = this.db.all(`SELECT volume.*,
|
|
913
|
+
(SELECT COUNT(*) FROM chapters chapter WHERE chapter.volume_id = volume.id AND chapter.deleted_at IS NULL) AS chapter_count
|
|
914
|
+
FROM volumes volume WHERE volume.work_id = ? ORDER BY volume.sort_order, volume.created_at`, workId);
|
|
915
|
+
const volumes = volumeRows.map((row) => ({
|
|
916
|
+
...this.mapVolume(row),
|
|
917
|
+
chapterCount: numberValue(row, "chapter_count"),
|
|
918
|
+
chapters: []
|
|
919
|
+
}));
|
|
920
|
+
return { ...work, volumes };
|
|
921
|
+
}
|
|
922
|
+
listVolumeChapters(volumeId) {
|
|
923
|
+
const volume = this.getVolume(volumeId);
|
|
924
|
+
const work = this.getWork(String(volume.workId));
|
|
925
|
+
if (work.modulePermissions.prose === "none")
|
|
926
|
+
return [];
|
|
927
|
+
return this.db.all(`SELECT id, work_id, volume_id, title, chapter_type, sort_order, word_count, version_no,
|
|
928
|
+
analysis_status, excluded_from_analysis, created_at, updated_at
|
|
929
|
+
FROM chapters WHERE volume_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at`, volumeId).map((row) => this.mapChapterDirectoryEntry(row));
|
|
930
|
+
}
|
|
931
|
+
listVolumeChaptersPage(volumeId, pagination) {
|
|
932
|
+
const volume = this.getVolume(volumeId);
|
|
933
|
+
const work = this.getWork(String(volume.workId));
|
|
934
|
+
if (work.modulePermissions.prose === "none")
|
|
935
|
+
return paginated([], pagination);
|
|
936
|
+
const page = paginationSql(pagination);
|
|
937
|
+
const rows = this.db.all(`SELECT id, work_id, volume_id, title, chapter_type, sort_order, word_count, version_no,
|
|
938
|
+
analysis_status, excluded_from_analysis, created_at, updated_at
|
|
939
|
+
FROM chapters WHERE volume_id = ? AND deleted_at IS NULL ORDER BY sort_order, created_at${page.sql}`, volumeId, ...page.params);
|
|
940
|
+
return paginated(rows.map((row) => this.mapChapterDirectoryEntry(row)), pagination);
|
|
941
|
+
}
|
|
894
942
|
getWorkDirectoryPage(workId, pagination) {
|
|
895
943
|
const work = this.getWork(workId);
|
|
896
944
|
const permissions = work.modulePermissions;
|
|
@@ -4091,7 +4139,7 @@ export class Store {
|
|
|
4091
4139
|
throw notFound("AI 对话");
|
|
4092
4140
|
if (requiredString(conversation, "work_id") !== workId)
|
|
4093
4141
|
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
4094
|
-
const rows = this.db.all("SELECT id, role, content FROM ai_conversation_messages WHERE conversation_id = ? ORDER BY created_at, rowid", conversationId);
|
|
4142
|
+
const rows = this.db.all("SELECT id, role, content, metadata_json FROM ai_conversation_messages WHERE conversation_id = ? ORDER BY created_at, rowid", conversationId);
|
|
4095
4143
|
const compactedMessageCount = Math.min(rows.length, Math.max(0, numberValue(conversation, "compacted_message_count")));
|
|
4096
4144
|
return {
|
|
4097
4145
|
workId,
|
|
@@ -4104,10 +4152,23 @@ export class Store {
|
|
|
4104
4152
|
.map((message) => ({
|
|
4105
4153
|
id: requiredString(message, "id"),
|
|
4106
4154
|
role: requiredString(message, "role") === "assistant" ? "assistant" : "user",
|
|
4107
|
-
content: requiredString(message, "content")
|
|
4155
|
+
content: requiredString(message, "content"),
|
|
4156
|
+
metadata: json(requiredString(message, "metadata_json"), {})
|
|
4108
4157
|
}))
|
|
4109
4158
|
};
|
|
4110
4159
|
}
|
|
4160
|
+
getAiConversationTitleContext(conversationId, workId) {
|
|
4161
|
+
const conversation = this.db.get("SELECT title, work_id FROM ai_conversations WHERE id = ?", conversationId);
|
|
4162
|
+
if (!conversation)
|
|
4163
|
+
throw notFound("AI 对话");
|
|
4164
|
+
if (requiredString(conversation, "work_id") !== workId)
|
|
4165
|
+
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
4166
|
+
const messages = this.db.all("SELECT role, content FROM ai_conversation_messages WHERE conversation_id = ? ORDER BY created_at, rowid", conversationId).map((message) => ({
|
|
4167
|
+
role: requiredString(message, "role") === "assistant" ? "assistant" : "user",
|
|
4168
|
+
content: requiredString(message, "content")
|
|
4169
|
+
}));
|
|
4170
|
+
return { title: requiredString(conversation, "title"), messages };
|
|
4171
|
+
}
|
|
4111
4172
|
setAiConversationContextWarning(conversationId, pending) {
|
|
4112
4173
|
const conversation = this.db.get("SELECT id FROM ai_conversations WHERE id = ?", conversationId);
|
|
4113
4174
|
if (!conversation)
|
|
@@ -4121,20 +4182,38 @@ export class Store {
|
|
|
4121
4182
|
this.db.run("UPDATE ai_conversations SET compacted_summary = ?, compacted_message_count = ?, context_warning_at = NULL, updated_at = ? WHERE id = ?", summary, Math.max(0, compactedMessageCount), now(), conversationId);
|
|
4122
4183
|
return this.getAiConversation(conversationId);
|
|
4123
4184
|
}
|
|
4185
|
+
setAiConversationTitle(conversationId, title) {
|
|
4186
|
+
const conversation = this.db.get("SELECT id FROM ai_conversations WHERE id = ?", conversationId);
|
|
4187
|
+
if (!conversation)
|
|
4188
|
+
throw notFound("AI 对话");
|
|
4189
|
+
const normalizedTitle = title.replace(/\s+/gu, " ").trim().slice(0, 200) || "新对话";
|
|
4190
|
+
this.db.run("UPDATE ai_conversations SET title = ?, updated_at = ? WHERE id = ?", normalizedTitle, now(), conversationId);
|
|
4191
|
+
return this.getAiConversation(conversationId);
|
|
4192
|
+
}
|
|
4124
4193
|
addAiConversationMessage(conversationId, input) {
|
|
4125
4194
|
const conversation = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
|
|
4126
4195
|
if (!conversation)
|
|
4127
4196
|
throw notFound("AI 对话");
|
|
4197
|
+
const requestId = input.requestId?.trim() || null;
|
|
4198
|
+
if (requestId) {
|
|
4199
|
+
const existing = this.db.get("SELECT * FROM ai_conversation_messages WHERE conversation_id = ? AND request_id = ?", conversationId, requestId);
|
|
4200
|
+
if (existing)
|
|
4201
|
+
return this.mapAiConversationMessage(existing);
|
|
4202
|
+
}
|
|
4128
4203
|
const messageId = id("message");
|
|
4129
4204
|
const timestamp = now();
|
|
4130
4205
|
const title = requiredString(conversation, "title") === "新对话" && input.role === "user"
|
|
4131
|
-
? input.content
|
|
4206
|
+
? defaultAiConversationTitle(input.content)
|
|
4132
4207
|
: requiredString(conversation, "title");
|
|
4133
4208
|
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.
|
|
4209
|
+
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);
|
|
4210
|
+
const inserted = this.db.get("SELECT id FROM ai_conversation_messages WHERE id = ?", messageId);
|
|
4211
|
+
if (inserted)
|
|
4212
|
+
this.db.run("UPDATE ai_conversations SET title = ?, updated_at = ? WHERE id = ?", title, timestamp, conversationId);
|
|
4136
4213
|
});
|
|
4137
|
-
const message =
|
|
4214
|
+
const message = requestId
|
|
4215
|
+
? this.db.get("SELECT * FROM ai_conversation_messages WHERE conversation_id = ? AND request_id = ?", conversationId, requestId)
|
|
4216
|
+
: this.db.get("SELECT * FROM ai_conversation_messages WHERE id = ?", messageId);
|
|
4138
4217
|
if (!message)
|
|
4139
4218
|
throw notFound("AI 对话消息");
|
|
4140
4219
|
return this.mapAiConversationMessage(message);
|
|
@@ -4157,7 +4236,7 @@ export class Store {
|
|
|
4157
4236
|
this.db.transaction(() => {
|
|
4158
4237
|
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
4238
|
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);
|
|
4239
|
+
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
4240
|
}
|
|
4162
4241
|
});
|
|
4163
4242
|
return this.getAiConversation(forkId);
|
|
@@ -4184,6 +4263,7 @@ export class Store {
|
|
|
4184
4263
|
content: requiredString(row, "content"),
|
|
4185
4264
|
citations: json(requiredString(row, "citations_json"), []),
|
|
4186
4265
|
metadata: json(requiredString(row, "metadata_json"), {}),
|
|
4266
|
+
requestId: optionalString(row, "request_id"),
|
|
4187
4267
|
createdAt: requiredString(row, "created_at")
|
|
4188
4268
|
};
|
|
4189
4269
|
}
|
|
@@ -5615,18 +5695,14 @@ export class Store {
|
|
|
5615
5695
|
const goal = this.db.get("SELECT * FROM writing_goals WHERE work_id = ?", workId);
|
|
5616
5696
|
const dailyGoal = goal ? numberValue(goal, "daily_goal") : 1000;
|
|
5617
5697
|
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);
|
|
5698
|
+
const calendar = buildWritingCalendar(new Date(), days);
|
|
5623
5699
|
const versions = this.db.all(`SELECT chapter_id, content, source, created_at FROM chapter_versions
|
|
5624
|
-
WHERE work_id = ? AND created_at
|
|
5700
|
+
WHERE work_id = ? AND created_at < ? ORDER BY created_at, version_no, id`, workId, calendar.endExclusive);
|
|
5625
5701
|
const chapterWords = new Map();
|
|
5626
5702
|
const events = new Map();
|
|
5627
5703
|
for (const version of versions) {
|
|
5628
|
-
const day = requiredString(version, "created_at")
|
|
5629
|
-
if (day < startKey) {
|
|
5704
|
+
const day = writingDateKey(new Date(requiredString(version, "created_at")), calendar.timeZone);
|
|
5705
|
+
if (day < calendar.startKey) {
|
|
5630
5706
|
chapterWords.set(requiredString(version, "chapter_id"), requiredString(version, "source") === "delete" ? 0 : countWords(requiredString(version, "content")));
|
|
5631
5707
|
}
|
|
5632
5708
|
else {
|
|
@@ -5637,10 +5713,7 @@ export class Store {
|
|
|
5637
5713
|
}
|
|
5638
5714
|
let previousTotal = [...chapterWords.values()].reduce((sum, value) => sum + value, 0);
|
|
5639
5715
|
const trend = [];
|
|
5640
|
-
for (
|
|
5641
|
-
const date = new Date(start);
|
|
5642
|
-
date.setUTCDate(start.getUTCDate() + index);
|
|
5643
|
-
const day = date.toISOString().slice(0, 10);
|
|
5716
|
+
for (const day of calendar.dateKeys) {
|
|
5644
5717
|
for (const version of events.get(day) ?? []) {
|
|
5645
5718
|
chapterWords.set(requiredString(version, "chapter_id"), requiredString(version, "source") === "delete" ? 0 : countWords(requiredString(version, "content")));
|
|
5646
5719
|
}
|