@musnows/scriverse 0.6.3 → 0.6.5
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 +22 -9
- package/dist/ai-protocol.js.map +1 -1
- package/dist/ai.js +758 -164
- package/dist/ai.js.map +1 -1
- package/dist/app.js +95 -14
- package/dist/app.js.map +1 -1
- package/dist/database.js +146 -2
- package/dist/database.js.map +1 -1
- package/dist/google-vertex-auth.js +155 -0
- package/dist/google-vertex-auth.js.map +1 -0
- package/dist/public/ai-mentions.js +15 -1
- package/dist/public/app.js +339 -45
- package/dist/public/display-labels.js +2 -1
- package/dist/public/index.html +27 -3
- package/dist/public/styles.css +59 -2
- package/dist/store.js +189 -7
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +21 -2
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/writing-progress-time.js +8 -0
- package/dist/writing-progress-time.js.map +1 -1
- package/package.json +1 -1
|
@@ -68,7 +68,8 @@ export function providerConnectionLabel(value) {
|
|
|
68
68
|
export function providerProtocolLabel(value) {
|
|
69
69
|
return enumLabel({
|
|
70
70
|
"openai-chat-completions": "OpenAI Chat Completions",
|
|
71
|
-
"anthropic-messages": "Anthropic Messages"
|
|
71
|
+
"anthropic-messages": "Anthropic Messages",
|
|
72
|
+
"google-vertex": "Google Vertex"
|
|
72
73
|
}, value, "未知接口协议");
|
|
73
74
|
}
|
|
74
75
|
|
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=20260801-
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260801-ai-context-mention-v1">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -306,6 +306,7 @@
|
|
|
306
306
|
<div class="prompt-options">
|
|
307
307
|
<select id="ai-task" aria-label="任务类型">
|
|
308
308
|
<option value="chat">问答</option>
|
|
309
|
+
<option value="roleplay">角色扮演</option>
|
|
309
310
|
<option value="continue">续写</option>
|
|
310
311
|
<option value="polish">润色选中文本</option>
|
|
311
312
|
</select>
|
|
@@ -315,6 +316,10 @@
|
|
|
315
316
|
<option value="chapter-summary">当前章节 + 全书概要</option>
|
|
316
317
|
<option value="volume">当前卷</option>
|
|
317
318
|
<option value="book">全书</option>
|
|
319
|
+
<option value="settings-catalog">设定库</option>
|
|
320
|
+
</select>
|
|
321
|
+
<select id="ai-roleplay-character" class="ai-roleplay-character hidden" aria-label="角色扮演角色卡">
|
|
322
|
+
<option value="">选择角色卡</option>
|
|
318
323
|
</select>
|
|
319
324
|
</div>
|
|
320
325
|
<div class="field-label prompt-model-field">
|
|
@@ -322,7 +327,7 @@
|
|
|
322
327
|
</div>
|
|
323
328
|
<div class="prompt-composer">
|
|
324
329
|
<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>
|
|
325
|
-
<div id="ai-mention-menu" class="ai-mention-menu hidden" role="listbox" aria-label="
|
|
330
|
+
<div id="ai-mention-menu" class="ai-mention-menu hidden" role="listbox" aria-label="引用角色、设定、章节或上下文能力"></div>
|
|
326
331
|
<div class="prompt-composer-actions">
|
|
327
332
|
<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>
|
|
328
333
|
<section id="ai-context-popover" class="ai-context-popover hidden" role="dialog" aria-labelledby="ai-context-popover-title" aria-describedby="ai-context-popover-description">
|
|
@@ -657,6 +662,24 @@
|
|
|
657
662
|
<option value="consolas">Consolas</option>
|
|
658
663
|
</select>
|
|
659
664
|
</label>
|
|
665
|
+
<label>界面字号
|
|
666
|
+
<select id="appearance-ui-font-size" name="uiFontSize">
|
|
667
|
+
<option value="14">14 px(小)</option>
|
|
668
|
+
<option value="15">15 px</option>
|
|
669
|
+
<option value="16">16 px(标准)</option>
|
|
670
|
+
<option value="17">17 px</option>
|
|
671
|
+
<option value="18">18 px(大)</option>
|
|
672
|
+
</select>
|
|
673
|
+
</label>
|
|
674
|
+
<label>Agent 对话字号
|
|
675
|
+
<select id="appearance-ai-font-size" name="aiFontSize">
|
|
676
|
+
<option value="12">12 px(小)</option>
|
|
677
|
+
<option value="13">13 px</option>
|
|
678
|
+
<option value="14">14 px(标准)</option>
|
|
679
|
+
<option value="15">15 px</option>
|
|
680
|
+
<option value="16">16 px(大)</option>
|
|
681
|
+
</select>
|
|
682
|
+
</label>
|
|
660
683
|
<label>正文字号
|
|
661
684
|
<select id="appearance-font-size" name="fontSize">
|
|
662
685
|
<option value="15">15 px</option>
|
|
@@ -681,6 +704,7 @@
|
|
|
681
704
|
<strong>字体预览 Typography 0123</strong>
|
|
682
705
|
<span>中文默认使用黑体,English letters always use monospace.</span>
|
|
683
706
|
<span>第二段用于预览正文的行距与段落密度。</span>
|
|
707
|
+
<span id="font-size-preview">界面 16 px · Agent 对话 14 px</span>
|
|
684
708
|
</div>
|
|
685
709
|
</div>
|
|
686
710
|
<div class="dialog-actions">
|
|
@@ -927,6 +951,6 @@
|
|
|
927
951
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
928
952
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
929
953
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
930
|
-
<script type="module" src="/app.js?v=20260801-
|
|
954
|
+
<script type="module" src="/app.js?v=20260801-global-setting-context-v1"></script>
|
|
931
955
|
</body>
|
|
932
956
|
</html>
|
package/dist/public/styles.css
CHANGED
|
@@ -55,11 +55,16 @@
|
|
|
55
55
|
--relationship-detail-muted: #67758c;
|
|
56
56
|
--font-latin: "SFMono-Regular", "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono";
|
|
57
57
|
--font-cjk: "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", "Heiti SC";
|
|
58
|
+
--ui-font-size: 16px;
|
|
59
|
+
--ui-font-scale: 1;
|
|
60
|
+
--ai-font-size: 14px;
|
|
61
|
+
--ai-font-scale: 1;
|
|
58
62
|
--editor-font-size: 17px;
|
|
59
63
|
--editor-line-height: 1.55;
|
|
60
64
|
--left-panel-width: 280px;
|
|
61
65
|
--ai-panel-width: 360px;
|
|
62
66
|
font-family: var(--font-latin), var(--font-cjk), monospace, sans-serif;
|
|
67
|
+
font-size: var(--ui-font-size);
|
|
63
68
|
line-height: 1.5;
|
|
64
69
|
color: var(--ink);
|
|
65
70
|
background: var(--paper);
|
|
@@ -510,7 +515,7 @@ input[type="checkbox"][hidden] { display: none; }
|
|
|
510
515
|
|
|
511
516
|
.left-panel, .ai-panel { position: relative; min-height: 0; background: var(--panel); }
|
|
512
517
|
.left-panel { display: flex; flex-direction: column; border-right: 1px solid var(--line); padding: 0; overflow: hidden; }
|
|
513
|
-
.left-panel-body { flex: 1 1 auto; min-height: 0; padding: 18px
|
|
518
|
+
.left-panel-body { flex: 1 1 auto; min-height: 0; padding: 18px 14px 16px; overflow-y: auto; }
|
|
514
519
|
.ai-panel { border-left: 1px solid var(--line); display: flex; flex-direction: column; padding: 14px 16px 18px; min-width: 0; }
|
|
515
520
|
.panel-collapse-button { z-index: 8; flex: 0 0 auto; width: 24px; height: 24px; padding: 0; border: 1px solid var(--line); border-radius: 4px; background: var(--panel); color: var(--muted); font-size: 18px; line-height: 1; }
|
|
516
521
|
.ai-heading #ai-panel-toggle { flex-basis: 30px; width: 30px; height: 30px; }
|
|
@@ -2205,6 +2210,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2205
2210
|
.ai-context-warning button:first-child:hover, .ai-context-warning button:first-child:focus-visible { background: var(--accent-dark); color: #fff; }
|
|
2206
2211
|
.prompt-options { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 7px; }
|
|
2207
2212
|
.prompt-options select { padding: 6px; color: var(--muted); font-size: 10px; }
|
|
2213
|
+
.prompt-options .ai-roleplay-character { min-width: 0; }
|
|
2214
|
+
.ai-panel.is-roleplaying .ai-roleplay-character { border-color: color-mix(in srgb, var(--accent) 52%, var(--line)); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); color: var(--accent-dark); }
|
|
2208
2215
|
.prompt-model-field { margin: 0 0 7px; }
|
|
2209
2216
|
.prompt-model-field select { padding: 6px; color: var(--muted); font-size: 10px; }
|
|
2210
2217
|
.prompt-composer { position: relative; }
|
|
@@ -2894,7 +2901,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
2894
2901
|
}
|
|
2895
2902
|
|
|
2896
2903
|
@media (max-width: 1100px) {
|
|
2897
|
-
.left-panel-body { padding-inline: 10px
|
|
2904
|
+
.left-panel-body { padding-inline: 10px; }
|
|
2898
2905
|
.ai-panel { padding-inline: 14px; }
|
|
2899
2906
|
.editor-toolbar { grid-template-areas: "path" "title" "actions"; grid-template-columns: minmax(0, 1fr); gap: 12px; padding-inline: 6%; }
|
|
2900
2907
|
.editor-actions { justify-content: flex-end; flex-wrap: wrap; }
|
|
@@ -3235,3 +3242,53 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
3235
3242
|
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
|
|
3236
3243
|
.dialog-submit-progress > span { width: 100%; transform: none; }
|
|
3237
3244
|
}
|
|
3245
|
+
|
|
3246
|
+
/* 显示设置中的界面字号覆盖常用控件和辅助文字;正文与 Agent 对话分别独立调节。 */
|
|
3247
|
+
body { font-size: var(--ui-font-size); }
|
|
3248
|
+
:is(.module-nav button, .ghost-button, .primary-button, .module-header p, .eyebrow,
|
|
3249
|
+
.panel-heading, .field-label, .dialog-header-meta, .dialog-fields label,
|
|
3250
|
+
.dialog-fields .form-field-note, .appearance-grid label, .config-section-header p,
|
|
3251
|
+
.config-section .config-save-button, .font-preview span, .settings-hub-card small,
|
|
3252
|
+
.settings-hub-card strong, .shelf-header p, .topbar, .book-card small) {
|
|
3253
|
+
font-size: calc(1em * var(--ui-font-scale));
|
|
3254
|
+
}
|
|
3255
|
+
.appearance-grid select, .dialog-fields input, .dialog-fields textarea, .dialog-fields select { font-size: calc(13px * var(--ui-font-scale)); }
|
|
3256
|
+
|
|
3257
|
+
/* Agent 面板原有的细小字号统一以可读的对话字号为基准缩放。 */
|
|
3258
|
+
.ai-panel,
|
|
3259
|
+
.ai-history-dialog,
|
|
3260
|
+
.ai-tool-call-dialog { font-size: var(--ai-font-size); }
|
|
3261
|
+
.ai-heading-title strong { font-size: calc(14px * var(--ai-font-scale)); }
|
|
3262
|
+
.ai-heading-actions > span, .ai-heading-actions button { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3263
|
+
.quick-actions button { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3264
|
+
.ai-context-compaction-divider { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3265
|
+
.assistant-message, .user-message { font-size: var(--ai-font-size); }
|
|
3266
|
+
.assistant-message > .message-heading, .user-message > .message-heading { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3267
|
+
.ai-message-status, .message-card-actions button, .message-meta { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3268
|
+
.message-body h1 { font-size: calc(17px * var(--ai-font-scale)); }
|
|
3269
|
+
.message-body h2 { font-size: calc(15px * var(--ai-font-scale)); }
|
|
3270
|
+
.message-body h3, .message-body h4 { font-size: calc(13px * var(--ai-font-scale)); }
|
|
3271
|
+
.message-body h5, .message-body h6 { font-size: calc(11px * var(--ai-font-scale)); }
|
|
3272
|
+
.message-body pre { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3273
|
+
.message-body .markdown-image small { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3274
|
+
.ai-process-details > summary, .ai-process-step-body, .ai-tool-call-summary { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3275
|
+
.ai-process-details > summary > small, .ai-process-step > small { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3276
|
+
.guard-card strong, .guard-card p, .message-actions button { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3277
|
+
.ai-context-warning strong { font-size: calc(11px * var(--ai-font-scale)); }
|
|
3278
|
+
.ai-context-warning p, .ai-context-warning button { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3279
|
+
.prompt-options select, .prompt-model-field select, .ai-mention-option strong, .ai-mention-empty { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3280
|
+
.ai-mention-option small { font-size: calc(8px * var(--ai-font-scale)); }
|
|
3281
|
+
.ai-prompt { font-size: var(--ai-font-size); }
|
|
3282
|
+
.ai-prompt-reference { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3283
|
+
.ai-context-meter b { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3284
|
+
.ai-context-popover-header strong { font-size: calc(12px * var(--ai-font-scale)); }
|
|
3285
|
+
.ai-context-popover-header small, .ai-context-distribution-label { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3286
|
+
.ai-context-distribution-label small, .ai-context-distribution-label strong { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3287
|
+
.ai-send-button { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3288
|
+
.ai-citation-main strong { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3289
|
+
.ai-citation-main small, .ai-citation-main span, .message-citations span { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3290
|
+
.ai-history-item strong { font-size: calc(14px * var(--ai-font-scale)); }
|
|
3291
|
+
.ai-history-item small { font-size: calc(10px * var(--ai-font-scale)); }
|
|
3292
|
+
.ai-tool-call-info dt, .ai-tool-call-detail h3 { font-size: calc(9px * var(--ai-font-scale)); }
|
|
3293
|
+
.ai-tool-call-info dd { font-size: calc(11px * var(--ai-font-scale)); }
|
|
3294
|
+
.ai-tool-call-detail pre { font-size: calc(10px * var(--ai-font-scale)); }
|
package/dist/store.js
CHANGED
|
@@ -153,6 +153,7 @@ export const versionedEntityTypes = [
|
|
|
153
153
|
"chapter-outline",
|
|
154
154
|
"foreshadow"
|
|
155
155
|
];
|
|
156
|
+
export const aiConversationTaskTypes = ["chat", "roleplay", "continue", "polish"];
|
|
156
157
|
export function defaultAiConversationTitle(prompt) {
|
|
157
158
|
const normalized = prompt.replace(/\s+/gu, " ").trim();
|
|
158
159
|
return Array.from(normalized).slice(0, 15).join("") || "新对话";
|
|
@@ -231,6 +232,29 @@ function booleanValue(row, key) {
|
|
|
231
232
|
export function normalizeCharacterName(value) {
|
|
232
233
|
return value.normalize("NFKC").trim().replace(/\s+/gu, " ").toLocaleLowerCase("zh-CN");
|
|
233
234
|
}
|
|
235
|
+
const EMPTY_AI_INJECTED_ENTITIES = {
|
|
236
|
+
characters: [],
|
|
237
|
+
races: [],
|
|
238
|
+
organizations: []
|
|
239
|
+
};
|
|
240
|
+
function parseAiInjectedEntities(value) {
|
|
241
|
+
const parsed = typeof value === "string" ? json(value, {}) : isRecord(value) ? value : {};
|
|
242
|
+
const uniqueIds = (items) => [...new Set((Array.isArray(items) ? items : [])
|
|
243
|
+
.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
244
|
+
.map((item) => item.trim()))];
|
|
245
|
+
return {
|
|
246
|
+
characters: uniqueIds(parsed.characters),
|
|
247
|
+
races: uniqueIds(parsed.races),
|
|
248
|
+
organizations: uniqueIds(parsed.organizations)
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function mergeAiInjectedEntities(base, extra) {
|
|
252
|
+
return {
|
|
253
|
+
characters: [...new Set([...base.characters, ...(extra.characters ?? [])])],
|
|
254
|
+
races: [...new Set([...base.races, ...(extra.races ?? [])])],
|
|
255
|
+
organizations: [...new Set([...base.organizations, ...(extra.organizations ?? [])])]
|
|
256
|
+
};
|
|
257
|
+
}
|
|
234
258
|
export class Store {
|
|
235
259
|
db;
|
|
236
260
|
constructor(db) {
|
|
@@ -728,6 +752,7 @@ export class Store {
|
|
|
728
752
|
agentToolCallLimit: Math.min(48, Math.max(5, Number(row?.agent_tool_call_limit ?? 12) || 12)),
|
|
729
753
|
agentToolCallGlobalMultiplier: Math.min(6, Math.max(1, Number(row?.agent_tool_call_global_multiplier ?? 3) || 3)),
|
|
730
754
|
agentTools: normalizeWorkAgentTools(row?.agent_tools_json),
|
|
755
|
+
alwaysIncludeSettingInfo: Number(row?.always_include_setting_info ?? 0) === 1,
|
|
731
756
|
titleGenerationModelId: row?.title_generation_model_id === null || row?.title_generation_model_id === undefined
|
|
732
757
|
? null
|
|
733
758
|
: String(row.title_generation_model_id),
|
|
@@ -752,6 +777,7 @@ export class Store {
|
|
|
752
777
|
const nextAgentToolCallLimit = input.agentToolCallLimit ?? Number(current.agentToolCallLimit);
|
|
753
778
|
const nextAgentToolCallGlobalMultiplier = input.agentToolCallGlobalMultiplier ?? Number(current.agentToolCallGlobalMultiplier);
|
|
754
779
|
const nextAgentTools = normalizeWorkAgentTools(input.agentTools ?? current.agentTools);
|
|
780
|
+
const nextAlwaysIncludeSettingInfo = input.alwaysIncludeSettingInfo ?? Boolean(current.alwaysIncludeSettingInfo);
|
|
755
781
|
const nextTitleGenerationModelId = input.titleGenerationModelId === undefined
|
|
756
782
|
? (current.titleGenerationModelId ? String(current.titleGenerationModelId) : null)
|
|
757
783
|
: input.titleGenerationModelId?.trim() || null;
|
|
@@ -760,8 +786,8 @@ export class Store {
|
|
|
760
786
|
auto_run_daily_task_limit, auto_run_failure_threshold, auto_run_paused, auto_run_pause_reason,
|
|
761
787
|
auto_run_resume_at, auto_run_consecutive_failures, book_summary_context_percent,
|
|
762
788
|
context_compact_threshold, agent_tool_call_limit, agent_tool_call_global_multiplier,
|
|
763
|
-
agent_tools_json, title_generation_model_id, updated_at
|
|
764
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
789
|
+
agent_tools_json, title_generation_model_id, always_include_setting_info, updated_at
|
|
790
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
765
791
|
ON CONFLICT(work_id) DO UPDATE SET
|
|
766
792
|
system_prompt = excluded.system_prompt,
|
|
767
793
|
daily_token_quota = excluded.daily_token_quota,
|
|
@@ -780,7 +806,8 @@ export class Store {
|
|
|
780
806
|
agent_tool_call_global_multiplier = excluded.agent_tool_call_global_multiplier,
|
|
781
807
|
agent_tools_json = excluded.agent_tools_json,
|
|
782
808
|
title_generation_model_id = excluded.title_generation_model_id,
|
|
783
|
-
|
|
809
|
+
always_include_setting_info = excluded.always_include_setting_info,
|
|
810
|
+
updated_at = excluded.updated_at`, workId, nextPrompt, nextDailyTokenQuota, 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)), Math.min(48, Math.max(5, nextAgentToolCallLimit)), Math.min(6, Math.max(1, nextAgentToolCallGlobalMultiplier)), JSON.stringify(nextAgentTools), nextTitleGenerationModelId, nextAlwaysIncludeSettingInfo ? 1 : 0, timestamp);
|
|
784
811
|
this.audit(workId, "work.ai-settings.updated", "work-ai-settings", workId, {
|
|
785
812
|
systemPromptChanged: input.systemPrompt !== undefined,
|
|
786
813
|
dailyTokenQuota: nextDailyTokenQuota,
|
|
@@ -794,6 +821,7 @@ export class Store {
|
|
|
794
821
|
agentToolCallLimit: Math.min(48, Math.max(5, nextAgentToolCallLimit)),
|
|
795
822
|
agentToolCallGlobalMultiplier: Math.min(6, Math.max(1, nextAgentToolCallGlobalMultiplier)),
|
|
796
823
|
agentTools: nextAgentTools,
|
|
824
|
+
alwaysIncludeSettingInfo: nextAlwaysIncludeSettingInfo,
|
|
797
825
|
titleGenerationModelId: nextTitleGenerationModelId
|
|
798
826
|
});
|
|
799
827
|
return this.getWorkAiSettings(workId);
|
|
@@ -4336,12 +4364,12 @@ export class Store {
|
|
|
4336
4364
|
const row = this.db.get("SELECT * FROM continuation_guard_runs WHERE suggestion_id = ? ORDER BY created_at DESC LIMIT 1", suggestionId);
|
|
4337
4365
|
return row ? this.mapContinuationGuard(row) : null;
|
|
4338
4366
|
}
|
|
4339
|
-
createAiConversation(workId, title = "新对话") {
|
|
4367
|
+
createAiConversation(workId, title = "新对话", taskType = null) {
|
|
4340
4368
|
this.getWork(workId);
|
|
4341
4369
|
const conversationId = id("conversation");
|
|
4342
4370
|
const timestamp = now();
|
|
4343
4371
|
const agentTools = normalizeWorkAgentTools(this.getWorkAiSettings(workId).agentTools);
|
|
4344
|
-
this.db.run("INSERT INTO ai_conversations (id, work_id, title, agent_tools_json, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?)", conversationId, workId, title.trim() || "新对话", JSON.stringify(agentTools), timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
4372
|
+
this.db.run("INSERT INTO ai_conversations (id, work_id, task_type, title, agent_tools_json, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", conversationId, workId, taskType, title.trim() || "新对话", JSON.stringify(agentTools), timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
4345
4373
|
return this.getAiConversation(conversationId);
|
|
4346
4374
|
}
|
|
4347
4375
|
listAiConversations(workId) {
|
|
@@ -4408,10 +4436,12 @@ export class Store {
|
|
|
4408
4436
|
const compactedMessageCount = Math.min(rows.length, Math.max(0, numberValue(conversation, "compacted_message_count")));
|
|
4409
4437
|
return {
|
|
4410
4438
|
workId,
|
|
4439
|
+
roleplayCharacterId: optionalString(conversation, "roleplay_character_id"),
|
|
4411
4440
|
summary: requiredString(conversation, "compacted_summary"),
|
|
4412
4441
|
compactedMessageCount,
|
|
4413
4442
|
totalMessageCount: rows.length,
|
|
4414
4443
|
warningPending: Boolean(optionalString(conversation, "context_warning_at")),
|
|
4444
|
+
injectedEntities: parseAiInjectedEntities(optionalString(conversation, "injected_entities_json") ?? EMPTY_AI_INJECTED_ENTITIES),
|
|
4415
4445
|
messages: rows.slice(compactedMessageCount)
|
|
4416
4446
|
.filter((message) => requiredString(message, "id") !== excludeMessageId)
|
|
4417
4447
|
.map((message) => ({
|
|
@@ -4422,6 +4452,53 @@ export class Store {
|
|
|
4422
4452
|
}))
|
|
4423
4453
|
};
|
|
4424
4454
|
}
|
|
4455
|
+
getAiConversationInjectedEntities(conversationId, workId) {
|
|
4456
|
+
const conversation = this.db.get("SELECT work_id, injected_entities_json FROM ai_conversations WHERE id = ?", conversationId);
|
|
4457
|
+
if (!conversation)
|
|
4458
|
+
throw notFound("AI 对话");
|
|
4459
|
+
if (requiredString(conversation, "work_id") !== workId)
|
|
4460
|
+
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
4461
|
+
return parseAiInjectedEntities(optionalString(conversation, "injected_entities_json") ?? EMPTY_AI_INJECTED_ENTITIES);
|
|
4462
|
+
}
|
|
4463
|
+
mergeAiConversationInjectedEntities(conversationId, workId, extra) {
|
|
4464
|
+
const conversation = this.db.get("SELECT work_id, injected_entities_json FROM ai_conversations WHERE id = ?", conversationId);
|
|
4465
|
+
if (!conversation)
|
|
4466
|
+
throw notFound("AI 对话");
|
|
4467
|
+
if (requiredString(conversation, "work_id") !== workId)
|
|
4468
|
+
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
4469
|
+
const merged = mergeAiInjectedEntities(parseAiInjectedEntities(optionalString(conversation, "injected_entities_json") ?? EMPTY_AI_INJECTED_ENTITIES), extra);
|
|
4470
|
+
this.db.run("UPDATE ai_conversations SET injected_entities_json = ?, updated_at = ? WHERE id = ?", JSON.stringify(merged), now(), conversationId);
|
|
4471
|
+
return merged;
|
|
4472
|
+
}
|
|
4473
|
+
/** 对话首轮写入 system 时钟文案;已有值则原样返回,禁止后续覆盖。 */
|
|
4474
|
+
ensureAiConversationSystemClock(conversationId, workId, candidate) {
|
|
4475
|
+
const conversation = this.db.get("SELECT work_id, system_clock_text FROM ai_conversations WHERE id = ?", conversationId);
|
|
4476
|
+
if (!conversation)
|
|
4477
|
+
throw notFound("AI 对话");
|
|
4478
|
+
if (requiredString(conversation, "work_id") !== workId)
|
|
4479
|
+
throw new AppError(400, "CONVERSATION_WORK_MISMATCH", "AI 对话不属于当前作品");
|
|
4480
|
+
const existing = (optionalString(conversation, "system_clock_text") ?? "").trim();
|
|
4481
|
+
if (existing)
|
|
4482
|
+
return existing;
|
|
4483
|
+
const clock = candidate.trim();
|
|
4484
|
+
if (!clock)
|
|
4485
|
+
return "";
|
|
4486
|
+
this.db.run("UPDATE ai_conversations SET system_clock_text = ? WHERE id = ? AND TRIM(system_clock_text) = ''", clock, conversationId);
|
|
4487
|
+
const refreshed = this.db.get("SELECT system_clock_text FROM ai_conversations WHERE id = ?", conversationId);
|
|
4488
|
+
return (optionalString(refreshed ?? {}, "system_clock_text") ?? clock).trim() || clock;
|
|
4489
|
+
}
|
|
4490
|
+
listCharacterNameEntries(workId) {
|
|
4491
|
+
this.getWork(workId);
|
|
4492
|
+
return this.db.all(`SELECT character_id, normalized_name, display_name, kind FROM character_names
|
|
4493
|
+
WHERE work_id = ?
|
|
4494
|
+
AND character_id NOT IN (SELECT id FROM characters WHERE work_id = ? AND merged_into_character_id IS NOT NULL)
|
|
4495
|
+
ORDER BY LENGTH(normalized_name) DESC, sort_order ASC`, workId, workId).map((row) => ({
|
|
4496
|
+
characterId: requiredString(row, "character_id"),
|
|
4497
|
+
normalizedName: requiredString(row, "normalized_name"),
|
|
4498
|
+
displayName: requiredString(row, "display_name"),
|
|
4499
|
+
kind: requiredString(row, "kind") === "alias" ? "alias" : "primary"
|
|
4500
|
+
}));
|
|
4501
|
+
}
|
|
4425
4502
|
getAiConversationTitleContext(conversationId, workId) {
|
|
4426
4503
|
const conversation = this.db.get("SELECT title, work_id FROM ai_conversations WHERE id = ?", conversationId);
|
|
4427
4504
|
if (!conversation)
|
|
@@ -4455,6 +4532,94 @@ export class Store {
|
|
|
4455
4532
|
this.db.run("UPDATE ai_conversations SET title = ?, updated_at = ? WHERE id = ?", normalizedTitle, now(), conversationId);
|
|
4456
4533
|
return this.getAiConversation(conversationId);
|
|
4457
4534
|
}
|
|
4535
|
+
setAiConversationRoleplayCharacter(conversationId, characterId) {
|
|
4536
|
+
const conversation = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
|
|
4537
|
+
if (!conversation)
|
|
4538
|
+
throw notFound("AI 对话");
|
|
4539
|
+
const workId = requiredString(conversation, "work_id");
|
|
4540
|
+
const previousCharacterId = optionalString(conversation, "roleplay_character_id");
|
|
4541
|
+
if (previousCharacterId === characterId)
|
|
4542
|
+
return this.getAiConversationSummary(conversationId);
|
|
4543
|
+
const messageCount = Number(this.db.get("SELECT COUNT(*) AS count FROM ai_conversation_messages WHERE conversation_id = ?", conversationId)?.count ?? 0);
|
|
4544
|
+
if (messageCount > 0) {
|
|
4545
|
+
throw new AppError(409, previousCharacterId ? "ROLEPLAY_CHARACTER_LOCKED" : "ROLEPLAY_CONVERSATION_STARTED", previousCharacterId ? "角色扮演对话开始后不能退出模式或更换角色卡" : "当前对话已经开始,不能中途切换为角色扮演");
|
|
4546
|
+
}
|
|
4547
|
+
if (characterId) {
|
|
4548
|
+
const character = this.getCharacter(characterId);
|
|
4549
|
+
if (String(character.workId) !== workId) {
|
|
4550
|
+
throw new AppError(400, "ROLEPLAY_CHARACTER_WORK_MISMATCH", "角色卡不属于当前作品");
|
|
4551
|
+
}
|
|
4552
|
+
if (character.mergedIntoCharacterId) {
|
|
4553
|
+
throw new AppError(409, "ROLEPLAY_CHARACTER_MERGED", "已合并角色不能用于角色扮演");
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
this.db.transaction(() => {
|
|
4557
|
+
this.db.run("UPDATE ai_conversations SET roleplay_character_id = ?, task_type = CASE WHEN ? IS NOT NULL THEN 'roleplay' ELSE task_type END, updated_at = ? WHERE id = ?", characterId, characterId, now(), conversationId);
|
|
4558
|
+
this.audit(workId, "ai-conversation.roleplay-updated", "ai-conversation", conversationId, {
|
|
4559
|
+
previousCharacterId,
|
|
4560
|
+
characterId
|
|
4561
|
+
});
|
|
4562
|
+
});
|
|
4563
|
+
return this.getAiConversationSummary(conversationId);
|
|
4564
|
+
}
|
|
4565
|
+
setAiConversationTaskType(conversationId, taskType) {
|
|
4566
|
+
const conversation = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
|
|
4567
|
+
if (!conversation)
|
|
4568
|
+
throw notFound("AI 对话");
|
|
4569
|
+
const workId = requiredString(conversation, "work_id");
|
|
4570
|
+
const previousCharacterId = optionalString(conversation, "roleplay_character_id");
|
|
4571
|
+
const previousTaskType = optionalString(conversation, "task_type") ?? (previousCharacterId ? "roleplay" : "chat");
|
|
4572
|
+
if (previousTaskType === taskType)
|
|
4573
|
+
return this.getAiConversationSummary(conversationId);
|
|
4574
|
+
const messageCount = Number(this.db.get("SELECT COUNT(*) AS count FROM ai_conversation_messages WHERE conversation_id = ?", conversationId)?.count ?? 0);
|
|
4575
|
+
if (messageCount > 0) {
|
|
4576
|
+
throw new AppError(409, "AI_CONVERSATION_TASK_LOCKED", "对话开始后不能切换任务类型");
|
|
4577
|
+
}
|
|
4578
|
+
this.db.transaction(() => {
|
|
4579
|
+
this.db.run("UPDATE ai_conversations SET task_type = ?, roleplay_character_id = CASE WHEN ? = 'roleplay' THEN roleplay_character_id ELSE NULL END, updated_at = ? WHERE id = ?", taskType, taskType, now(), conversationId);
|
|
4580
|
+
this.audit(workId, "ai-conversation.task-type-updated", "ai-conversation", conversationId, {
|
|
4581
|
+
previousTaskType,
|
|
4582
|
+
taskType
|
|
4583
|
+
});
|
|
4584
|
+
});
|
|
4585
|
+
return this.getAiConversationSummary(conversationId);
|
|
4586
|
+
}
|
|
4587
|
+
setAiConversationContextScope(conversationId, scope) {
|
|
4588
|
+
const conversation = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
|
|
4589
|
+
if (!conversation)
|
|
4590
|
+
throw notFound("AI 对话");
|
|
4591
|
+
const workId = requiredString(conversation, "work_id");
|
|
4592
|
+
const assertWork = (record, code, label) => {
|
|
4593
|
+
if (String(record.workId) !== workId)
|
|
4594
|
+
throw new AppError(400, code, `${label}不属于当前作品`);
|
|
4595
|
+
};
|
|
4596
|
+
if (scope.chapterId)
|
|
4597
|
+
assertWork(this.getChapter(scope.chapterId), "CHAPTER_WORK_MISMATCH", "章节");
|
|
4598
|
+
if (scope.volumeId)
|
|
4599
|
+
assertWork(this.getVolume(scope.volumeId), "VOLUME_WORK_MISMATCH", "卷");
|
|
4600
|
+
for (const chapterId of scope.chapterIds ?? [])
|
|
4601
|
+
assertWork(this.getChapter(chapterId), "CHAPTER_WORK_MISMATCH", "章节");
|
|
4602
|
+
for (const characterId of scope.characterIds ?? [])
|
|
4603
|
+
assertWork(this.getCharacter(characterId), "CHARACTER_WORK_MISMATCH", "角色");
|
|
4604
|
+
for (const settingId of scope.settingIds ?? [])
|
|
4605
|
+
assertWork(this.getSetting(settingId), "SETTING_WORK_MISMATCH", "设定");
|
|
4606
|
+
const previousScope = json(optionalString(conversation, "context_scope_json") ?? "", { type: "none" });
|
|
4607
|
+
const serializedScope = JSON.stringify(scope);
|
|
4608
|
+
if (JSON.stringify(previousScope) === serializedScope)
|
|
4609
|
+
return this.getAiConversationSummary(conversationId);
|
|
4610
|
+
const messageCount = Number(this.db.get("SELECT COUNT(*) AS count FROM ai_conversation_messages WHERE conversation_id = ?", conversationId)?.count ?? 0);
|
|
4611
|
+
if (messageCount > 0) {
|
|
4612
|
+
throw new AppError(409, "AI_CONVERSATION_CONTEXT_LOCKED", "对话开始后不能切换上下文引用");
|
|
4613
|
+
}
|
|
4614
|
+
this.db.transaction(() => {
|
|
4615
|
+
this.db.run("UPDATE ai_conversations SET context_scope_json = ?, updated_at = ? WHERE id = ?", serializedScope, now(), conversationId);
|
|
4616
|
+
this.audit(workId, "ai-conversation.context-scope-updated", "ai-conversation", conversationId, {
|
|
4617
|
+
previousScope,
|
|
4618
|
+
scope
|
|
4619
|
+
});
|
|
4620
|
+
});
|
|
4621
|
+
return this.getAiConversationSummary(conversationId);
|
|
4622
|
+
}
|
|
4458
4623
|
addAiConversationMessage(conversationId, input) {
|
|
4459
4624
|
const conversation = this.db.get("SELECT * FROM ai_conversations WHERE id = ?", conversationId);
|
|
4460
4625
|
if (!conversation)
|
|
@@ -4498,10 +4663,13 @@ export class Store {
|
|
|
4498
4663
|
const sourceCompactedCount = Math.max(0, numberValue(conversation, "compacted_message_count"));
|
|
4499
4664
|
const forkCompactedCount = targetIndex + 1 >= sourceCompactedCount ? Math.min(sourceCompactedCount, targetIndex + 1) : 0;
|
|
4500
4665
|
const forkSummary = forkCompactedCount ? requiredString(conversation, "compacted_summary") : "";
|
|
4666
|
+
const injectedEntitiesJson = optionalString(conversation, "injected_entities_json")
|
|
4667
|
+
?? JSON.stringify(EMPTY_AI_INJECTED_ENTITIES);
|
|
4668
|
+
const systemClockText = optionalString(conversation, "system_clock_text") ?? "";
|
|
4501
4669
|
this.db.transaction(() => {
|
|
4502
|
-
this.db.run("INSERT INTO ai_conversations (id, work_id, title, compacted_summary, compacted_message_count, agent_tools_json, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", forkId, requiredString(conversation, "work_id"), title.slice(0, 200), forkSummary, forkCompactedCount, conversation.agent_tools_json == null
|
|
4670
|
+
this.db.run("INSERT INTO ai_conversations (id, work_id, roleplay_character_id, task_type, context_scope_json, title, compacted_summary, compacted_message_count, agent_tools_json, injected_entities_json, system_clock_text, created_at, updated_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", forkId, requiredString(conversation, "work_id"), optionalString(conversation, "roleplay_character_id"), optionalString(conversation, "task_type"), optionalString(conversation, "context_scope_json"), title.slice(0, 200), forkSummary, forkCompactedCount, conversation.agent_tools_json == null
|
|
4503
4671
|
? JSON.stringify(normalizeWorkAgentTools(this.getWorkAiSettings(requiredString(conversation, "work_id")).agentTools))
|
|
4504
|
-
: String(conversation.agent_tools_json), timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
4672
|
+
: String(conversation.agent_tools_json), injectedEntitiesJson, systemClockText, timestamp, timestamp, currentRequestActor()?.userId ?? null);
|
|
4505
4673
|
for (const message of messages.slice(0, targetIndex + 1)) {
|
|
4506
4674
|
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);
|
|
4507
4675
|
}
|
|
@@ -4509,6 +4677,10 @@ export class Store {
|
|
|
4509
4677
|
return this.getAiConversation(forkId);
|
|
4510
4678
|
}
|
|
4511
4679
|
mapAiConversation(row) {
|
|
4680
|
+
const roleplayCharacterId = optionalString(row, "roleplay_character_id");
|
|
4681
|
+
const roleplayCharacter = roleplayCharacterId
|
|
4682
|
+
? this.db.get("SELECT id, name, code FROM characters WHERE id = ? AND work_id = ?", roleplayCharacterId, requiredString(row, "work_id"))
|
|
4683
|
+
: undefined;
|
|
4512
4684
|
return {
|
|
4513
4685
|
id: requiredString(row, "id"),
|
|
4514
4686
|
workId: requiredString(row, "work_id"),
|
|
@@ -4518,6 +4690,13 @@ export class Store {
|
|
|
4518
4690
|
compactedMessageCount: numberValue(row, "compacted_message_count"),
|
|
4519
4691
|
hasCompactedSummary: Boolean(requiredString(row, "compacted_summary")),
|
|
4520
4692
|
contextWarningPending: Boolean(optionalString(row, "context_warning_at")),
|
|
4693
|
+
taskType: optionalString(row, "task_type") ?? (roleplayCharacterId ? "roleplay" : "chat"),
|
|
4694
|
+
contextScope: json(optionalString(row, "context_scope_json") ?? "", { type: "none" }),
|
|
4695
|
+
roleplayCharacter: roleplayCharacter ? {
|
|
4696
|
+
id: requiredString(roleplayCharacter, "id"),
|
|
4697
|
+
name: requiredString(roleplayCharacter, "name"),
|
|
4698
|
+
code: requiredString(roleplayCharacter, "code")
|
|
4699
|
+
} : null,
|
|
4521
4700
|
agentTools: row.agent_tools_json == null || row.agent_tools_json === undefined
|
|
4522
4701
|
? null
|
|
4523
4702
|
: normalizeWorkAgentTools(row.agent_tools_json),
|
|
@@ -5832,6 +6011,9 @@ export class Store {
|
|
|
5832
6011
|
if (scope.type === "settings") {
|
|
5833
6012
|
return [{ type: "settings", title: "仅设定集" }];
|
|
5834
6013
|
}
|
|
6014
|
+
if (scope.type === "settings-catalog") {
|
|
6015
|
+
return [{ type: "settings-catalog", title: "设定库" }];
|
|
6016
|
+
}
|
|
5835
6017
|
if (scope.type === "selection" && typeof scope.selection === "string") {
|
|
5836
6018
|
return [{ type: "selection", selection: scope.selection }];
|
|
5837
6019
|
}
|