@musnows/scriverse 0.6.0 → 0.6.2
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/README.en.md +3 -2
- package/README.md +3 -2
- package/dist/ai-tool-results.js +177 -0
- package/dist/ai-tool-results.js.map +1 -0
- package/dist/ai.js +516 -138
- package/dist/ai.js.map +1 -1
- package/dist/app.js +186 -55
- package/dist/app.js.map +1 -1
- package/dist/attachment-storage.js +9 -1
- package/dist/attachment-storage.js.map +1 -1
- package/dist/cli-contract.js +9 -5
- package/dist/cli-contract.js.map +1 -1
- package/dist/cli-core.js +14 -2
- package/dist/cli-core.js.map +1 -1
- package/dist/collaboration-presence.js +1 -0
- package/dist/collaboration-presence.js.map +1 -1
- package/dist/credential-vault.js +7 -5
- package/dist/credential-vault.js.map +1 -1
- package/dist/database.js +121 -2
- package/dist/database.js.map +1 -1
- package/dist/domain.js +9 -0
- package/dist/domain.js.map +1 -1
- package/dist/public/ai-context-meter.js +7 -0
- package/dist/public/app.js +556 -223
- package/dist/public/entity-version.js +2 -2
- package/dist/public/index.html +19 -11
- package/dist/public/markdown.js +1 -2
- package/dist/public/model-config.d.ts +3 -0
- package/dist/public/model-config.js +14 -0
- package/dist/public/page-route.js +1 -0
- package/dist/public/styles.css +40 -20
- package/dist/public/work-permissions.d.ts +1 -1
- package/dist/public/work-permissions.js +1 -1
- package/dist/security.js +45 -10
- package/dist/security.js.map +1 -1
- package/dist/server-runtime.js +56 -1
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +236 -25
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +19 -71
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/work-permissions.js +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { buildRelationshipGraph, createGalaxyRenderer, renderRelationshipMindMap } from "/relationship-graph.js?v=20260728-galaxy-edge-stars-v3";
|
|
2
2
|
import { collapseExcessBlankLines, formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
|
|
3
|
-
import { renderMarkdown } from "/markdown.js?v=
|
|
3
|
+
import { renderMarkdown } from "/markdown.js?v=20260731-no-external-images-v1";
|
|
4
4
|
import { buildAiReferenceScope, findAiMention, listAiMentionOptions } from "/ai-mentions.js?v=20260716-chapter-references";
|
|
5
5
|
import { shouldShowAiQuickActions } from "/ai-conversation.js?v=20260713-quick-actions";
|
|
6
6
|
import { calculateLineNumberRowHeight, calculateLineNumberRowTop, calculateLineNumberTextOffset, calculateLineNumberTop } from "/line-number-layout.js?v=20260713-row-box-alignment";
|
|
7
7
|
import { buildVditorLineNumberRows } from "/vditor-line-number-layout.js?v=20260729-vditor-line-numbers-v3";
|
|
8
|
-
import { MODEL_PURPOSE_OPTIONS, isKimiModelId, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=
|
|
8
|
+
import { MIN_MODEL_CONTEXT_WINDOW, MODEL_PURPOSE_OPTIONS, isKimiModelId, modelContextWindowGuidance, modelFormValues, modelOptionLabel, modelPayload } from "/model-config.js?v=20260731-model-context-guidance-v1";
|
|
9
9
|
import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-send";
|
|
10
10
|
import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260726-cache-hit-percent";
|
|
11
11
|
import { createStreamTypewriter } from "/stream-typewriter.js?v=20260730-ai-stream-typewriter-v3";
|
|
12
12
|
import { buildUsageCalendar, formatCacheHitRate, formatTokenCount } from "/ai-usage.js?v=20260727-ai-usage-v1";
|
|
13
13
|
import { formatAiMessageTime } from "/ai-message-time.js?v=20260713-cross-day-time";
|
|
14
|
-
import { formatAiContextUsageTooltip, normalizeAiContextTokenDistribution } from "/ai-context-meter.js?v=
|
|
14
|
+
import { formatAiContextUsagePercent, formatAiContextUsageTooltip, normalizeAiContextTokenDistribution } from "/ai-context-meter.js?v=20260731-skills-description-v1";
|
|
15
15
|
import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
|
|
16
16
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
17
17
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
18
18
|
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260725-unified-permissions";
|
|
19
|
-
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=
|
|
19
|
+
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260731-drafts-to-ideas-v1";
|
|
20
20
|
import {
|
|
21
21
|
chapterVersionSourceLabel,
|
|
22
22
|
foreshadowStatusLabel,
|
|
@@ -36,12 +36,12 @@ import {
|
|
|
36
36
|
timelineStatusLabel,
|
|
37
37
|
characterStateFieldLabel
|
|
38
38
|
} from "/display-labels.js?v=20260728-hybrid-search-v1";
|
|
39
|
-
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=
|
|
39
|
+
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260731-work-comments-v2";
|
|
40
40
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
41
41
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
42
42
|
import { buildRaceForest, eligibleRaceParents, orderRaceFilterOptions, racePathLabel } from "/race-hierarchy.js?v=20260729-race-tree-all-v1";
|
|
43
43
|
import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
|
|
44
|
-
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=
|
|
44
|
+
import { WORK_PERMISSION_MODULES, canReadPermissionModule, canReadUiModule, canWritePermissionModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260731-drafts-to-ideas-v1";
|
|
45
45
|
import { MODULE_LAYOUT_STORAGE_KEY, LEGACY_SETTINGS_LAYOUT_STORAGE_KEY, normalizeModuleLayout } from "/module-layout.js?v=20260723-module-layout-toggle";
|
|
46
46
|
import { isGlobalSearchShortcut } from "/keyboard-shortcuts.js?v=20260723-global-search";
|
|
47
47
|
import { resolveGlobalSearchTarget, splitGlobalSearchHighlight } from "/global-search.js?v=20260728-hybrid-search-v1";
|
|
@@ -69,6 +69,7 @@ const defaultPageSizes = Object.freeze({
|
|
|
69
69
|
timeline: 30,
|
|
70
70
|
outlines: 30,
|
|
71
71
|
relationships: 30,
|
|
72
|
+
comments: 30,
|
|
72
73
|
reviews: 30,
|
|
73
74
|
analysisTasks: 30,
|
|
74
75
|
fileVersions: 30
|
|
@@ -134,6 +135,7 @@ const cachedWorkModules = new Set([
|
|
|
134
135
|
"timeline",
|
|
135
136
|
"outlines",
|
|
136
137
|
"relationships",
|
|
138
|
+
"comments",
|
|
137
139
|
"reviews",
|
|
138
140
|
"tasks",
|
|
139
141
|
"ai-settings"
|
|
@@ -311,6 +313,7 @@ function applyWorkAccessMode() {
|
|
|
311
313
|
const button = $(`#module-nav [data-module="${item.uiModule}"]`);
|
|
312
314
|
if (button) button.classList.toggle("permission-hidden", !canReadModule(item.uiModule));
|
|
313
315
|
}
|
|
316
|
+
$("#module-nav [data-module=\"comments\"]").classList.toggle("permission-hidden", Boolean(state.work) && !canReadModule("comments"));
|
|
314
317
|
$("#module-nav [data-work-settings]").classList.toggle("permission-hidden", Boolean(state.work) && !canManageWork());
|
|
315
318
|
$("#new-volume-button").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
|
|
316
319
|
$("#chapter-batch-button").classList.toggle("permission-hidden", Boolean(state.work) && proseReadOnly);
|
|
@@ -392,6 +395,8 @@ let aiReferencesLoadPromise = null;
|
|
|
392
395
|
let aiReferencesLoadWorkId = null;
|
|
393
396
|
let aiConversationsLoadPromise = null;
|
|
394
397
|
let aiConversationsLoadWorkId = null;
|
|
398
|
+
const aiConversationHistoryPageLimit = 20;
|
|
399
|
+
let aiConversationHistoryPage = { page: 1, limit: aiConversationHistoryPageLimit, hasMore: false, nextPage: null };
|
|
395
400
|
let workScopedUiGeneration = 0;
|
|
396
401
|
const loadedVolumeChapterIds = new Set();
|
|
397
402
|
const volumeChapterLoadingIds = new Set();
|
|
@@ -662,7 +667,7 @@ function renderPresence() {
|
|
|
662
667
|
const localKey = presencePageKey(presencePageForRoute());
|
|
663
668
|
syncChapterAutoSaveWithPresence();
|
|
664
669
|
control.classList.remove("hidden");
|
|
665
|
-
$("#presence-count").textContent = `${groups.length}
|
|
670
|
+
$("#presence-count").textContent = `${groups.length} 人`;
|
|
666
671
|
$("#presence-list").innerHTML = groups.map((participant) => {
|
|
667
672
|
const isCurrent = participant.userId === state.user?.userId;
|
|
668
673
|
const samePage = !isCurrent && participant.pages.some((page) => page.key === localKey);
|
|
@@ -899,6 +904,7 @@ let chapterEditorReadOnly = true;
|
|
|
899
904
|
let characterListPage = 1;
|
|
900
905
|
let taskListPage = 1;
|
|
901
906
|
let draftTypeFilter = "all";
|
|
907
|
+
let draftBindingFilters = [];
|
|
902
908
|
let draftFiltersPanelOpen = false;
|
|
903
909
|
const moduleListPages = {
|
|
904
910
|
drafts: 1,
|
|
@@ -908,6 +914,7 @@ const moduleListPages = {
|
|
|
908
914
|
outlinePlans: 1,
|
|
909
915
|
foreshadows: 1,
|
|
910
916
|
relationships: 1,
|
|
917
|
+
comments: 1,
|
|
911
918
|
reviews: 1
|
|
912
919
|
};
|
|
913
920
|
const characterFilters = { raceIds: [], organizationIds: [] };
|
|
@@ -1244,7 +1251,7 @@ function renderAiCitations() {
|
|
|
1244
1251
|
card.append(main, remove, fullText);
|
|
1245
1252
|
host.append(card);
|
|
1246
1253
|
}
|
|
1247
|
-
|
|
1254
|
+
setAiContextMeter(null);
|
|
1248
1255
|
}
|
|
1249
1256
|
|
|
1250
1257
|
function aiReferenceKey(reference) {
|
|
@@ -1288,7 +1295,7 @@ function renderAiReferences() {
|
|
|
1288
1295
|
if (rendered.has(aiReferenceKey(reference))) continue;
|
|
1289
1296
|
prompt.append(createAiReferenceChip(reference), document.createTextNode(" "));
|
|
1290
1297
|
}
|
|
1291
|
-
|
|
1298
|
+
setAiContextMeter(null);
|
|
1292
1299
|
}
|
|
1293
1300
|
|
|
1294
1301
|
function renderAiQuickActions() {
|
|
@@ -1331,6 +1338,34 @@ function resetAiFeed() {
|
|
|
1331
1338
|
$("#ai-feed").innerHTML = '<div class="assistant-message"><span class="message-heading"><span>助手</span></span><div class="message-body"><p>选择章节和模型后,可以问答、续写或校对。所有引用都基于已保存正文。</p></div></div>';
|
|
1332
1339
|
}
|
|
1333
1340
|
|
|
1341
|
+
function appendAiContextCompactionDivider(kind, before = null) {
|
|
1342
|
+
const divider = document.createElement("div");
|
|
1343
|
+
divider.className = "ai-context-compaction-divider";
|
|
1344
|
+
divider.dataset.contextCompaction = kind;
|
|
1345
|
+
divider.dataset.testid = "ai-context-compaction-divider";
|
|
1346
|
+
divider.setAttribute("role", "separator");
|
|
1347
|
+
divider.setAttribute("aria-label", "已压缩上下文");
|
|
1348
|
+
divider.innerHTML = "<span>已压缩上下文</span>";
|
|
1349
|
+
const feed = $("#ai-feed");
|
|
1350
|
+
if (before?.parentElement === feed) feed.insertBefore(divider, before);
|
|
1351
|
+
else feed.append(divider);
|
|
1352
|
+
scrollAiFeedToBottom();
|
|
1353
|
+
return divider;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function renderConversationCompactionDivider(compactedMessageCount, totalMessageCount = null) {
|
|
1357
|
+
const feed = $("#ai-feed");
|
|
1358
|
+
feed.querySelector('[data-context-compaction="conversation"]')?.remove();
|
|
1359
|
+
const compactedCount = Math.max(0, Number(compactedMessageCount) || 0);
|
|
1360
|
+
if (compactedCount === 0) return null;
|
|
1361
|
+
const messages = [...feed.querySelectorAll("[data-message-id]")];
|
|
1362
|
+
const current = state.aiConversations.find((conversation) => conversation.id === state.aiConversationId);
|
|
1363
|
+
const totalCount = Math.max(messages.length, Number(totalMessageCount ?? current?.messageCount) || messages.length);
|
|
1364
|
+
const loadedOffset = Math.max(0, totalCount - messages.length);
|
|
1365
|
+
const boundaryIndex = Math.max(0, Math.min(messages.length, compactedCount - loadedOffset));
|
|
1366
|
+
return appendAiContextCompactionDivider("conversation", messages[boundaryIndex] ?? null);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1334
1369
|
function renderMessageCardActions(message) {
|
|
1335
1370
|
let actions = message.querySelector(".message-card-actions");
|
|
1336
1371
|
if (!actions) {
|
|
@@ -1367,7 +1402,7 @@ function renderMessageCardActions(message) {
|
|
|
1367
1402
|
fork.disabled = true;
|
|
1368
1403
|
try {
|
|
1369
1404
|
const conversation = await api(`/api/ai-conversations/${state.aiConversationId}/fork`, { method: "POST", body: { messageId: message.dataset.messageId } });
|
|
1370
|
-
|
|
1405
|
+
upsertAiConversationSummary(conversation);
|
|
1371
1406
|
await openAiConversation(conversation.id);
|
|
1372
1407
|
toast("已从所选消息创建分支对话");
|
|
1373
1408
|
} catch (error) {
|
|
@@ -1399,7 +1434,7 @@ const AI_TOOL_DISPLAY_NAMES = {
|
|
|
1399
1434
|
grep: "查询正文关键字",
|
|
1400
1435
|
search_story_entities: "搜索作品实体",
|
|
1401
1436
|
read_character_sections: "读取人物 Markdown 章节",
|
|
1402
|
-
search_drafts: "
|
|
1437
|
+
search_drafts: "搜索想法"
|
|
1403
1438
|
};
|
|
1404
1439
|
|
|
1405
1440
|
const AI_TOOL_DESCRIPTIONS = {
|
|
@@ -1559,6 +1594,17 @@ function renderAiProcessSteps(message, steps, completed, durationMs = null) {
|
|
|
1559
1594
|
const list = document.createElement("div");
|
|
1560
1595
|
list.className = "ai-process-list";
|
|
1561
1596
|
for (const step of steps) {
|
|
1597
|
+
if (step?.type === "context_compaction") {
|
|
1598
|
+
const compaction = document.createElement("div");
|
|
1599
|
+
compaction.className = "ai-process-context-compaction";
|
|
1600
|
+
compaction.dataset.testid = "ai-process-context-compaction";
|
|
1601
|
+
compaction.setAttribute("role", "separator");
|
|
1602
|
+
compaction.setAttribute("aria-label", `第 ${Number(step.round) || 1} 轮已压缩上下文`);
|
|
1603
|
+
compaction.title = `已将 ${Number(step.sourceMessageCount) || 0} 条工具上下文压缩为摘要`;
|
|
1604
|
+
compaction.innerHTML = "<span>已压缩上下文</span>";
|
|
1605
|
+
list.append(compaction);
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1562
1608
|
if (step?.type === "tool" && step.toolCall) {
|
|
1563
1609
|
const tool = document.createElement("section");
|
|
1564
1610
|
tool.className = "ai-process-step ai-process-tool-step";
|
|
@@ -1599,6 +1645,12 @@ function setAiHistoryVisible(visible) {
|
|
|
1599
1645
|
function renderAiConversationHistory() {
|
|
1600
1646
|
const host = $("#ai-history-list");
|
|
1601
1647
|
host.replaceChildren();
|
|
1648
|
+
const pagination = $("#ai-history-pagination");
|
|
1649
|
+
const showPagination = aiConversationHistoryPage.page > 1 || aiConversationHistoryPage.hasMore;
|
|
1650
|
+
pagination.classList.toggle("hidden", !showPagination);
|
|
1651
|
+
$("#ai-history-previous").disabled = aiConversationHistoryPage.page <= 1;
|
|
1652
|
+
$("#ai-history-next").disabled = !aiConversationHistoryPage.hasMore;
|
|
1653
|
+
$("#ai-history-page-label").textContent = `第 ${aiConversationHistoryPage.page} 页 · 本页 ${state.aiConversations.length} 条`;
|
|
1602
1654
|
if (!state.aiConversations.length) {
|
|
1603
1655
|
host.innerHTML = '<p class="ai-history-empty">还没有历史对话</p>';
|
|
1604
1656
|
return;
|
|
@@ -1618,6 +1670,43 @@ function renderAiConversationHistory() {
|
|
|
1618
1670
|
}
|
|
1619
1671
|
}
|
|
1620
1672
|
|
|
1673
|
+
function defaultAiConversationTitle(prompt) {
|
|
1674
|
+
const normalized = String(prompt ?? "").replace(/\s+/gu, " ").trim();
|
|
1675
|
+
return Array.from(normalized).slice(0, 15).join("") || "新对话";
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function upsertAiConversationSummary(conversation) {
|
|
1679
|
+
if (!conversation?.id) return;
|
|
1680
|
+
const current = state.aiConversations.find((item) => item.id === conversation.id);
|
|
1681
|
+
const lastMessage = Array.isArray(conversation.messages) ? conversation.messages.at(-1) : null;
|
|
1682
|
+
const summary = { ...current, ...conversation, ...(lastMessage ? { preview: lastMessage.content } : {}) };
|
|
1683
|
+
delete summary.messages;
|
|
1684
|
+
delete summary.messagesPage;
|
|
1685
|
+
if (!current && loadedAiConversationsWorkId === state.work?.id) {
|
|
1686
|
+
loadedAiConversationsWorkId = null;
|
|
1687
|
+
aiConversationHistoryPage = { page: 1, limit: aiConversationHistoryPageLimit, hasMore: false, nextPage: null };
|
|
1688
|
+
state.aiConversations = [summary];
|
|
1689
|
+
renderAiConversationHistory();
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
state.aiConversations = [summary, ...state.aiConversations.filter((item) => item.id !== summary.id)]
|
|
1693
|
+
.sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
|
|
1694
|
+
renderAiConversationHistory();
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function updateAiConversationSummaryFromMessage(message) {
|
|
1698
|
+
if (!message?.conversationId) return;
|
|
1699
|
+
const current = state.aiConversations.find((conversation) => conversation.id === message.conversationId);
|
|
1700
|
+
if (!current) return;
|
|
1701
|
+
upsertAiConversationSummary({
|
|
1702
|
+
...current,
|
|
1703
|
+
title: current.title === "新对话" && message.role === "user" ? defaultAiConversationTitle(message.content) : current.title,
|
|
1704
|
+
messageCount: Number(current.messageCount ?? 0) + 1,
|
|
1705
|
+
preview: message.content,
|
|
1706
|
+
updatedAt: message.createdAt ?? current.updatedAt
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1621
1710
|
function applyAiConversationTitle(title) {
|
|
1622
1711
|
if (!title || !state.aiConversationId) return;
|
|
1623
1712
|
const current = state.aiConversations.find((conversation) => conversation.id === state.aiConversationId);
|
|
@@ -1626,20 +1715,27 @@ function applyAiConversationTitle(title) {
|
|
|
1626
1715
|
renderAiConversationHistory();
|
|
1627
1716
|
}
|
|
1628
1717
|
|
|
1629
|
-
|
|
1718
|
+
function applyAiConversations(pageResult) {
|
|
1719
|
+
state.aiConversations = pageResult.items;
|
|
1720
|
+
aiConversationHistoryPage = {
|
|
1721
|
+
page: pageResult.page,
|
|
1722
|
+
limit: pageResult.limit,
|
|
1723
|
+
hasMore: pageResult.hasMore,
|
|
1724
|
+
nextPage: pageResult.nextPage
|
|
1725
|
+
};
|
|
1726
|
+
loadedAiConversationsWorkId = state.work?.id ?? null;
|
|
1727
|
+
renderAiConversationHistory();
|
|
1728
|
+
const current = state.aiConversations.find((conversation) => conversation.id === state.aiConversationId);
|
|
1729
|
+
if (current) $("#ai-conversation-title").textContent = current.title;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
async function loadAiConversations(page = 1) {
|
|
1630
1733
|
const workId = state.work?.id;
|
|
1631
1734
|
if (!workId) return;
|
|
1632
1735
|
const generation = workScopedUiGeneration;
|
|
1633
|
-
const conversations =
|
|
1736
|
+
const conversations = await apiPage(`/api/works/${workId}/ai-conversations`, page, aiConversationHistoryPageLimit);
|
|
1634
1737
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
1635
|
-
|
|
1636
|
-
loadedAiConversationsWorkId = workId;
|
|
1637
|
-
renderAiConversationHistory();
|
|
1638
|
-
if (openLatest && state.aiConversations.length) await openAiConversation(state.aiConversations[0].id, false);
|
|
1639
|
-
else {
|
|
1640
|
-
const current = state.aiConversations.find((conversation) => conversation.id === state.aiConversationId);
|
|
1641
|
-
if (current) $("#ai-conversation-title").textContent = current.title;
|
|
1642
|
-
}
|
|
1738
|
+
applyAiConversations(conversations);
|
|
1643
1739
|
}
|
|
1644
1740
|
|
|
1645
1741
|
async function ensureAiConversationsLoaded() {
|
|
@@ -1647,7 +1743,7 @@ async function ensureAiConversationsLoaded() {
|
|
|
1647
1743
|
if (!workId || loadedAiConversationsWorkId === workId) return;
|
|
1648
1744
|
if (aiConversationsLoadPromise && aiConversationsLoadWorkId === workId) return aiConversationsLoadPromise;
|
|
1649
1745
|
aiConversationsLoadWorkId = workId;
|
|
1650
|
-
aiConversationsLoadPromise = loadAiConversations(
|
|
1746
|
+
aiConversationsLoadPromise = loadAiConversations();
|
|
1651
1747
|
try {
|
|
1652
1748
|
await aiConversationsLoadPromise;
|
|
1653
1749
|
} finally {
|
|
@@ -1660,11 +1756,15 @@ async function ensureAiConversationsLoaded() {
|
|
|
1660
1756
|
|
|
1661
1757
|
async function openAiConversation(conversationId, hideHistory = true) {
|
|
1662
1758
|
const conversation = await api(`/api/ai-conversations/${conversationId}?page=1&limit=100`);
|
|
1759
|
+
upsertAiConversationSummary(conversation);
|
|
1663
1760
|
state.aiConversationId = conversation.id;
|
|
1664
1761
|
state.aiPromptSent = conversation.messages.some((message) => message.role === "user");
|
|
1665
1762
|
$("#ai-conversation-title").textContent = conversation.title;
|
|
1666
1763
|
resetAiFeed();
|
|
1667
1764
|
for (const message of conversation.messages) appendMessage(message.role, message.content, message.citations, message.createdAt, message.metadata, message.id);
|
|
1765
|
+
if (conversation.hasCompactedSummary) {
|
|
1766
|
+
renderConversationCompactionDivider(conversation.compactedMessageCount, conversation.messageCount);
|
|
1767
|
+
}
|
|
1668
1768
|
state.aiCitations = [];
|
|
1669
1769
|
state.aiReferences = [];
|
|
1670
1770
|
setAiPromptText("");
|
|
@@ -1680,14 +1780,15 @@ async function openAiConversation(conversationId, hideHistory = true) {
|
|
|
1680
1780
|
async function createNewAiConversation() {
|
|
1681
1781
|
if (!state.work) return;
|
|
1682
1782
|
const conversation = await api(`/api/works/${state.work.id}/ai-conversations`, { method: "POST", body: {} });
|
|
1783
|
+
upsertAiConversationSummary(conversation);
|
|
1683
1784
|
state.aiConversationId = conversation.id;
|
|
1684
1785
|
state.aiPromptSent = false;
|
|
1685
1786
|
$("#ai-conversation-title").textContent = conversation.title;
|
|
1686
1787
|
resetAiFeed();
|
|
1687
1788
|
hideAiContextWarning();
|
|
1789
|
+
setAiContextMeter(null);
|
|
1688
1790
|
renderAiQuickActions();
|
|
1689
1791
|
setAiHistoryVisible(false);
|
|
1690
|
-
await loadAiConversations(false);
|
|
1691
1792
|
}
|
|
1692
1793
|
|
|
1693
1794
|
async function ensureAiConversation() {
|
|
@@ -1700,7 +1801,7 @@ async function persistAiConversationMessage(role, content, citations = [], metad
|
|
|
1700
1801
|
const conversationId = await ensureAiConversation();
|
|
1701
1802
|
if (!conversationId) throw new Error("无法创建 AI 对话");
|
|
1702
1803
|
const message = await api(`/api/ai-conversations/${conversationId}/messages`, { method: "POST", body: { role, content, citations, metadata } });
|
|
1703
|
-
|
|
1804
|
+
updateAiConversationSummaryFromMessage(message);
|
|
1704
1805
|
return message;
|
|
1705
1806
|
}
|
|
1706
1807
|
|
|
@@ -1837,9 +1938,9 @@ async function createSelectedLineAnnotation(kind) {
|
|
|
1837
1938
|
if (!state.chapter || !chapterLineSelection || !canEditProse()) return;
|
|
1838
1939
|
const selection = selectedChapterLinePayload(chapterLineSelection.start, chapterLineSelection.end);
|
|
1839
1940
|
closeLineCitationMenu();
|
|
1840
|
-
const note = await inputToast(kind === "todo" ? "描述需要后续处理的事项" :
|
|
1841
|
-
title: kind === "todo" ? "添加正文待办" : "
|
|
1842
|
-
inputLabel: kind === "todo" ? "待办内容" : "
|
|
1941
|
+
const note = await inputToast(kind === "todo" ? "描述需要后续处理的事项" : `评论第 ${selection.safeStart + 1} 行正文`, {
|
|
1942
|
+
title: kind === "todo" ? "添加正文待办" : "添加正文评论",
|
|
1943
|
+
inputLabel: kind === "todo" ? "待办内容" : "评论内容",
|
|
1843
1944
|
confirmLabel: "添加",
|
|
1844
1945
|
maxLength: 2000
|
|
1845
1946
|
});
|
|
@@ -1849,61 +1950,107 @@ async function createSelectedLineAnnotation(kind) {
|
|
|
1849
1950
|
method: "POST",
|
|
1850
1951
|
body: { kind, startLine: selection.safeStart + 1, endLine: selection.safeEnd + 1, note }
|
|
1851
1952
|
});
|
|
1852
|
-
toast(kind === "todo" ? "正文待办已添加" : "
|
|
1953
|
+
toast(kind === "todo" ? "正文待办已添加" : "正文评论已添加");
|
|
1853
1954
|
await openChapterAnnotationsDialog();
|
|
1854
1955
|
} catch (error) {
|
|
1855
1956
|
toast(error.message, "error");
|
|
1856
1957
|
}
|
|
1857
1958
|
}
|
|
1858
1959
|
|
|
1859
|
-
function
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1960
|
+
function chapterAnnotationLineLabel(annotation) {
|
|
1961
|
+
return annotation.startLine === annotation.endLine
|
|
1962
|
+
? `第 ${annotation.startLine} 行`
|
|
1963
|
+
: `第 ${annotation.startLine}-${annotation.endLine} 行`;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
function chapterAnnotationStatusLabel(annotation) {
|
|
1967
|
+
if (annotation.status === "resolved") return annotation.kind === "todo" ? "已完成" : "已解决";
|
|
1968
|
+
return annotation.kind === "todo" ? "待处理" : "未解决";
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
function chapterAnnotationCard(annotation, { showSource = false } = {}) {
|
|
1972
|
+
const lineLabel = chapterAnnotationLineLabel(annotation);
|
|
1973
|
+
const sourceLabel = `${annotation.volumeTitle ?? "正文"} / ${annotation.chapterTitle ?? "未命名章节"}`;
|
|
1974
|
+
const heading = showSource
|
|
1975
|
+
? `<strong class="chapter-annotation-source" title="${esc(sourceLabel)}">${esc(sourceLabel)}</strong><span class="chapter-annotation-line">${esc(lineLabel)}</span>`
|
|
1976
|
+
: `<strong>${esc(lineLabel)}</strong>`;
|
|
1977
|
+
return `<article class="chapter-annotation-card ${annotation.status === "resolved" ? "is-resolved" : ""}" data-annotation-id="${esc(annotation.id)}">
|
|
1978
|
+
<header><span class="chapter-annotation-kind">${annotation.kind === "todo" ? "待办" : "评论"}</span>${heading}<em>${esc(chapterAnnotationStatusLabel(annotation))}</em></header>
|
|
1863
1979
|
<blockquote>${esc(annotation.quote || "空白行")}</blockquote>
|
|
1864
|
-
<p>${esc(annotation.note)}</p>
|
|
1980
|
+
<p data-annotation-content>${esc(annotation.note)}</p>
|
|
1865
1981
|
<small>${esc(annotation.actor)} · ${esc(formatDateTime(annotation.updatedAt))} · v${Number(annotation.versionNo)}</small>
|
|
1866
|
-
<footer><button type="button" data-annotation-locate>定位原文</button>${canEditProse() ? `<button type="button" data-annotation-edit
|
|
1867
|
-
</article
|
|
1982
|
+
<footer><button type="button" data-annotation-locate>定位原文</button>${canEditProse() ? `<button type="button" data-annotation-edit>${annotation.kind === "todo" ? "编辑待办" : "编辑评论"}</button><button type="button" data-annotation-status>${annotation.status === "resolved" ? "重新打开" : (annotation.kind === "todo" ? "完成待办" : "解决评论")}</button><button class="danger-button" type="button" data-annotation-delete>${annotation.kind === "todo" ? "删除待办" : "删除评论"}</button>` : ""}</footer>
|
|
1983
|
+
</article>`;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
function bindChapterAnnotationCards(host, annotations, { refresh, locate, overlayDialog = null }) {
|
|
1987
|
+
const suspendOverlay = () => {
|
|
1988
|
+
if (overlayDialog?.open) overlayDialog.close();
|
|
1989
|
+
};
|
|
1990
|
+
const resumeOverlay = () => {
|
|
1991
|
+
if (overlayDialog && !overlayDialog.open) overlayDialog.showModal();
|
|
1992
|
+
};
|
|
1868
1993
|
host.querySelectorAll("[data-annotation-id]").forEach((card) => {
|
|
1869
|
-
const annotation =
|
|
1994
|
+
const annotation = annotations.find((item) => item.id === card.dataset.annotationId);
|
|
1870
1995
|
if (!annotation) return;
|
|
1871
|
-
card.querySelector("[data-annotation-locate]")?.addEventListener("click", () => {
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
|
|
1996
|
+
card.querySelector("[data-annotation-locate]")?.addEventListener("click", async () => {
|
|
1997
|
+
try { await locate(annotation); }
|
|
1998
|
+
catch (error) { toast(error.message, "error"); }
|
|
1875
1999
|
});
|
|
1876
2000
|
card.querySelector("[data-annotation-status]")?.addEventListener("click", async () => {
|
|
1877
2001
|
try {
|
|
1878
2002
|
await api(`/api/chapter-annotations/${encodeURIComponent(annotation.id)}`, { method: "PATCH", body: { status: annotation.status === "resolved" ? "open" : "resolved", expectedVersionNo: annotation.versionNo } });
|
|
1879
|
-
await
|
|
2003
|
+
await refresh();
|
|
1880
2004
|
} catch (error) { toast(error.message, "error"); }
|
|
1881
2005
|
});
|
|
1882
2006
|
card.querySelector("[data-annotation-edit]")?.addEventListener("click", async () => {
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2007
|
+
suspendOverlay();
|
|
2008
|
+
const note = await inputToast(annotation.kind === "todo" ? "修改待办内容" : "修改评论内容", {
|
|
2009
|
+
title: annotation.kind === "todo" ? "编辑待办" : "编辑评论",
|
|
2010
|
+
inputLabel: annotation.kind === "todo" ? "待办内容" : "评论内容",
|
|
2011
|
+
value: annotation.note,
|
|
2012
|
+
confirmLabel: "保存",
|
|
2013
|
+
maxLength: 2000
|
|
2014
|
+
});
|
|
2015
|
+
if (!note) return resumeOverlay();
|
|
1887
2016
|
try {
|
|
1888
2017
|
await api(`/api/chapter-annotations/${encodeURIComponent(annotation.id)}`, { method: "PATCH", body: { note, expectedVersionNo: annotation.versionNo } });
|
|
1889
|
-
await
|
|
2018
|
+
await refresh();
|
|
1890
2019
|
} catch (error) { toast(error.message, "error"); }
|
|
1891
|
-
|
|
2020
|
+
resumeOverlay();
|
|
1892
2021
|
});
|
|
1893
2022
|
card.querySelector("[data-annotation-delete]")?.addEventListener("click", async () => {
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2023
|
+
suspendOverlay();
|
|
2024
|
+
const confirmed = await confirmToast("删除后不会在正文评论中显示,但历史版本仍会保留。", {
|
|
2025
|
+
title: annotation.kind === "todo" ? "删除待办" : "删除评论",
|
|
2026
|
+
confirmLabel: "确认删除"
|
|
2027
|
+
});
|
|
2028
|
+
if (!confirmed) return resumeOverlay();
|
|
1898
2029
|
try {
|
|
1899
2030
|
await api(`/api/chapter-annotations/${encodeURIComponent(annotation.id)}`, { method: "DELETE", body: { expectedVersionNo: annotation.versionNo } });
|
|
1900
|
-
await
|
|
1901
|
-
|
|
1902
|
-
|
|
2031
|
+
await refresh();
|
|
2032
|
+
} catch (error) { toast(error.message, "error"); }
|
|
2033
|
+
resumeOverlay();
|
|
1903
2034
|
});
|
|
1904
2035
|
});
|
|
1905
2036
|
}
|
|
1906
2037
|
|
|
2038
|
+
function renderChapterAnnotations() {
|
|
2039
|
+
const host = $("#chapter-annotations-list");
|
|
2040
|
+
host.innerHTML = chapterAnnotations.length
|
|
2041
|
+
? chapterAnnotations.map((annotation) => chapterAnnotationCard(annotation)).join("")
|
|
2042
|
+
: '<p class="entity-history-empty">本章还没有评论。在正文行上点击右键即可添加。</p>';
|
|
2043
|
+
bindChapterAnnotationCards(host, chapterAnnotations, {
|
|
2044
|
+
refresh: loadChapterAnnotations,
|
|
2045
|
+
locate: (annotation) => {
|
|
2046
|
+
$("#chapter-annotations-dialog").close();
|
|
2047
|
+
paintChapterLineSelection(annotation.startLine - 1, annotation.endLine - 1);
|
|
2048
|
+
selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
|
|
2049
|
+
},
|
|
2050
|
+
overlayDialog: $("#chapter-annotations-dialog")
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
|
|
1907
2054
|
async function loadChapterAnnotations() {
|
|
1908
2055
|
if (!state.chapter) return;
|
|
1909
2056
|
chapterAnnotations = await api(`/api/chapters/${encodeURIComponent(state.chapter.id)}/annotations`);
|
|
@@ -1912,8 +2059,8 @@ async function loadChapterAnnotations() {
|
|
|
1912
2059
|
|
|
1913
2060
|
async function openChapterAnnotationsDialog() {
|
|
1914
2061
|
if (!state.chapter) return;
|
|
1915
|
-
$("#chapter-annotations-meta").textContent = `《${state.chapter.title}
|
|
1916
|
-
$("#chapter-annotations-list").innerHTML = '<p class="entity-history-empty"
|
|
2062
|
+
$("#chapter-annotations-meta").textContent = `《${state.chapter.title}》的正文评论`;
|
|
2063
|
+
$("#chapter-annotations-list").innerHTML = '<p class="entity-history-empty">正在加载评论…</p>';
|
|
1917
2064
|
if (!$("#chapter-annotations-dialog").open) $("#chapter-annotations-dialog").showModal();
|
|
1918
2065
|
try {
|
|
1919
2066
|
await loadChapterAnnotations();
|
|
@@ -2182,6 +2329,10 @@ function createClientError(payload, fallbackMessage, fallbackStatus = null) {
|
|
|
2182
2329
|
error.details = source.details;
|
|
2183
2330
|
error.failure = typeof source.failure === "string" ? source.failure : undefined;
|
|
2184
2331
|
error.callId = typeof source.callId === "string" ? source.callId : undefined;
|
|
2332
|
+
error.providerName = typeof source.providerName === "string" ? source.providerName : undefined;
|
|
2333
|
+
error.providerId = typeof source.providerId === "string" ? source.providerId : undefined;
|
|
2334
|
+
error.modelId = typeof source.modelId === "string" ? source.modelId : undefined;
|
|
2335
|
+
error.modelRecordId = typeof source.modelRecordId === "string" ? source.modelRecordId : undefined;
|
|
2185
2336
|
return error;
|
|
2186
2337
|
}
|
|
2187
2338
|
|
|
@@ -2193,8 +2344,13 @@ function formatAiFailureMessage(error) {
|
|
|
2193
2344
|
const details = error?.details && typeof error.details === "object" && !Array.isArray(error.details) ? error.details : {};
|
|
2194
2345
|
const failure = typeof error?.failure === "string" ? error.failure : typeof details.failure === "string" ? details.failure : "";
|
|
2195
2346
|
const callId = typeof error?.callId === "string" ? error.callId : typeof details.callId === "string" ? details.callId : "";
|
|
2347
|
+
const providerName = typeof error?.providerName === "string" ? error.providerName : typeof details.providerName === "string" ? details.providerName : "";
|
|
2348
|
+
const providerId = typeof error?.providerId === "string" ? error.providerId : typeof details.providerId === "string" ? details.providerId : "";
|
|
2349
|
+
const modelId = typeof error?.modelId === "string" ? error.modelId : typeof details.modelId === "string" ? details.modelId : "";
|
|
2196
2350
|
if (code) lines.push(`错误码:${code}`);
|
|
2197
2351
|
if (status) lines.push(`服务端状态:HTTP ${status}`);
|
|
2352
|
+
if (providerName || providerId) lines.push(`模型供应商:${providerName || providerId}`);
|
|
2353
|
+
if (modelId) lines.push(`模型 ID:${modelId}`);
|
|
2198
2354
|
if (failure && failure !== message) lines.push(`详细原因:${failure}`);
|
|
2199
2355
|
if (callId) lines.push(`调用 ID:${callId}`);
|
|
2200
2356
|
return lines.join("\n\n");
|
|
@@ -2242,7 +2398,6 @@ function invalidateModuleRequestsAfterMutation(path, method) {
|
|
|
2242
2398
|
path.startsWith("/api/auth/")
|
|
2243
2399
|
|| path.includes("/presence")
|
|
2244
2400
|
|| path.includes("/context/prepare")
|
|
2245
|
-
|| path.includes("/ai-context-usage")
|
|
2246
2401
|
|| path.includes("/chat/stream")
|
|
2247
2402
|
) return;
|
|
2248
2403
|
|
|
@@ -2259,6 +2414,7 @@ function invalidateModuleRequestsAfterMutation(path, method) {
|
|
|
2259
2414
|
if (path.includes("/timeline")) affected.add("timeline");
|
|
2260
2415
|
if (path.includes("/outlines") || path.includes("/foreshadows")) affected.add("outlines");
|
|
2261
2416
|
if (path.includes("/relationships")) affected.add("relationships");
|
|
2417
|
+
if (path.includes("/chapter-annotations/") || /\/chapters\/[^/]+\/annotations(?:$|\?)/u.test(path)) affected.add("comments");
|
|
2262
2418
|
if (path.includes("/reviews")) affected.add("reviews");
|
|
2263
2419
|
if (path.includes("/entity-versions/")) {
|
|
2264
2420
|
if (path.includes("/draft/")) affected.add("drafts");
|
|
@@ -2329,7 +2485,7 @@ async function refreshAuthCaptcha(target = "login") {
|
|
|
2329
2485
|
if (answerInput) answerInput.value = "";
|
|
2330
2486
|
}
|
|
2331
2487
|
|
|
2332
|
-
function showAuth(setupRequired, registrationOpen = false) {
|
|
2488
|
+
function showAuth(setupRequired, registrationOpen = false, setupTokenRequired = false) {
|
|
2333
2489
|
if (state.user) return;
|
|
2334
2490
|
document.body.classList.add("auth-pending");
|
|
2335
2491
|
$("#auth-view").classList.remove("hidden");
|
|
@@ -2346,6 +2502,10 @@ function showAuth(setupRequired, registrationOpen = false) {
|
|
|
2346
2502
|
registerTab.disabled = !canRegister;
|
|
2347
2503
|
registerTab.setAttribute("aria-disabled", String(!canRegister));
|
|
2348
2504
|
registerTab.textContent = canRegister ? "注册" : "注册已禁用";
|
|
2505
|
+
const setupTokenField = $("#register-setup-token-field");
|
|
2506
|
+
const setupTokenInput = setupTokenField.querySelector('input[name="setupToken"]');
|
|
2507
|
+
setupTokenField.classList.toggle("hidden", !setupTokenRequired);
|
|
2508
|
+
setupTokenInput.required = setupTokenRequired;
|
|
2349
2509
|
selectAuthMode(setupRequired && canRegister ? "register" : "login");
|
|
2350
2510
|
}
|
|
2351
2511
|
|
|
@@ -2424,7 +2584,7 @@ async function initializeAuthentication() {
|
|
|
2424
2584
|
if (!session.authenticated) {
|
|
2425
2585
|
// 未登录时一律转到登录页路由;登录页本身则保持原样
|
|
2426
2586
|
if (route.view !== "login") window.history.replaceState(null, "", serializePageRoute({ view: "login" }));
|
|
2427
|
-
showAuth(session.setupRequired, session.registrationOpen === true);
|
|
2587
|
+
showAuth(session.setupRequired, session.registrationOpen === true, session.setupTokenRequired === true);
|
|
2428
2588
|
return false;
|
|
2429
2589
|
}
|
|
2430
2590
|
// 已登录却停在登录页路由时,回到书架首页
|
|
@@ -2523,7 +2683,7 @@ function confirmToast(message, { title = "请再次确认", confirmLabel = "确
|
|
|
2523
2683
|
});
|
|
2524
2684
|
}
|
|
2525
2685
|
|
|
2526
|
-
function inputToast(message, { title = "请输入", inputLabel = title, placeholder = "", confirmLabel = "确认", cancelLabel = "取消", maxLength = 500 } = {}) {
|
|
2686
|
+
function inputToast(message, { title = "请输入", inputLabel = title, value = "", placeholder = "", confirmLabel = "确认", cancelLabel = "取消", maxLength = 500 } = {}) {
|
|
2527
2687
|
const region = $("#toast-region");
|
|
2528
2688
|
const element = document.createElement("section");
|
|
2529
2689
|
element.className = "toast toast-confirmation toast-input-dialog";
|
|
@@ -2537,6 +2697,7 @@ function inputToast(message, { title = "请输入", inputLabel = title, placehol
|
|
|
2537
2697
|
input.className = "toast-input";
|
|
2538
2698
|
input.type = "text";
|
|
2539
2699
|
input.maxLength = maxLength;
|
|
2700
|
+
input.value = value;
|
|
2540
2701
|
input.placeholder = placeholder;
|
|
2541
2702
|
input.setAttribute("aria-label", inputLabel);
|
|
2542
2703
|
const actions = document.createElement("div");
|
|
@@ -2963,15 +3124,15 @@ const workAuditActionLabels = {
|
|
|
2963
3124
|
"chapter.deleted": "删除章节",
|
|
2964
3125
|
"chapter.purged": "彻底删除章节",
|
|
2965
3126
|
"chapter.restored": "恢复章节",
|
|
2966
|
-
"draft.created": "
|
|
2967
|
-
"draft.updated": "
|
|
2968
|
-
"draft.deleted": "
|
|
2969
|
-
"draft.restored": "
|
|
3127
|
+
"draft.created": "创建想法",
|
|
3128
|
+
"draft.updated": "更新想法",
|
|
3129
|
+
"draft.deleted": "删除想法",
|
|
3130
|
+
"draft.restored": "恢复想法",
|
|
2970
3131
|
"work.imported": "导入正文"
|
|
2971
3132
|
};
|
|
2972
3133
|
|
|
2973
3134
|
function workAuditEntityLabel(type) {
|
|
2974
|
-
return ({ work: "作品", volume: "分卷", chapter: "章节", draft: "
|
|
3135
|
+
return ({ work: "作品", volume: "分卷", chapter: "章节", draft: "想法", user: "用户" })[type] ?? type;
|
|
2975
3136
|
}
|
|
2976
3137
|
|
|
2977
3138
|
function workAuditDetailText(detail) {
|
|
@@ -3089,6 +3250,7 @@ async function openPlatformUiSettingsDialog() {
|
|
|
3089
3250
|
$("#page-size-timeline").value = String(pageSizes.timeline);
|
|
3090
3251
|
$("#page-size-outlines").value = String(pageSizes.outlines);
|
|
3091
3252
|
$("#page-size-relationships").value = String(pageSizes.relationships);
|
|
3253
|
+
$("#page-size-comments").value = String(pageSizes.comments);
|
|
3092
3254
|
$("#page-size-reviews").value = String(pageSizes.reviews);
|
|
3093
3255
|
$("#page-size-analysis-tasks").value = String(pageSizes.analysisTasks);
|
|
3094
3256
|
$("#page-size-file-versions").value = String(pageSizes.fileVersions);
|
|
@@ -3455,6 +3617,7 @@ function resetWorkScopedUiCaches() {
|
|
|
3455
3617
|
aiReferencesLoadWorkId = null;
|
|
3456
3618
|
aiConversationsLoadPromise = null;
|
|
3457
3619
|
aiConversationsLoadWorkId = null;
|
|
3620
|
+
aiConversationHistoryPage = { page: 1, limit: aiConversationHistoryPageLimit, hasMore: false, nextPage: null };
|
|
3458
3621
|
raceHierarchyLoadPromise = null;
|
|
3459
3622
|
raceHierarchyLoadWorkId = null;
|
|
3460
3623
|
loadedRaceHierarchyWorkId = null;
|
|
@@ -3465,6 +3628,7 @@ function resetWorkScopedUiCaches() {
|
|
|
3465
3628
|
state.races = [];
|
|
3466
3629
|
characterListPage = 1;
|
|
3467
3630
|
draftTypeFilter = "all";
|
|
3631
|
+
draftBindingFilters = [];
|
|
3468
3632
|
draftFiltersPanelOpen = false;
|
|
3469
3633
|
Object.keys(moduleListPages).forEach((key) => { moduleListPages[key] = 1; });
|
|
3470
3634
|
relationshipFilters.fromCharacterIds = [];
|
|
@@ -3476,9 +3640,6 @@ function resetWorkScopedUiCaches() {
|
|
|
3476
3640
|
volumeChapterRequests.clear();
|
|
3477
3641
|
state.collapsedRaceIds.clear();
|
|
3478
3642
|
lastSavedChapterSnapshot = null;
|
|
3479
|
-
if (aiContextUsageTimer !== null) clearTimeout(aiContextUsageTimer);
|
|
3480
|
-
aiContextUsageTimer = null;
|
|
3481
|
-
aiContextUsageRequest += 1;
|
|
3482
3643
|
state.aiCitations = [];
|
|
3483
3644
|
state.aiReferences = [];
|
|
3484
3645
|
state.aiPromptSent = false;
|
|
@@ -4000,7 +4161,7 @@ function markActiveModule(module) {
|
|
|
4000
4161
|
}
|
|
4001
4162
|
|
|
4002
4163
|
const moduleMeta = {
|
|
4003
|
-
drafts: ["临时想法", "
|
|
4164
|
+
drafts: ["临时想法", "创作想法", "记录可能采用、也可能永远不会写入正文或正式设定的想法。", "新建想法"],
|
|
4004
4165
|
settings: ["世界事实", "世界观与设定库", "锁定的设定会成为 AI 续写与校对的硬约束。", "新建设定"],
|
|
4005
4166
|
characters: ["人物档案", "角色与人物属性", "维护别名、属性、当前状态及不可被 AI 覆盖的字段。", "新建角色"],
|
|
4006
4167
|
races: ["物种档案", "种族与共同设定", "先维护种族档案,再由角色选择引用;角色不能临时填写种族。", "新建种族"],
|
|
@@ -4008,6 +4169,7 @@ const moduleMeta = {
|
|
|
4008
4169
|
timeline: ["剧情脉络", "大事件时间轴", "候选事件经作者确认后,才进入正式时间线。", "新建事件"],
|
|
4009
4170
|
outlines: ["创作规划", "大纲/伏笔", "为每章维护目标、冲突与转折,并持续提醒尚未回收的伏笔。", "新建伏笔"],
|
|
4010
4171
|
relationships: ["跨章证据", "人物关系", "记录关系方向、阶段、置信度与原文依据。", "新建关系"],
|
|
4172
|
+
comments: ["正文协作", "正文评论", "集中查看并处理当前作品所有章节的评论与待办。", ""],
|
|
4011
4173
|
reviews: ["作者决策", "审核队列", "集中处理冲突、候选设定、低置信度关系和时间问题。", "新增审核项"],
|
|
4012
4174
|
tasks: ["AI 深度分析", "AI 分析中心", "对全书或指定章节运行人物关系、世界观、设定、事件与一致性分析。", "开始 AI 分析"],
|
|
4013
4175
|
"ai-settings": ["书籍提示词", "本书 AI 设置", "本书系统提示词会追加在内置提示词和平台全局提示词之后;任务默认模型只作用于当前作品。", "保存设置"]
|
|
@@ -4052,7 +4214,7 @@ async function showModule(module) {
|
|
|
4052
4214
|
$("#module-description").textContent = meta[2];
|
|
4053
4215
|
$("#module-header-actions").querySelectorAll("[data-module-header-action]").forEach((action) => action.remove());
|
|
4054
4216
|
$("#module-create-button").textContent = meta[3];
|
|
4055
|
-
$("#module-create-button").classList.toggle("hidden", module === "ai-settings" || !canEditModule(module));
|
|
4217
|
+
$("#module-create-button").classList.toggle("hidden", module === "ai-settings" || module === "comments" || !canEditModule(module));
|
|
4056
4218
|
$("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
|
|
4057
4219
|
bindModuleContentInteractions();
|
|
4058
4220
|
try {
|
|
@@ -4064,6 +4226,7 @@ async function showModule(module) {
|
|
|
4064
4226
|
if (module === "timeline") await renderTimeline();
|
|
4065
4227
|
if (module === "outlines") await renderOutlines();
|
|
4066
4228
|
if (module === "relationships") await renderRelationships();
|
|
4229
|
+
if (module === "comments") await renderWorkChapterComments();
|
|
4067
4230
|
if (module === "reviews") await renderReviews();
|
|
4068
4231
|
if (module === "tasks") await renderTasks(taskListPage);
|
|
4069
4232
|
if (module === "ai-settings") await renderBookAiSettings();
|
|
@@ -4371,7 +4534,7 @@ function mountRelationshipFilterToggle() {
|
|
|
4371
4534
|
|
|
4372
4535
|
function mountDraftFilterToggle() {
|
|
4373
4536
|
$("#module-header-actions").querySelector('[data-module-header-action="draft-filter-toggle"]')?.remove();
|
|
4374
|
-
$("#module-header-actions").insertAdjacentHTML("afterbegin", `<button type="button" class="module-filter-toggle" data-module-header-action="draft-filter-toggle" aria-label="
|
|
4537
|
+
$("#module-header-actions").insertAdjacentHTML("afterbegin", `<button type="button" class="module-filter-toggle" data-module-header-action="draft-filter-toggle" aria-label="筛选想法" aria-controls="draft-filter-panel" aria-expanded="${draftFiltersPanelOpen}" title="筛选想法"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 5h16l-6.5 7.2v5.3l-3 1.5v-6.8L4 5Z"></path></svg></button>`);
|
|
4375
4538
|
$("#module-header-actions").querySelector('[data-module-header-action="draft-filter-toggle"]')?.addEventListener("click", async () => {
|
|
4376
4539
|
draftFiltersPanelOpen = !draftFiltersPanelOpen;
|
|
4377
4540
|
await renderDrafts(moduleListPages.drafts);
|
|
@@ -4467,15 +4630,47 @@ function settingRecordActions(item) {
|
|
|
4467
4630
|
}
|
|
4468
4631
|
|
|
4469
4632
|
function draftTypeLabel(draftType) {
|
|
4470
|
-
return draftType === "setting" ? "
|
|
4633
|
+
return draftType === "setting" ? "设定想法" : "正文想法";
|
|
4634
|
+
}
|
|
4635
|
+
|
|
4636
|
+
const draftSettingModules = Object.freeze([
|
|
4637
|
+
["settings", "设定库"],
|
|
4638
|
+
["characters", "角色"],
|
|
4639
|
+
["races", "种族"],
|
|
4640
|
+
["organizations", "组织"],
|
|
4641
|
+
["timeline", "时间轴"],
|
|
4642
|
+
["relationships", "关系"],
|
|
4643
|
+
["outlines", "大纲/伏笔"]
|
|
4644
|
+
]);
|
|
4645
|
+
|
|
4646
|
+
function draftSettingModuleLabel(module) {
|
|
4647
|
+
return draftSettingModules.find(([value]) => value === module)?.[1] ?? "未知模块";
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4650
|
+
function draftBindingKey(item) {
|
|
4651
|
+
if (item.draftType === "prose" && item.volumeId) return `volume:${item.volumeId}`;
|
|
4652
|
+
if (item.draftType === "setting" && item.settingModule) return `module:${item.settingModule}`;
|
|
4653
|
+
return "global";
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
function draftBindingLabel(item) {
|
|
4657
|
+
if (item.draftType === "prose" && item.volumeId) return `分卷 · ${item.volumeTitle || "已删除分卷"}`;
|
|
4658
|
+
if (item.draftType === "setting" && item.settingModule) return `设定模块 · ${draftSettingModuleLabel(item.settingModule)}`;
|
|
4659
|
+
return "全局";
|
|
4471
4660
|
}
|
|
4472
4661
|
|
|
4473
4662
|
async function deleteDraft(item) {
|
|
4474
4663
|
if (!item || !canEditModule("drafts")) return;
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4664
|
+
$("#form-dialog").close();
|
|
4665
|
+
if (!await confirmToast(`确认删除想法“${item.title}”吗?想法将从当前列表移除。`, {
|
|
4666
|
+
title: "删除想法",
|
|
4667
|
+
confirmLabel: "继续删除"
|
|
4668
|
+
})) {
|
|
4669
|
+
openDraftDialog(item);
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
if (!await confirmToast(`删除想法“${item.title}”后将从想法列表移除,版本历史仍会保留。仍要删除吗?`, {
|
|
4673
|
+
title: "删除操作需要再次确认",
|
|
4479
4674
|
confirmLabel: "确认删除"
|
|
4480
4675
|
})) {
|
|
4481
4676
|
openDraftDialog(item);
|
|
@@ -4484,7 +4679,7 @@ async function deleteDraft(item) {
|
|
|
4484
4679
|
try {
|
|
4485
4680
|
await api(`/api/drafts/${encodeURIComponent(item.id)}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
|
|
4486
4681
|
await renderDrafts(moduleListPages.drafts);
|
|
4487
|
-
toast("
|
|
4682
|
+
toast("想法已删除");
|
|
4488
4683
|
} catch (error) {
|
|
4489
4684
|
toast(error.message, "error");
|
|
4490
4685
|
try {
|
|
@@ -4497,23 +4692,31 @@ async function deleteDraft(item) {
|
|
|
4497
4692
|
|
|
4498
4693
|
function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
4499
4694
|
const viewOnly = readOnly || !canEditModule("drafts");
|
|
4500
|
-
const
|
|
4501
|
-
|
|
4502
|
-
|
|
4695
|
+
const volumeOptions = [["", "全局(不绑定分卷)"], ...(state.work?.volumes ?? []).map((volume) => [volume.id, volume.title])];
|
|
4696
|
+
const settingModuleOptions = [["", "全局(不绑定设定模块)"], ...draftSettingModules];
|
|
4697
|
+
const management = item && !viewOnly ? `<section class="entity-dialog-management" aria-label="想法操作">
|
|
4698
|
+
<div><strong>想法操作</strong><small>删除后将从想法列表移除,版本历史仍会保留。</small></div>
|
|
4699
|
+
<div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-draft-delete>删除想法</button></div>
|
|
4503
4700
|
</section>` : "";
|
|
4504
|
-
const fields = `<p class="form-field-note"
|
|
4505
|
-
+ field("draftType", "
|
|
4701
|
+
const fields = `<p class="form-field-note">这里记录未确认的临时想法,可能采用,也可能永远不会写入正文或正式设定。</p>`
|
|
4702
|
+
+ field("draftType", "想法类型", "select", item?.draftType ?? "prose", [["prose", "正文想法"], ["setting", "设定想法"]])
|
|
4703
|
+
+ `<div class="draft-binding-field" data-draft-binding-field="prose">${field("volumeId", "绑定分卷", "select", item?.volumeId ?? "", volumeOptions)}</div>`
|
|
4704
|
+
+ `<div class="draft-binding-field" data-draft-binding-field="setting">${field("settingModule", "绑定设定模块", "select", item?.settingModule ?? "", settingModuleOptions)}</div>`
|
|
4506
4705
|
+ field("title", "标题", "text", item?.title ?? "")
|
|
4507
4706
|
+ field("content", "内容", "markdown", item?.content ?? "", {
|
|
4508
4707
|
placeholder: "记录尚未定稿的片段、方向或设定想法……",
|
|
4708
|
+
attachmentModule: "drafts",
|
|
4509
4709
|
readOnly: viewOnly
|
|
4510
4710
|
}) + management;
|
|
4511
|
-
openDialog(item ? viewOnly ? "
|
|
4711
|
+
openDialog(item ? viewOnly ? "查看想法" : "编辑想法" : "新建想法", fields, async (form) => {
|
|
4512
4712
|
if (viewOnly) return;
|
|
4513
4713
|
const title = String(form.get("title") ?? "").trim();
|
|
4514
|
-
if (!title) throw new Error("
|
|
4714
|
+
if (!title) throw new Error("请填写想法标题");
|
|
4715
|
+
const draftType = form.get("draftType") === "setting" ? "setting" : "prose";
|
|
4515
4716
|
const body = {
|
|
4516
|
-
draftType
|
|
4717
|
+
draftType,
|
|
4718
|
+
volumeId: draftType === "prose" ? String(form.get("volumeId") ?? "") || null : null,
|
|
4719
|
+
settingModule: draftType === "setting" ? String(form.get("settingModule") ?? "") || null : null,
|
|
4517
4720
|
title,
|
|
4518
4721
|
content: String(form.get("content") ?? ""),
|
|
4519
4722
|
...(item ? { expectedVersionNo: item.versionNo } : {})
|
|
@@ -4523,13 +4726,25 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
|
4523
4726
|
body
|
|
4524
4727
|
});
|
|
4525
4728
|
await renderDrafts(moduleListPages.drafts);
|
|
4526
|
-
toast(item ? "
|
|
4729
|
+
toast(item ? "想法已保存" : "想法已创建");
|
|
4527
4730
|
}, item ? draftTypeLabel(item.draftType) : "未确认想法", {
|
|
4528
|
-
submitLabel: viewOnly ? "关闭" : "
|
|
4731
|
+
submitLabel: viewOnly ? "关闭" : "保存想法",
|
|
4529
4732
|
hideCancel: viewOnly,
|
|
4530
4733
|
editor: true,
|
|
4531
|
-
errorPrefix: "
|
|
4532
|
-
});
|
|
4734
|
+
errorPrefix: "想法保存失败:"
|
|
4735
|
+
});
|
|
4736
|
+
const draftTypeSelect = $("#dialog-fields").querySelector('select[name="draftType"]');
|
|
4737
|
+
const syncDraftBindingFields = () => {
|
|
4738
|
+
const draftType = draftTypeSelect?.value === "setting" ? "setting" : "prose";
|
|
4739
|
+
$("#dialog-fields").querySelectorAll("[data-draft-binding-field]").forEach((bindingField) => {
|
|
4740
|
+
const active = bindingField.dataset.draftBindingField === draftType;
|
|
4741
|
+
bindingField.classList.toggle("hidden", !active);
|
|
4742
|
+
const select = bindingField.querySelector("select");
|
|
4743
|
+
if (select) select.disabled = !active || viewOnly;
|
|
4744
|
+
});
|
|
4745
|
+
};
|
|
4746
|
+
draftTypeSelect?.addEventListener("change", syncDraftBindingFields);
|
|
4747
|
+
syncDraftBindingFields();
|
|
4533
4748
|
if (viewOnly) {
|
|
4534
4749
|
$("#dialog-fields").querySelectorAll("input, select, textarea").forEach((control) => {
|
|
4535
4750
|
if (control instanceof HTMLSelectElement) control.disabled = true;
|
|
@@ -4543,48 +4758,58 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
|
4543
4758
|
|
|
4544
4759
|
async function renderDrafts(page = moduleListPages.drafts) {
|
|
4545
4760
|
const allDrafts = await moduleApiAllPages("drafts", `/api/works/${state.work.id}/drafts`);
|
|
4546
|
-
const
|
|
4761
|
+
const typeDrafts = draftTypeFilter === "all"
|
|
4547
4762
|
? allDrafts
|
|
4548
4763
|
: allDrafts.filter((draft) => draft.draftType === draftTypeFilter);
|
|
4764
|
+
const selectedBindingKeys = new Set(draftBindingFilters);
|
|
4765
|
+
const drafts = selectedBindingKeys.size
|
|
4766
|
+
? typeDrafts.filter((draft) => selectedBindingKeys.has(draftBindingKey(draft)))
|
|
4767
|
+
: typeDrafts;
|
|
4768
|
+
const hasDraftFilters = draftTypeFilter !== "all" || selectedBindingKeys.size > 0;
|
|
4549
4769
|
mountModuleCount(drafts.length);
|
|
4550
4770
|
const pageResult = paginateModuleItems(drafts, page, "drafts");
|
|
4551
4771
|
moduleListPages.drafts = pageResult.page;
|
|
4552
4772
|
const layout = readModuleLayout();
|
|
4553
|
-
if (drafts.length) mountModuleLayoutToggle(layout, "
|
|
4773
|
+
if (drafts.length) mountModuleLayoutToggle(layout, "想法列表样式");
|
|
4554
4774
|
else $("#module-header-actions").querySelector('[data-module-header-action="layout-toggle"]')?.remove();
|
|
4555
4775
|
mountDraftFilterToggle();
|
|
4556
|
-
const
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4776
|
+
const bindingOptions = [
|
|
4777
|
+
["global", "全局(未绑定)"],
|
|
4778
|
+
...(state.work?.volumes ?? []).map((volume) => [`volume:${volume.id}`, `分卷 · ${volume.title}`]),
|
|
4779
|
+
...draftSettingModules.map(([value, label]) => [`module:${value}`, `设定模块 · ${label}`])
|
|
4780
|
+
];
|
|
4781
|
+
const filterToolbar = `<section id="draft-filter-panel" class="character-filter-toolbar draft-filter-toolbar${draftFiltersPanelOpen ? "" : " hidden"}" aria-label="想法筛选">
|
|
4782
|
+
<label class="draft-type-filter-field" for="draft-type-filter"><span>按想法类型筛选</span><select id="draft-type-filter" aria-label="按想法类型筛选">
|
|
4783
|
+
<option value="all" ${draftTypeFilter === "all" ? "selected" : ""}>全部想法</option>
|
|
4784
|
+
<option value="prose" ${draftTypeFilter === "prose" ? "selected" : ""}>正文想法</option>
|
|
4785
|
+
<option value="setting" ${draftTypeFilter === "setting" ? "selected" : ""}>设定想法</option>
|
|
4786
|
+
</select></label>
|
|
4787
|
+
<details class="character-filter-dropdown"><summary><span>按绑定位置筛选</span><strong>${selectedBindingKeys.size ? `已选 ${selectedBindingKeys.size} 项` : "全部位置"}</strong></summary><div id="draft-binding-filter" class="character-filter-options">${bindingOptions.map(([value, label]) => `<label class="character-filter-option"><input type="checkbox" value="${esc(value)}" ${selectedBindingKeys.has(value) ? "checked" : ""}><span>${esc(label)}</span></label>`).join("")}</div></details>
|
|
4788
|
+
<div class="character-filter-toolbar-actions">${hasDraftFilters ? `<span class="character-filter-result-count" aria-live="polite">筛选后剩余 ${drafts.length} 条想法</span>` : ""}<button id="clear-draft-filters" class="ghost-button" type="button" ${hasDraftFilters ? "" : "disabled"}>重置筛选</button></div>
|
|
4564
4789
|
</section>`;
|
|
4565
4790
|
const actions = (item) => canEditModule("drafts")
|
|
4566
|
-
? `${recordCardEditButton("edit-draft", item.id,
|
|
4791
|
+
? `${recordCardEditButton("edit-draft", item.id, `想法“${item.title}”`)}${recordHistoryButton("draft", item.id, item.title)}`
|
|
4567
4792
|
: recordHistoryButton("draft", item.id, item.title);
|
|
4568
4793
|
const cards = `<div class="card-grid">${pageResult.items.map((item) => `
|
|
4569
|
-
<article class="record-card preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="
|
|
4570
|
-
<small>${esc(draftTypeLabel(item.draftType))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
4794
|
+
<article class="record-card preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="查看想法 ${esc(item.title)}">
|
|
4795
|
+
<small>${esc(draftTypeLabel(item.draftType))} · ${esc(draftBindingLabel(item))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
4571
4796
|
<h3>${esc(item.title)}</h3>
|
|
4572
4797
|
<div class="record-markdown-preview">${esc(item.contentPreview || "暂无内容")}</div>
|
|
4573
4798
|
<div class="card-actions">${actions(item)}</div>
|
|
4574
4799
|
</article>`).join("")}</div>`;
|
|
4575
4800
|
const rows = `<div class="module-row-list">${pageResult.items.map((item) => {
|
|
4576
4801
|
const preview = moduleRowPreview(item.contentPreview || "暂无内容");
|
|
4577
|
-
return `<article class="record-card module-row preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="
|
|
4578
|
-
<small>${esc(draftTypeLabel(item.draftType))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
4802
|
+
return `<article class="record-card module-row preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="查看想法 ${esc(item.title)}">
|
|
4803
|
+
<small>${esc(draftTypeLabel(item.draftType))} · ${esc(draftBindingLabel(item))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
4579
4804
|
<h3>${esc(item.title)}</h3><p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
|
|
4580
4805
|
<div class="card-actions">${actions(item)}</div>
|
|
4581
4806
|
</article>`;
|
|
4582
4807
|
}).join("")}</div>`;
|
|
4583
4808
|
const emptyDrafts = allDrafts.length
|
|
4584
|
-
? emptyModule("
|
|
4585
|
-
: emptyModule("
|
|
4809
|
+
? emptyModule("没有符合筛选条件的想法", "可以切换想法类型、绑定位置或重置筛选。")
|
|
4810
|
+
: emptyModule("还没有想法", "把不一定会进入正文或设定的片段、方向和备选想法先记在这里。");
|
|
4586
4811
|
$("#module-content").innerHTML = filterToolbar + (drafts.length
|
|
4587
|
-
? `${layout === "rows" ? rows : cards}${renderModulePagination(pageResult, "drafts", "
|
|
4812
|
+
? `${layout === "rows" ? rows : cards}${renderModulePagination(pageResult, "drafts", "想法列表")}`
|
|
4588
4813
|
: emptyDrafts);
|
|
4589
4814
|
$("#draft-type-filter").addEventListener("change", async (event) => {
|
|
4590
4815
|
draftTypeFilter = ["prose", "setting"].includes(event.currentTarget.value) ? event.currentTarget.value : "all";
|
|
@@ -4592,6 +4817,19 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
4592
4817
|
moduleListPages.drafts = 1;
|
|
4593
4818
|
await renderDrafts(1);
|
|
4594
4819
|
});
|
|
4820
|
+
$("#draft-binding-filter").addEventListener("change", async () => {
|
|
4821
|
+
draftBindingFilters = [...$("#draft-binding-filter").querySelectorAll('input[type="checkbox"]:checked')].map((input) => input.value);
|
|
4822
|
+
draftFiltersPanelOpen = true;
|
|
4823
|
+
moduleListPages.drafts = 1;
|
|
4824
|
+
await renderDrafts(1);
|
|
4825
|
+
});
|
|
4826
|
+
$("#clear-draft-filters")?.addEventListener("click", async () => {
|
|
4827
|
+
draftTypeFilter = "all";
|
|
4828
|
+
draftBindingFilters = [];
|
|
4829
|
+
draftFiltersPanelOpen = true;
|
|
4830
|
+
moduleListPages.drafts = 1;
|
|
4831
|
+
await renderDrafts(1);
|
|
4832
|
+
});
|
|
4595
4833
|
bindModuleLayoutToggle(() => renderDrafts(pageResult.page));
|
|
4596
4834
|
bindModulePagination("drafts", renderDrafts);
|
|
4597
4835
|
$("#module-content").querySelectorAll("[data-open-draft]").forEach((card) => {
|
|
@@ -5095,6 +5333,35 @@ async function renderRelationships(page = moduleListPages.relationships) {
|
|
|
5095
5333
|
bindEntityHistoryButtons(async () => { await renderRelationships(pageResult.page); await loadAiReferences(); });
|
|
5096
5334
|
}
|
|
5097
5335
|
|
|
5336
|
+
async function renderWorkChapterComments(page = moduleListPages.comments) {
|
|
5337
|
+
const pageSize = pageSizeFor("comments");
|
|
5338
|
+
const result = await moduleApiPage(
|
|
5339
|
+
"comments",
|
|
5340
|
+
`/api/works/${encodeURIComponent(state.work.id)}/chapter-annotations`,
|
|
5341
|
+
page,
|
|
5342
|
+
pageSize
|
|
5343
|
+
);
|
|
5344
|
+
if (!result.items.length && page > 1) return renderWorkChapterComments(page - 1);
|
|
5345
|
+
const total = Number(result.total ?? result.items.length);
|
|
5346
|
+
const pageCount = Math.max(1, Math.ceil(total / result.limit));
|
|
5347
|
+
const pageResult = { ...result, total, pageCount, itemCount: result.items.length };
|
|
5348
|
+
moduleListPages.comments = pageResult.page;
|
|
5349
|
+
mountModuleCount(total);
|
|
5350
|
+
$("#module-content").innerHTML = result.items.length
|
|
5351
|
+
? `<div class="chapter-comment-module-list">${result.items.map((annotation) => chapterAnnotationCard(annotation, { showSource: true })).join("")}</div>${renderModulePagination(pageResult, "comments", "正文评论列表")}`
|
|
5352
|
+
: emptyModule("还没有正文评论", "在任一正文行上点击右键,即可添加评论或待办。");
|
|
5353
|
+
bindModulePagination("comments", renderWorkChapterComments);
|
|
5354
|
+
bindChapterAnnotationCards($("#module-content"), result.items, {
|
|
5355
|
+
refresh: () => renderWorkChapterComments(pageResult.page),
|
|
5356
|
+
locate: async (annotation) => {
|
|
5357
|
+
await selectChapter(annotation.chapterId);
|
|
5358
|
+
await new Promise((resolve) => window.requestAnimationFrame(resolve));
|
|
5359
|
+
paintChapterLineSelection(annotation.startLine - 1, annotation.endLine - 1);
|
|
5360
|
+
selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
|
|
5361
|
+
}
|
|
5362
|
+
});
|
|
5363
|
+
}
|
|
5364
|
+
|
|
5098
5365
|
async function renderReviews(page = moduleListPages.reviews) {
|
|
5099
5366
|
const canReadCharacters = canReadModule("characters");
|
|
5100
5367
|
const canResolveReview = canEditModule("reviews");
|
|
@@ -5943,9 +6210,9 @@ function renderProviderCards(providers, models) {
|
|
|
5943
6210
|
: provider.connectionStatus !== "success"
|
|
5944
6211
|
? `<span class="model-status-badge is-unavailable">连接不可用</span>`
|
|
5945
6212
|
: "";
|
|
5946
|
-
return `<div class="provider-model-row${modelUnavailable ? " is-unavailable" : ""}"><button class="pill model-pill" type="button" data-edit-model="${esc(model.id)}" aria-label="编辑模型 ${esc(model.displayName)}">${esc(model.displayName)} · ${model.enabled ? "启用" : "停用"} · 思考模式 ${model.thinkingEnabled ? "开启" : "关闭"} · 上下文 ${Number(model.contextWindow ?? 128000).toLocaleString("zh-CN")} 令牌 · 最大输出 ${Number(model.preset?.max_tokens ?? 32000).toLocaleString("zh-CN")}</button>${modelStatus}
|
|
6213
|
+
return `<div class="provider-model-row${modelUnavailable ? " is-unavailable" : ""}"><button class="pill model-pill" type="button" data-edit-model="${esc(model.id)}" aria-label="编辑模型 ${esc(model.displayName)}">${esc(model.displayName)} · ${model.enabled ? "启用" : "停用"} · 思考模式 ${model.thinkingEnabled ? "开启" : "关闭"} · 上下文 ${Number(model.contextWindow ?? 128000).toLocaleString("zh-CN")} 令牌 · 最大输出 ${Number(model.preset?.max_tokens ?? 32000).toLocaleString("zh-CN")}</button>${modelStatus}</div>`;
|
|
5947
6214
|
}).join("")}</div>
|
|
5948
|
-
<div class="card-actions"><button data-edit-provider="${esc(provider.id)}">编辑配置</button
|
|
6215
|
+
<div class="card-actions"><button data-edit-provider="${esc(provider.id)}">编辑配置</button>${provider.status === "enabled" ? `<button data-test-provider="${esc(provider.id)}" ${providerModels.length ? "" : "disabled aria-disabled=\"true\" title=\"请先添加模型\""}>测试连接</button>` : ""}<button data-add-model="${esc(provider.id)}">添加模型</button></div></article>`;
|
|
5949
6216
|
}).join("")}</div>`
|
|
5950
6217
|
: emptyModule("尚未配置 AI 供应商", "添加 OpenAI 或 Anthropic 兼容接口地址和密钥,测试成功后再添加模型。");
|
|
5951
6218
|
}
|
|
@@ -5959,14 +6226,6 @@ function bindPlatformProviderActions(host, providers, models) {
|
|
|
5959
6226
|
await renderPlatformAiConfig();
|
|
5960
6227
|
await loadModels();
|
|
5961
6228
|
}));
|
|
5962
|
-
host.querySelectorAll("[data-test-model]").forEach((button) => button.addEventListener("click", async () => {
|
|
5963
|
-
button.disabled = true;
|
|
5964
|
-
button.textContent = "测试中";
|
|
5965
|
-
const result = await api(`/api/models/${button.dataset.testModel}/test`, { method: "POST", body: {} });
|
|
5966
|
-
toast(result.ok ? "模型连接测试成功" : `模型连接失败:${result.error}`, result.ok ? "info" : "error");
|
|
5967
|
-
await renderPlatformAiConfig();
|
|
5968
|
-
await loadModels();
|
|
5969
|
-
}));
|
|
5970
6229
|
host.querySelectorAll("[data-add-model]").forEach((button) => button.addEventListener("click", () => openModelDialog(button.dataset.addModel)));
|
|
5971
6230
|
host.querySelectorAll("[data-edit-model]").forEach((button) => button.addEventListener("click", () => openModelDialog(undefined, models.find((model) => model.id === button.dataset.editModel))));
|
|
5972
6231
|
host.querySelectorAll("[data-edit-provider]").forEach((button) => button.addEventListener("click", () => openProviderDialog(providers.find((provider) => provider.id === button.dataset.editProvider))));
|
|
@@ -6408,7 +6667,7 @@ async function renderBookAiSettings() {
|
|
|
6408
6667
|
);
|
|
6409
6668
|
host.querySelector(".ai-agent-tools").insertAdjacentHTML(
|
|
6410
6669
|
"beforeend",
|
|
6411
|
-
`<label><input name="agent-tool" type="checkbox" value="search_drafts" ${agentTools.has("search_drafts") ? "checked" : ""}><span><strong
|
|
6670
|
+
`<label><input name="agent-tool" type="checkbox" value="search_drafts" ${agentTools.has("search_drafts") ? "checked" : ""}><span><strong>搜索想法</strong><small>查询正文想法和设定想法。这些内容只是可能采用、也可能永远不会进入正文或正式设定的临时方向,Agent 不会把它当作已确认事实。</small></span></label>`
|
|
6412
6671
|
);
|
|
6413
6672
|
if (!canEditModule("ai-settings")) {
|
|
6414
6673
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
@@ -6447,7 +6706,7 @@ async function renderBookAiSettings() {
|
|
|
6447
6706
|
try {
|
|
6448
6707
|
await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { systemPrompt: $("#work-system-prompt").value } });
|
|
6449
6708
|
toast("本书系统提示词已保存");
|
|
6450
|
-
|
|
6709
|
+
setAiContextMeter(null);
|
|
6451
6710
|
} catch (error) {
|
|
6452
6711
|
toast(error.message, "error");
|
|
6453
6712
|
} finally {
|
|
@@ -6500,7 +6759,7 @@ async function renderBookAiSettings() {
|
|
|
6500
6759
|
try {
|
|
6501
6760
|
await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { bookSummaryContextPercent: Number($("#book-summary-context-percent").value) } });
|
|
6502
6761
|
toast("全书概要引用配额已保存");
|
|
6503
|
-
|
|
6762
|
+
setAiContextMeter(null);
|
|
6504
6763
|
} catch (error) {
|
|
6505
6764
|
toast(error.message, "error");
|
|
6506
6765
|
} finally {
|
|
@@ -6513,7 +6772,7 @@ async function renderBookAiSettings() {
|
|
|
6513
6772
|
try {
|
|
6514
6773
|
await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { contextCompactThreshold: Number($("#context-compact-threshold").value) } });
|
|
6515
6774
|
toast("对话上下文 compact 阈值已保存");
|
|
6516
|
-
|
|
6775
|
+
setAiContextMeter(null);
|
|
6517
6776
|
} catch (error) {
|
|
6518
6777
|
toast(error.message, "error");
|
|
6519
6778
|
} finally {
|
|
@@ -6564,13 +6823,17 @@ async function loadModels() {
|
|
|
6564
6823
|
const generation = workScopedUiGeneration;
|
|
6565
6824
|
const models = await api(`/api/works/${workId}/models`);
|
|
6566
6825
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
6567
|
-
|
|
6826
|
+
applyAiModels(models);
|
|
6568
6827
|
loadedAiModelsWorkId = workId;
|
|
6828
|
+
}
|
|
6829
|
+
|
|
6830
|
+
function applyAiModels(models) {
|
|
6831
|
+
state.models = models.filter((model) => isSelectableModel(model));
|
|
6569
6832
|
const select = $("#ai-model");
|
|
6570
6833
|
select.innerHTML = state.models.length
|
|
6571
6834
|
? state.models.map((model) => `<option value="${esc(model.id)}">${esc(modelOptionLabel(model))}</option>`).join("")
|
|
6572
6835
|
: '<option value="">请先配置模型</option>';
|
|
6573
|
-
|
|
6836
|
+
setAiContextMeter(null);
|
|
6574
6837
|
}
|
|
6575
6838
|
|
|
6576
6839
|
async function ensureAiModelsLoaded() {
|
|
@@ -6594,9 +6857,6 @@ async function ensureAiModelsLoaded() {
|
|
|
6594
6857
|
}
|
|
6595
6858
|
}
|
|
6596
6859
|
|
|
6597
|
-
let aiContextUsageTimer = null;
|
|
6598
|
-
let aiContextUsageRequest = 0;
|
|
6599
|
-
|
|
6600
6860
|
function currentAiRequestScope() {
|
|
6601
6861
|
if (!state.work) return null;
|
|
6602
6862
|
const taskType = $("#ai-task").value;
|
|
@@ -6622,7 +6882,7 @@ function renderAiContextDistribution(usage) {
|
|
|
6622
6882
|
const distribution = normalizeAiContextTokenDistribution(usage);
|
|
6623
6883
|
const contextWindow = distribution.contextWindow.toLocaleString("zh-CN");
|
|
6624
6884
|
$("#ai-context-popover-description").textContent = usage
|
|
6625
|
-
? `已占用 ${distribution.occupiedTokens.toLocaleString("zh-CN")} / ${contextWindow} tok`
|
|
6885
|
+
? `已占用 ${distribution.occupiedTokens.toLocaleString("zh-CN")} / ${contextWindow} tok · ${formatAiContextUsagePercent(distribution.occupiedTokens, distribution.contextWindow)}`
|
|
6626
6886
|
: "选择可用模型后显示当前上下文用量";
|
|
6627
6887
|
host.replaceChildren(...distribution.items.map((item) => {
|
|
6628
6888
|
const row = document.createElement("div");
|
|
@@ -6635,9 +6895,9 @@ function renderAiContextDistribution(usage) {
|
|
|
6635
6895
|
label.className = "ai-context-distribution-label";
|
|
6636
6896
|
const title = document.createElement("span");
|
|
6637
6897
|
title.textContent = item.label;
|
|
6638
|
-
if (item.key === "context") {
|
|
6898
|
+
if (item.key === "skills" || item.key === "context") {
|
|
6639
6899
|
const description = document.createElement("small");
|
|
6640
|
-
description.textContent = "用户和 agent 的交互";
|
|
6900
|
+
description.textContent = item.key === "skills" ? "待加入" : "用户和 agent 的交互";
|
|
6641
6901
|
title.append(" ", description);
|
|
6642
6902
|
}
|
|
6643
6903
|
const value = document.createElement("strong");
|
|
@@ -6701,57 +6961,6 @@ function hideAiContextWarning() {
|
|
|
6701
6961
|
$("#ai-context-warning").classList.add("hidden");
|
|
6702
6962
|
}
|
|
6703
6963
|
|
|
6704
|
-
async function prepareAiConversationContext({ instruction, scope, modelId, citations }) {
|
|
6705
|
-
const conversationId = await ensureAiConversation();
|
|
6706
|
-
const prepared = await api(`/api/ai-conversations/${conversationId}/context/prepare`, {
|
|
6707
|
-
method: "POST",
|
|
6708
|
-
body: { instruction, scope, modelId, citations }
|
|
6709
|
-
});
|
|
6710
|
-
setAiContextMeter(prepared.usage);
|
|
6711
|
-
if (prepared.action === "warn") {
|
|
6712
|
-
showAiContextWarning(prepared.usage);
|
|
6713
|
-
return false;
|
|
6714
|
-
}
|
|
6715
|
-
hideAiContextWarning();
|
|
6716
|
-
if (prepared.action === "compacted") toast("已自动整理较早对话为长期记忆并继续发送");
|
|
6717
|
-
return true;
|
|
6718
|
-
}
|
|
6719
|
-
|
|
6720
|
-
function scheduleAiContextUsage() {
|
|
6721
|
-
if (aiContextUsageTimer !== null) clearTimeout(aiContextUsageTimer);
|
|
6722
|
-
aiContextUsageTimer = setTimeout(() => {
|
|
6723
|
-
aiContextUsageTimer = null;
|
|
6724
|
-
void refreshAiContextUsage();
|
|
6725
|
-
}, 260);
|
|
6726
|
-
}
|
|
6727
|
-
|
|
6728
|
-
async function refreshAiContextUsage() {
|
|
6729
|
-
const requestScope = currentAiRequestScope();
|
|
6730
|
-
const modelId = $("#ai-model").value;
|
|
6731
|
-
if (!requestScope || !modelId || (requestScope.taskType === "polish" && !requestScope.selection)) {
|
|
6732
|
-
setAiContextMeter(null);
|
|
6733
|
-
return;
|
|
6734
|
-
}
|
|
6735
|
-
const requestId = ++aiContextUsageRequest;
|
|
6736
|
-
try {
|
|
6737
|
-
const citations = state.aiCitations.map(({ chapterId, chapterTitle, startLine, endLine, text }) => ({ chapterId, chapterTitle, startLine, endLine, text }));
|
|
6738
|
-
const usage = await api(`/api/works/${state.work.id}/ai-context-usage`, {
|
|
6739
|
-
method: "POST",
|
|
6740
|
-
body: {
|
|
6741
|
-
modelId,
|
|
6742
|
-
taskType: requestScope.taskType,
|
|
6743
|
-
scope: requestScope.scope,
|
|
6744
|
-
instruction: aiPromptText(),
|
|
6745
|
-
citations,
|
|
6746
|
-
conversationId: state.aiConversationId || undefined
|
|
6747
|
-
}
|
|
6748
|
-
});
|
|
6749
|
-
if (requestId === aiContextUsageRequest) setAiContextMeter(usage);
|
|
6750
|
-
} catch {
|
|
6751
|
-
if (requestId === aiContextUsageRequest) setAiContextMeter(null);
|
|
6752
|
-
}
|
|
6753
|
-
}
|
|
6754
|
-
|
|
6755
6964
|
async function loadAiReferences() {
|
|
6756
6965
|
const workId = state.work?.id;
|
|
6757
6966
|
if (!workId) return;
|
|
@@ -6784,7 +6993,7 @@ async function ensureAiReferencesLoaded() {
|
|
|
6784
6993
|
|
|
6785
6994
|
function field(name, label, type = "text", value = "", options = []) {
|
|
6786
6995
|
if (type === "textarea") return `<label>${esc(label)}<textarea name="${esc(name)}">${esc(value)}</textarea></label>`;
|
|
6787
|
-
if (type === "markdown") return `<div class="form-field markdown-editor-field" data-vditor-editor-field><span>${esc(label)}</span><div class="vditor-editor-host" data-vditor-editor data-placeholder="${esc(options.placeholder ?? `在这里编辑${label}`)}" aria-label="${esc(label)} Markdown 编辑器"></div><textarea class="hidden" name="${esc(name)}" data-vditor-value maxlength="200000" aria-label="${esc(label)} Markdown 原文" ${options.readOnly ? "readonly" : ""}>${esc(value)}</textarea></div>`;
|
|
6996
|
+
if (type === "markdown") return `<div class="form-field markdown-editor-field" data-vditor-editor-field><span>${esc(label)}</span><div class="vditor-editor-host" data-vditor-editor data-attachment-module="${esc(options.attachmentModule ?? "settings")}" data-placeholder="${esc(options.placeholder ?? `在这里编辑${label}`)}" aria-label="${esc(label)} Markdown 编辑器"></div><textarea class="hidden" name="${esc(name)}" data-vditor-value maxlength="200000" aria-label="${esc(label)} Markdown 原文" ${options.readOnly ? "readonly" : ""}>${esc(value)}</textarea></div>`;
|
|
6788
6997
|
if (type === "item-list") {
|
|
6789
6998
|
const values = Array.isArray(value) && value.length ? value : [""];
|
|
6790
6999
|
return `<div class="form-field item-list-field"><span>${esc(label)}</span><div class="item-list-rows" data-item-list-rows data-name="${esc(name)}" data-label="${esc(label)}">${values.map((item) => `<div class="item-list-row"><input name="${esc(name)}" value="${esc(item)}" aria-label="${esc(label)}"><button type="button" data-item-list-remove aria-label="删除此条">删除</button></div>`).join("")}</div><button class="item-list-add" type="button" data-item-list-add>添加一条</button></div>`;
|
|
@@ -7470,10 +7679,10 @@ function markdownImageLabel(file, fallback = "图片附件") {
|
|
|
7470
7679
|
return String(file?.name ?? "").replace(/[\[\]\r\n]/gu, "").trim() || fallback;
|
|
7471
7680
|
}
|
|
7472
7681
|
|
|
7473
|
-
async function uploadMarkdownAttachment(file) {
|
|
7682
|
+
async function uploadMarkdownAttachment(file, module = "settings") {
|
|
7474
7683
|
const body = new FormData();
|
|
7475
7684
|
body.append("file", file);
|
|
7476
|
-
const attachment = await api(`/api/works/${state.work.id}/attachments`, { method: "POST", body });
|
|
7685
|
+
const attachment = await api(`/api/works/${state.work.id}/attachments?module=${encodeURIComponent(module)}`, { method: "POST", body });
|
|
7477
7686
|
if (!attachment.deduplicated) markdownEditorPendingAttachments.push(String(attachment.id));
|
|
7478
7687
|
return { attachment, imageLabel: markdownImageLabel(file) };
|
|
7479
7688
|
}
|
|
@@ -7504,7 +7713,7 @@ function createVditorUploadHandler(uploadAttachment, getEditor) {
|
|
|
7504
7713
|
};
|
|
7505
7714
|
}
|
|
7506
7715
|
|
|
7507
|
-
function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment =
|
|
7716
|
+
function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment = null, attachmentModule = "settings", placeholder = "", readOnly = false, width = "auto" } = {}) {
|
|
7508
7717
|
if (!window.Vditor) {
|
|
7509
7718
|
toast("Markdown 编辑器资源加载失败,请刷新页面后重试", "error");
|
|
7510
7719
|
return null;
|
|
@@ -7535,7 +7744,7 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
|
|
|
7535
7744
|
accept: "image/*",
|
|
7536
7745
|
max: 10 * 1024 * 1024,
|
|
7537
7746
|
multiple: true,
|
|
7538
|
-
handler: createVditorUploadHandler(uploadAttachment, () => editor)
|
|
7747
|
+
handler: createVditorUploadHandler(uploadAttachment ?? ((file) => uploadMarkdownAttachment(file, attachmentModule)), () => editor)
|
|
7539
7748
|
},
|
|
7540
7749
|
input: (markdown) => {
|
|
7541
7750
|
normalizeVditorAttachmentImages(editor);
|
|
@@ -7764,6 +7973,7 @@ function bindVditorEditors(container) {
|
|
|
7764
7973
|
if (valueField) valueField.value = markdown;
|
|
7765
7974
|
markEntityEditorDirty();
|
|
7766
7975
|
},
|
|
7976
|
+
attachmentModule: host.dataset.attachmentModule ?? "settings",
|
|
7767
7977
|
placeholder: host.dataset.placeholder ?? "",
|
|
7768
7978
|
readOnly: Boolean(valueField?.readOnly)
|
|
7769
7979
|
});
|
|
@@ -7779,7 +7989,7 @@ function characterSectionImageLabel(file, fallback = "图片附件") {
|
|
|
7779
7989
|
async function uploadCharacterSectionAttachment(file) {
|
|
7780
7990
|
const body = new FormData();
|
|
7781
7991
|
body.append("file", file);
|
|
7782
|
-
const attachment = await api(`/api/works/${state.work.id}/attachments`, { method: "POST", body });
|
|
7992
|
+
const attachment = await api(`/api/works/${state.work.id}/attachments?module=characters`, { method: "POST", body });
|
|
7783
7993
|
if (!attachment.deduplicated) characterSectionPendingAttachments.push(String(attachment.id));
|
|
7784
7994
|
return {
|
|
7785
7995
|
attachment,
|
|
@@ -7852,6 +8062,7 @@ async function openKnowledgeSectionEditor(index = null) {
|
|
|
7852
8062
|
host.querySelectorAll("input, textarea").forEach((control) => control.addEventListener("input", () => { knowledgeSectionEditorDirty = true; }));
|
|
7853
8063
|
knowledgeSectionVditor = createVditorEditor($("#knowledge-section-markdown"), section?.contentMarkdown ?? "", {
|
|
7854
8064
|
onInput: () => { knowledgeSectionEditorDirty = true; },
|
|
8065
|
+
attachmentModule: knowledgeEditorKind === "race" ? "races" : "organizations",
|
|
7855
8066
|
placeholder: "从这里开始写 Markdown 设定…",
|
|
7856
8067
|
width: "100%"
|
|
7857
8068
|
});
|
|
@@ -9026,16 +9237,30 @@ function openProviderDialog(item) {
|
|
|
9026
9237
|
|
|
9027
9238
|
function openModelDialog(providerId, item = null) {
|
|
9028
9239
|
const values = modelFormValues(item);
|
|
9240
|
+
const contextWindowField = `<div class="form-field model-context-window-field"><label for="model-context-window">模型上下文令牌总量<input id="model-context-window" name="contextWindow" type="number" value="${esc(values.contextWindow)}" min="${MIN_MODEL_CONTEXT_WINDOW}" max="2000000" step="1" required aria-describedby="model-context-window-hint"></label><small id="model-context-window-hint" class="model-context-window-hint" hidden>低于 128K 的模型在小说创作场景不太适用,建议使用支持更长上下文的模型。</small></div>`;
|
|
9029
9241
|
const temperatureField = `<div class="form-field model-temperature-field"><label for="model-temperature">默认温度<input id="model-temperature" name="temperature" type="number" value="${esc(values.temperature)}" step="any" aria-describedby="model-temperature-hint"></label><small id="model-temperature-hint" class="model-temperature-hint" hidden>Kimi 模型必须设置温度为 1。</small></div>`;
|
|
9030
|
-
|
|
9242
|
+
const connectionTest = item && item.providerStatus === "enabled"
|
|
9243
|
+
? `<section class="model-connection-test">
|
|
9244
|
+
<div><strong>模型连接测试</strong><p>使用当前已保存的模型标识符和供应商凭据发起最小请求。</p></div>
|
|
9245
|
+
<button class="ghost-button" type="button" data-test-model="${esc(item.id)}">测试连接</button>
|
|
9246
|
+
</section>`
|
|
9247
|
+
: "";
|
|
9248
|
+
openDialog(item ? "编辑模型" : "添加模型", field("displayName", "显示名称", "text", values.displayName) + field("modelId", "模型标识符", "text", values.modelId) + field("purposes", "支持用途(可多选)", "chips", values.purposes, MODEL_PURPOSE_OPTIONS) + contextWindowField + temperatureField + field("maxTokens", "默认最大输出令牌数", "number", values.maxTokens) + field("thinkingEnabled", "开启思考模式(供应商需支持相应参数)", "checkbox", values.thinkingEnabled) + field("enabled", "启用模型", "checkbox", values.enabled) + connectionTest, async (form) => {
|
|
9031
9249
|
const body = modelPayload({ displayName: form.get("displayName"), modelId: form.get("modelId"), purposes: form.getAll("purposes"), contextWindow: form.get("contextWindow"), temperature: form.get("temperature"), maxTokens: form.get("maxTokens"), thinkingEnabled: form.get("thinkingEnabled") === "on", enabled: form.get("enabled") === "on" }, item?.preset);
|
|
9032
9250
|
await api(item ? `/api/models/${item.id}` : `/api/providers/${providerId}/models`, { method: item ? "PATCH" : "POST", body });
|
|
9033
9251
|
await renderPlatformAiConfig();
|
|
9034
9252
|
await loadModels();
|
|
9035
9253
|
}, item ? "模型配置" : "供应商模型");
|
|
9036
9254
|
const modelIdInput = $("#dialog-fields input[name='modelId']");
|
|
9255
|
+
const contextWindowInput = $("#model-context-window");
|
|
9256
|
+
const contextWindowHint = $("#model-context-window-hint");
|
|
9037
9257
|
const temperatureInput = $("#dialog-fields input[name='temperature']");
|
|
9038
9258
|
const temperatureHint = $("#model-temperature-hint");
|
|
9259
|
+
const syncModelContextWindowGuidance = () => {
|
|
9260
|
+
const guidance = modelContextWindowGuidance(contextWindowInput.value);
|
|
9261
|
+
contextWindowInput.setCustomValidity(guidance.belowMinimum ? "模型上下文不能低于 32K(32768 Token)。" : "");
|
|
9262
|
+
contextWindowHint.hidden = !guidance.showRecommendation;
|
|
9263
|
+
};
|
|
9039
9264
|
const syncKimiTemperature = () => {
|
|
9040
9265
|
const isKimi = isKimiModelId(modelIdInput.value);
|
|
9041
9266
|
temperatureHint.hidden = !isKimi;
|
|
@@ -9044,13 +9269,31 @@ function openModelDialog(providerId, item = null) {
|
|
|
9044
9269
|
if (isKimiModelId(modelIdInput.value)) temperatureInput.value = "1";
|
|
9045
9270
|
syncKimiTemperature();
|
|
9046
9271
|
});
|
|
9272
|
+
contextWindowInput.addEventListener("input", syncModelContextWindowGuidance);
|
|
9273
|
+
$("#dialog-fields [data-test-model]")?.addEventListener("click", async (event) => {
|
|
9274
|
+
const button = event.currentTarget;
|
|
9275
|
+
button.disabled = true;
|
|
9276
|
+
button.textContent = "测试中";
|
|
9277
|
+
try {
|
|
9278
|
+
const result = await api(`/api/models/${button.dataset.testModel}/test`, { method: "POST", body: {} });
|
|
9279
|
+
toast(result.ok ? "模型连接测试成功" : `模型连接失败:${result.error}`, result.ok ? "info" : "error");
|
|
9280
|
+
await renderPlatformAiConfig();
|
|
9281
|
+
await loadModels();
|
|
9282
|
+
} catch (error) {
|
|
9283
|
+
toast(`模型连接测试失败:${error.message}`, "error");
|
|
9284
|
+
} finally {
|
|
9285
|
+
button.disabled = false;
|
|
9286
|
+
button.textContent = "测试连接";
|
|
9287
|
+
}
|
|
9288
|
+
});
|
|
9289
|
+
syncModelContextWindowGuidance();
|
|
9047
9290
|
syncKimiTemperature();
|
|
9048
9291
|
}
|
|
9049
9292
|
|
|
9050
9293
|
async function sendAi() {
|
|
9051
9294
|
if (!state.work) return toast("请先选择作品", "error");
|
|
9052
9295
|
try {
|
|
9053
|
-
await
|
|
9296
|
+
await ensureAiModelsLoaded();
|
|
9054
9297
|
} catch (error) {
|
|
9055
9298
|
return toast(`创作助手加载失败:${error.message}`, "error");
|
|
9056
9299
|
}
|
|
@@ -9063,30 +9306,18 @@ async function sendAi() {
|
|
|
9063
9306
|
const { taskType, scope, selection } = requestScope;
|
|
9064
9307
|
if (taskType === "polish" && !selection) return toast("请先在正文中选中一段文本", "error");
|
|
9065
9308
|
const citations = state.aiCitations.map(({ chapterId, chapterTitle, startLine, endLine, text }) => ({ chapterId, chapterTitle, startLine, endLine, text }));
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
$("#ai-send").textContent = "检查上下文";
|
|
9309
|
+
let persistedUserMessage = null;
|
|
9310
|
+
if (taskType !== "chat") {
|
|
9069
9311
|
try {
|
|
9070
|
-
|
|
9071
|
-
if (!mayContinue) return;
|
|
9312
|
+
persistedUserMessage = await persistAiConversationMessage("user", instruction, citations);
|
|
9072
9313
|
} catch (error) {
|
|
9073
|
-
toast(
|
|
9074
|
-
return;
|
|
9075
|
-
} finally {
|
|
9076
|
-
$("#ai-send").disabled = false;
|
|
9077
|
-
$("#ai-send").textContent = "发送";
|
|
9314
|
+
return toast(`对话记录创建失败:${error.message}`, "error");
|
|
9078
9315
|
}
|
|
9316
|
+
state.aiPromptSent = true;
|
|
9317
|
+
renderAiQuickActions();
|
|
9318
|
+
appendMessage("user", instruction, citations, persistedUserMessage.createdAt, {}, persistedUserMessage.id);
|
|
9319
|
+
clearAiPromptComposer();
|
|
9079
9320
|
}
|
|
9080
|
-
let persistedUserMessage;
|
|
9081
|
-
try {
|
|
9082
|
-
persistedUserMessage = await persistAiConversationMessage("user", instruction, citations);
|
|
9083
|
-
} catch (error) {
|
|
9084
|
-
return toast(`对话记录创建失败:${error.message}`, "error");
|
|
9085
|
-
}
|
|
9086
|
-
state.aiPromptSent = true;
|
|
9087
|
-
renderAiQuickActions();
|
|
9088
|
-
appendMessage("user", instruction, citations, persistedUserMessage.createdAt, {}, persistedUserMessage.id);
|
|
9089
|
-
clearAiPromptComposer();
|
|
9090
9321
|
$("#ai-send").disabled = true;
|
|
9091
9322
|
$("#ai-send").textContent = "发送中";
|
|
9092
9323
|
try {
|
|
@@ -9096,7 +9327,8 @@ async function sendAi() {
|
|
|
9096
9327
|
let persistedStreamMessage = null;
|
|
9097
9328
|
let suggestion = null;
|
|
9098
9329
|
if (taskType === "chat") {
|
|
9099
|
-
const streamed = await streamChat({ instruction, scope, modelId, citations, conversationId: state.aiConversationId
|
|
9330
|
+
const streamed = await streamChat({ instruction, scope, modelId, citations, conversationId: state.aiConversationId || undefined });
|
|
9331
|
+
if (streamed.action === "warn") return;
|
|
9100
9332
|
assistantContent = streamed.content;
|
|
9101
9333
|
assistantMessage = streamed.message;
|
|
9102
9334
|
assistantMetadata = streamed.metadata;
|
|
@@ -9104,11 +9336,18 @@ async function sendAi() {
|
|
|
9104
9336
|
applyAiConversationTitle(streamed.conversationTitle);
|
|
9105
9337
|
} else {
|
|
9106
9338
|
suggestion = await api(`/api/works/${state.work.id}/suggestions`, { method: "POST", body: { taskType, instruction, scope, modelId, citations } });
|
|
9339
|
+
setAiContextMeter(suggestion.contextUsage);
|
|
9107
9340
|
assistantContent = suggestion.content;
|
|
9108
9341
|
assistantMetadata = { modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens, cacheHitPercent: suggestion.cacheHitPercent };
|
|
9109
9342
|
}
|
|
9110
9343
|
try {
|
|
9111
9344
|
if (persistedStreamMessage) {
|
|
9345
|
+
updateAiConversationSummaryFromMessage({
|
|
9346
|
+
conversationId: state.aiConversationId,
|
|
9347
|
+
role: "assistant",
|
|
9348
|
+
content: assistantContent,
|
|
9349
|
+
createdAt: persistedStreamMessage.createdAt
|
|
9350
|
+
});
|
|
9112
9351
|
updateMessageCreatedAt(assistantMessage, persistedStreamMessage.createdAt);
|
|
9113
9352
|
attachMessageIdentity(assistantMessage, persistedStreamMessage.id);
|
|
9114
9353
|
} else {
|
|
@@ -9138,13 +9377,19 @@ async function streamChat(body) {
|
|
|
9138
9377
|
message.className = "assistant-message is-streaming";
|
|
9139
9378
|
message.dataset.testid = "ai-stream-message";
|
|
9140
9379
|
message.innerHTML = '<div class="message-body" data-testid="ai-stream-content" aria-live="polite" aria-busy="true"></div><div class="message-meta">正在连接模型流……</div>';
|
|
9141
|
-
attachMessageHeading(message, "助手 · 正在生成");
|
|
9142
|
-
$("#ai-feed").append(message);
|
|
9143
|
-
scrollAiFeedToBottom();
|
|
9144
9380
|
const content = message.querySelector(".message-body");
|
|
9145
9381
|
const meta = message.querySelector(".message-meta");
|
|
9382
|
+
let messageMounted = false;
|
|
9383
|
+
const mountAssistantMessage = () => {
|
|
9384
|
+
if (messageMounted) return;
|
|
9385
|
+
attachMessageHeading(message, "助手 · 正在生成");
|
|
9386
|
+
$("#ai-feed").append(message);
|
|
9387
|
+
messageMounted = true;
|
|
9388
|
+
scrollAiFeedToBottom();
|
|
9389
|
+
};
|
|
9146
9390
|
const typewriter = createStreamTypewriter({
|
|
9147
9391
|
onRender: (text, progress) => {
|
|
9392
|
+
mountAssistantMessage();
|
|
9148
9393
|
content.innerHTML = renderMarkdown(text);
|
|
9149
9394
|
const receivedCharacters = progress.visibleCharacters + progress.pendingCharacters;
|
|
9150
9395
|
meta.textContent = progress.pendingCharacters > 0
|
|
@@ -9160,6 +9405,8 @@ async function streamChat(body) {
|
|
|
9160
9405
|
let persistedMessageId = null;
|
|
9161
9406
|
let persistedMessageCreatedAt = null;
|
|
9162
9407
|
let conversationTitle = null;
|
|
9408
|
+
let persistedUserMessage = null;
|
|
9409
|
+
let contextAction = "ready";
|
|
9163
9410
|
let finalAnswerStarted = false;
|
|
9164
9411
|
const processStartedAt = Date.now();
|
|
9165
9412
|
const elapsedProcessTime = () => Math.max(0, Date.now() - processStartedAt);
|
|
@@ -9186,7 +9433,35 @@ async function streamChat(body) {
|
|
|
9186
9433
|
}
|
|
9187
9434
|
if (!dataLines.length) return;
|
|
9188
9435
|
const payload = JSON.parse(dataLines.join("\n"));
|
|
9189
|
-
if (eventName === "
|
|
9436
|
+
if (eventName === "context") {
|
|
9437
|
+
contextAction = typeof payload.action === "string" ? payload.action : "ready";
|
|
9438
|
+
setAiContextMeter(payload.usage);
|
|
9439
|
+
if (payload.conversation?.id) {
|
|
9440
|
+
state.aiConversationId = payload.conversation.id;
|
|
9441
|
+
upsertAiConversationSummary(payload.conversation);
|
|
9442
|
+
$("#ai-conversation-title").textContent = payload.conversation.title;
|
|
9443
|
+
}
|
|
9444
|
+
if (contextAction === "warn") showAiContextWarning(payload.usage);
|
|
9445
|
+
else {
|
|
9446
|
+
hideAiContextWarning();
|
|
9447
|
+
if (contextAction === "compacted") {
|
|
9448
|
+
renderConversationCompactionDivider(payload.compaction?.compactedMessageCount, payload.conversation?.messageCount);
|
|
9449
|
+
toast("已自动整理较早对话为长期记忆并继续发送");
|
|
9450
|
+
}
|
|
9451
|
+
}
|
|
9452
|
+
} else if (eventName === "user_message") {
|
|
9453
|
+
persistedUserMessage = payload.message;
|
|
9454
|
+
if (persistedUserMessage?.id) {
|
|
9455
|
+
state.aiConversationId = persistedUserMessage.conversationId;
|
|
9456
|
+
updateAiConversationSummaryFromMessage(persistedUserMessage);
|
|
9457
|
+
state.aiPromptSent = true;
|
|
9458
|
+
renderAiQuickActions();
|
|
9459
|
+
appendMessage("user", persistedUserMessage.content, persistedUserMessage.citations, persistedUserMessage.createdAt, {}, persistedUserMessage.id);
|
|
9460
|
+
clearAiPromptComposer();
|
|
9461
|
+
mountAssistantMessage();
|
|
9462
|
+
}
|
|
9463
|
+
} else if (eventName === "delta") {
|
|
9464
|
+
mountAssistantMessage();
|
|
9190
9465
|
const firstFinalDelta = streamedText.length === 0;
|
|
9191
9466
|
const delta = typeof payload.delta === "string" ? payload.delta : "";
|
|
9192
9467
|
streamedText += delta;
|
|
@@ -9195,6 +9470,7 @@ async function streamChat(body) {
|
|
|
9195
9470
|
if (firstFinalDelta && processSteps.length) renderAiProcessSteps(message, processSteps, true, elapsedProcessTime());
|
|
9196
9471
|
meta.textContent = "正在生成回复……";
|
|
9197
9472
|
} else if (eventName === "process_step") {
|
|
9473
|
+
mountAssistantMessage();
|
|
9198
9474
|
const step = { ...payload };
|
|
9199
9475
|
const append = step.append === true;
|
|
9200
9476
|
delete step.append;
|
|
@@ -9202,9 +9478,14 @@ async function streamChat(body) {
|
|
|
9202
9478
|
if (existing && typeof step.content === "string") existing.content += step.content;
|
|
9203
9479
|
else processSteps.push(step);
|
|
9204
9480
|
renderAiProcessSteps(message, processSteps, finalAnswerStarted, elapsedProcessTime());
|
|
9205
|
-
meta.textContent = step.type === "thinking"
|
|
9481
|
+
meta.textContent = step.type === "thinking"
|
|
9482
|
+
? `正在思考 · 第 ${Number(step.round) || 1} 轮`
|
|
9483
|
+
: step.type === "context_compaction"
|
|
9484
|
+
? `已压缩第 ${Number(step.round) || 1} 轮工具上下文`
|
|
9485
|
+
: `正在处理第 ${Number(step.round) || 1} 轮中间结果`;
|
|
9206
9486
|
scrollAiFeedToBottom();
|
|
9207
9487
|
} else if (eventName === "tool_call") {
|
|
9488
|
+
mountAssistantMessage();
|
|
9208
9489
|
const toolCall = { ...payload };
|
|
9209
9490
|
const round = toolCall.round;
|
|
9210
9491
|
delete toolCall.round;
|
|
@@ -9213,10 +9494,15 @@ async function streamChat(body) {
|
|
|
9213
9494
|
renderAiProcessSteps(message, processSteps, finalAnswerStarted, elapsedProcessTime());
|
|
9214
9495
|
meta.textContent = `已调用 ${toolCalls.length} 个工具,正在等待模型处理结果`;
|
|
9215
9496
|
scrollAiFeedToBottom();
|
|
9497
|
+
} else if (eventName === "context_compacted") {
|
|
9498
|
+
setAiContextMeter(payload.contextUsage);
|
|
9499
|
+
if (messageMounted) meta.textContent = "已压缩工具上下文,正在继续生成";
|
|
9216
9500
|
} else if (eventName === "complete") {
|
|
9501
|
+
mountAssistantMessage();
|
|
9217
9502
|
persistedMessageId = typeof payload.messageId === "string" ? payload.messageId : null;
|
|
9218
9503
|
persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
|
|
9219
9504
|
conversationTitle = typeof payload.conversationTitle === "string" ? payload.conversationTitle : null;
|
|
9505
|
+
setAiContextMeter(payload.contextUsage);
|
|
9220
9506
|
await typewriter.finish();
|
|
9221
9507
|
message.classList.remove("is-streaming");
|
|
9222
9508
|
content.setAttribute("aria-busy", "false");
|
|
@@ -9244,8 +9530,9 @@ async function streamChat(body) {
|
|
|
9244
9530
|
if (buffer.trim()) await consume(buffer);
|
|
9245
9531
|
await typewriter.finish();
|
|
9246
9532
|
if (streamError) throw streamError;
|
|
9247
|
-
return { content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle };
|
|
9533
|
+
return { action: contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, userMessage: persistedUserMessage };
|
|
9248
9534
|
} catch (error) {
|
|
9535
|
+
mountAssistantMessage();
|
|
9249
9536
|
typewriter.reveal();
|
|
9250
9537
|
message.classList.remove("is-streaming");
|
|
9251
9538
|
content.setAttribute("aria-busy", "false");
|
|
@@ -9265,7 +9552,15 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
9265
9552
|
? `<p class="ai-error-text">${esc(text)}</p>`
|
|
9266
9553
|
: renderMarkdown(text);
|
|
9267
9554
|
message.innerHTML = `<div class="message-body">${messageBody}</div>`;
|
|
9268
|
-
attachMessageHeading(message, role === "user" ? "作者" : "助手", createdAt ?? undefined);
|
|
9555
|
+
const heading = attachMessageHeading(message, role === "user" ? "作者" : "助手", createdAt ?? undefined);
|
|
9556
|
+
if (isFailure) {
|
|
9557
|
+
message.dataset.status = "failed";
|
|
9558
|
+
const failureBadge = document.createElement("strong");
|
|
9559
|
+
failureBadge.className = "ai-message-status is-error";
|
|
9560
|
+
failureBadge.textContent = "失败";
|
|
9561
|
+
failureBadge.setAttribute("aria-label", "消息状态:失败");
|
|
9562
|
+
heading.firstElementChild?.append(failureBadge);
|
|
9563
|
+
}
|
|
9269
9564
|
if (citations.length) {
|
|
9270
9565
|
const references = document.createElement("div");
|
|
9271
9566
|
references.className = "message-citations";
|
|
@@ -10169,6 +10464,7 @@ $("#register-form").addEventListener("submit", async (event) => {
|
|
|
10169
10464
|
username: form.get("username"),
|
|
10170
10465
|
password: form.get("password"),
|
|
10171
10466
|
passwordConfirmation: form.get("passwordConfirmation"),
|
|
10467
|
+
setupToken: form.get("setupToken") || undefined,
|
|
10172
10468
|
captchaId: form.get("captchaId"),
|
|
10173
10469
|
captchaAnswer: form.get("captchaAnswer")
|
|
10174
10470
|
}
|
|
@@ -10238,6 +10534,7 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
10238
10534
|
timeline: Number($("#page-size-timeline").value),
|
|
10239
10535
|
outlines: Number($("#page-size-outlines").value),
|
|
10240
10536
|
relationships: Number($("#page-size-relationships").value),
|
|
10537
|
+
comments: Number($("#page-size-comments").value),
|
|
10241
10538
|
reviews: Number($("#page-size-reviews").value),
|
|
10242
10539
|
analysisTasks: Number($("#page-size-analysis-tasks").value),
|
|
10243
10540
|
fileVersions: Number($("#page-size-file-versions").value)
|
|
@@ -10383,10 +10680,14 @@ $("#chapter-content").addEventListener("input", (event) => {
|
|
|
10383
10680
|
scheduleChapterAutoSave();
|
|
10384
10681
|
clearChapterLineSelection();
|
|
10385
10682
|
scheduleChapterLineNumbers();
|
|
10386
|
-
|
|
10683
|
+
setAiContextMeter(null);
|
|
10387
10684
|
});
|
|
10388
|
-
$("#chapter-content").addEventListener("select",
|
|
10685
|
+
$("#chapter-content").addEventListener("select", () => setAiContextMeter(null));
|
|
10389
10686
|
$("#chapter-content").addEventListener("scroll", syncChapterLineNumberScroll);
|
|
10687
|
+
$("#chapter-content").addEventListener("contextmenu", (event) => {
|
|
10688
|
+
if (!state.chapter) return;
|
|
10689
|
+
showLineCitationMenu(event, lineIndexAtPointer(event.clientY));
|
|
10690
|
+
});
|
|
10390
10691
|
$("#chapter-line-numbers-inner").addEventListener("pointerdown", (event) => {
|
|
10391
10692
|
const row = event.target.closest(".chapter-line-number");
|
|
10392
10693
|
if (!row || event.button !== 0) return;
|
|
@@ -10466,7 +10767,7 @@ $("#module-more-button").addEventListener("click", () => setModuleNavExpanded(!m
|
|
|
10466
10767
|
$("#module-create-button").addEventListener("click", () => ({ drafts: openDraftDialog, settings: openSettingEditor, characters: openCharacterEditor, races: openRaceDialog, organizations: openOrganizationDialog, timeline: openTimelineDialog, outlines: openForeshadowDialog, relationships: openRelationshipDialog, reviews: openReviewDialog, tasks: openTaskDialog })[state.module]?.());
|
|
10467
10768
|
$("#ai-prompt").addEventListener("input", async () => {
|
|
10468
10769
|
updateAiMentionMenu();
|
|
10469
|
-
|
|
10770
|
+
setAiContextMeter(null);
|
|
10470
10771
|
if (!findAiMention(aiPromptTextBeforeCursor())) return;
|
|
10471
10772
|
try {
|
|
10472
10773
|
await ensureAiReferencesLoaded();
|
|
@@ -10481,9 +10782,9 @@ $("#ai-prompt").addEventListener("focus", () => {
|
|
|
10481
10782
|
$("#ai-model").addEventListener("focus", () => {
|
|
10482
10783
|
ensureAiModelsLoaded().catch((error) => toast(`模型加载失败:${error.message}`, "error"));
|
|
10483
10784
|
});
|
|
10484
|
-
$("#ai-model").addEventListener("change",
|
|
10485
|
-
$("#ai-task").addEventListener("change",
|
|
10486
|
-
$("#ai-scope").addEventListener("change",
|
|
10785
|
+
$("#ai-model").addEventListener("change", () => setAiContextMeter(null));
|
|
10786
|
+
$("#ai-task").addEventListener("change", () => setAiContextMeter(null));
|
|
10787
|
+
$("#ai-scope").addEventListener("change", () => setAiContextMeter(null));
|
|
10487
10788
|
$("#ai-mention-menu").addEventListener("click", (event) => {
|
|
10488
10789
|
const button = event.target.closest("[data-ai-reference-id]");
|
|
10489
10790
|
if (button) selectAiMention(button);
|
|
@@ -10686,7 +10987,15 @@ $("#ai-context-compact").addEventListener("click", async () => {
|
|
|
10686
10987
|
});
|
|
10687
10988
|
hideAiContextWarning();
|
|
10688
10989
|
toast(result.changed ? `已整理 ${result.compactedMessageCount} 条较早消息为长期记忆` : "当前没有需要整理的较早消息");
|
|
10689
|
-
|
|
10990
|
+
setAiContextMeter(result.usage);
|
|
10991
|
+
if (result.changed) {
|
|
10992
|
+
const current = state.aiConversations.find((conversation) => conversation.id === conversationId);
|
|
10993
|
+
if (current) {
|
|
10994
|
+
current.compactedMessageCount = result.compactedMessageCount;
|
|
10995
|
+
current.hasCompactedSummary = true;
|
|
10996
|
+
}
|
|
10997
|
+
renderConversationCompactionDivider(result.compactedMessageCount, current?.messageCount);
|
|
10998
|
+
}
|
|
10690
10999
|
} catch (error) {
|
|
10691
11000
|
toast(`上下文压缩失败:${error.message}`, "error");
|
|
10692
11001
|
} finally {
|
|
@@ -10712,6 +11021,30 @@ $("#ai-history-toggle").addEventListener("click", async () => {
|
|
|
10712
11021
|
toast(`对话历史加载失败:${error.message}`, "error");
|
|
10713
11022
|
}
|
|
10714
11023
|
});
|
|
11024
|
+
$("#ai-history-previous").addEventListener("click", async () => {
|
|
11025
|
+
const targetPage = aiConversationHistoryPage.page - 1;
|
|
11026
|
+
if (targetPage < 1) return;
|
|
11027
|
+
$("#ai-history-previous").disabled = true;
|
|
11028
|
+
$("#ai-history-next").disabled = true;
|
|
11029
|
+
try {
|
|
11030
|
+
await loadAiConversations(targetPage);
|
|
11031
|
+
} catch (error) {
|
|
11032
|
+
renderAiConversationHistory();
|
|
11033
|
+
toast(`对话历史加载失败:${error.message}`, "error");
|
|
11034
|
+
}
|
|
11035
|
+
});
|
|
11036
|
+
$("#ai-history-next").addEventListener("click", async () => {
|
|
11037
|
+
const targetPage = aiConversationHistoryPage.nextPage;
|
|
11038
|
+
if (!targetPage) return;
|
|
11039
|
+
$("#ai-history-previous").disabled = true;
|
|
11040
|
+
$("#ai-history-next").disabled = true;
|
|
11041
|
+
try {
|
|
11042
|
+
await loadAiConversations(targetPage);
|
|
11043
|
+
} catch (error) {
|
|
11044
|
+
renderAiConversationHistory();
|
|
11045
|
+
toast(`对话历史加载失败:${error.message}`, "error");
|
|
11046
|
+
}
|
|
11047
|
+
});
|
|
10715
11048
|
$("#ai-history-close").addEventListener("click", () => setAiHistoryVisible(false));
|
|
10716
11049
|
$("#ai-history-dialog").addEventListener("close", () => {
|
|
10717
11050
|
$("#ai-history-toggle").setAttribute("aria-expanded", "false");
|