@musnows/scriverse 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-tool-results.js +177 -0
- package/dist/ai-tool-results.js.map +1 -0
- package/dist/ai.js +412 -122
- package/dist/ai.js.map +1 -1
- package/dist/app.js +81 -30
- package/dist/app.js.map +1 -1
- package/dist/cli-contract.js +5 -5
- package/dist/cli-core.js +8 -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/public/ai-context-meter.js +7 -0
- package/dist/public/app.js +455 -206
- package/dist/public/entity-version.js +2 -2
- package/dist/public/index.html +17 -10
- 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 +30 -10
- package/dist/public/work-permissions.d.ts +1 -1
- package/dist/public/work-permissions.js +1 -1
- package/dist/store.js +58 -6
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +4 -5
- 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
|
@@ -5,18 +5,18 @@ import { buildAiReferenceScope, findAiMention, listAiMentionOptions } from "/ai-
|
|
|
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);
|
|
@@ -908,6 +913,7 @@ const moduleListPages = {
|
|
|
908
913
|
outlinePlans: 1,
|
|
909
914
|
foreshadows: 1,
|
|
910
915
|
relationships: 1,
|
|
916
|
+
comments: 1,
|
|
911
917
|
reviews: 1
|
|
912
918
|
};
|
|
913
919
|
const characterFilters = { raceIds: [], organizationIds: [] };
|
|
@@ -1244,7 +1250,7 @@ function renderAiCitations() {
|
|
|
1244
1250
|
card.append(main, remove, fullText);
|
|
1245
1251
|
host.append(card);
|
|
1246
1252
|
}
|
|
1247
|
-
|
|
1253
|
+
setAiContextMeter(null);
|
|
1248
1254
|
}
|
|
1249
1255
|
|
|
1250
1256
|
function aiReferenceKey(reference) {
|
|
@@ -1288,7 +1294,7 @@ function renderAiReferences() {
|
|
|
1288
1294
|
if (rendered.has(aiReferenceKey(reference))) continue;
|
|
1289
1295
|
prompt.append(createAiReferenceChip(reference), document.createTextNode(" "));
|
|
1290
1296
|
}
|
|
1291
|
-
|
|
1297
|
+
setAiContextMeter(null);
|
|
1292
1298
|
}
|
|
1293
1299
|
|
|
1294
1300
|
function renderAiQuickActions() {
|
|
@@ -1331,6 +1337,34 @@ function resetAiFeed() {
|
|
|
1331
1337
|
$("#ai-feed").innerHTML = '<div class="assistant-message"><span class="message-heading"><span>助手</span></span><div class="message-body"><p>选择章节和模型后,可以问答、续写或校对。所有引用都基于已保存正文。</p></div></div>';
|
|
1332
1338
|
}
|
|
1333
1339
|
|
|
1340
|
+
function appendAiContextCompactionDivider(kind, before = null) {
|
|
1341
|
+
const divider = document.createElement("div");
|
|
1342
|
+
divider.className = "ai-context-compaction-divider";
|
|
1343
|
+
divider.dataset.contextCompaction = kind;
|
|
1344
|
+
divider.dataset.testid = "ai-context-compaction-divider";
|
|
1345
|
+
divider.setAttribute("role", "separator");
|
|
1346
|
+
divider.setAttribute("aria-label", "已压缩上下文");
|
|
1347
|
+
divider.innerHTML = "<span>已压缩上下文</span>";
|
|
1348
|
+
const feed = $("#ai-feed");
|
|
1349
|
+
if (before?.parentElement === feed) feed.insertBefore(divider, before);
|
|
1350
|
+
else feed.append(divider);
|
|
1351
|
+
scrollAiFeedToBottom();
|
|
1352
|
+
return divider;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function renderConversationCompactionDivider(compactedMessageCount, totalMessageCount = null) {
|
|
1356
|
+
const feed = $("#ai-feed");
|
|
1357
|
+
feed.querySelector('[data-context-compaction="conversation"]')?.remove();
|
|
1358
|
+
const compactedCount = Math.max(0, Number(compactedMessageCount) || 0);
|
|
1359
|
+
if (compactedCount === 0) return null;
|
|
1360
|
+
const messages = [...feed.querySelectorAll("[data-message-id]")];
|
|
1361
|
+
const current = state.aiConversations.find((conversation) => conversation.id === state.aiConversationId);
|
|
1362
|
+
const totalCount = Math.max(messages.length, Number(totalMessageCount ?? current?.messageCount) || messages.length);
|
|
1363
|
+
const loadedOffset = Math.max(0, totalCount - messages.length);
|
|
1364
|
+
const boundaryIndex = Math.max(0, Math.min(messages.length, compactedCount - loadedOffset));
|
|
1365
|
+
return appendAiContextCompactionDivider("conversation", messages[boundaryIndex] ?? null);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1334
1368
|
function renderMessageCardActions(message) {
|
|
1335
1369
|
let actions = message.querySelector(".message-card-actions");
|
|
1336
1370
|
if (!actions) {
|
|
@@ -1367,7 +1401,7 @@ function renderMessageCardActions(message) {
|
|
|
1367
1401
|
fork.disabled = true;
|
|
1368
1402
|
try {
|
|
1369
1403
|
const conversation = await api(`/api/ai-conversations/${state.aiConversationId}/fork`, { method: "POST", body: { messageId: message.dataset.messageId } });
|
|
1370
|
-
|
|
1404
|
+
upsertAiConversationSummary(conversation);
|
|
1371
1405
|
await openAiConversation(conversation.id);
|
|
1372
1406
|
toast("已从所选消息创建分支对话");
|
|
1373
1407
|
} catch (error) {
|
|
@@ -1399,7 +1433,7 @@ const AI_TOOL_DISPLAY_NAMES = {
|
|
|
1399
1433
|
grep: "查询正文关键字",
|
|
1400
1434
|
search_story_entities: "搜索作品实体",
|
|
1401
1435
|
read_character_sections: "读取人物 Markdown 章节",
|
|
1402
|
-
search_drafts: "
|
|
1436
|
+
search_drafts: "搜索想法"
|
|
1403
1437
|
};
|
|
1404
1438
|
|
|
1405
1439
|
const AI_TOOL_DESCRIPTIONS = {
|
|
@@ -1559,6 +1593,17 @@ function renderAiProcessSteps(message, steps, completed, durationMs = null) {
|
|
|
1559
1593
|
const list = document.createElement("div");
|
|
1560
1594
|
list.className = "ai-process-list";
|
|
1561
1595
|
for (const step of steps) {
|
|
1596
|
+
if (step?.type === "context_compaction") {
|
|
1597
|
+
const compaction = document.createElement("div");
|
|
1598
|
+
compaction.className = "ai-process-context-compaction";
|
|
1599
|
+
compaction.dataset.testid = "ai-process-context-compaction";
|
|
1600
|
+
compaction.setAttribute("role", "separator");
|
|
1601
|
+
compaction.setAttribute("aria-label", `第 ${Number(step.round) || 1} 轮已压缩上下文`);
|
|
1602
|
+
compaction.title = `已将 ${Number(step.sourceMessageCount) || 0} 条工具上下文压缩为摘要`;
|
|
1603
|
+
compaction.innerHTML = "<span>已压缩上下文</span>";
|
|
1604
|
+
list.append(compaction);
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1562
1607
|
if (step?.type === "tool" && step.toolCall) {
|
|
1563
1608
|
const tool = document.createElement("section");
|
|
1564
1609
|
tool.className = "ai-process-step ai-process-tool-step";
|
|
@@ -1599,6 +1644,12 @@ function setAiHistoryVisible(visible) {
|
|
|
1599
1644
|
function renderAiConversationHistory() {
|
|
1600
1645
|
const host = $("#ai-history-list");
|
|
1601
1646
|
host.replaceChildren();
|
|
1647
|
+
const pagination = $("#ai-history-pagination");
|
|
1648
|
+
const showPagination = aiConversationHistoryPage.page > 1 || aiConversationHistoryPage.hasMore;
|
|
1649
|
+
pagination.classList.toggle("hidden", !showPagination);
|
|
1650
|
+
$("#ai-history-previous").disabled = aiConversationHistoryPage.page <= 1;
|
|
1651
|
+
$("#ai-history-next").disabled = !aiConversationHistoryPage.hasMore;
|
|
1652
|
+
$("#ai-history-page-label").textContent = `第 ${aiConversationHistoryPage.page} 页 · 本页 ${state.aiConversations.length} 条`;
|
|
1602
1653
|
if (!state.aiConversations.length) {
|
|
1603
1654
|
host.innerHTML = '<p class="ai-history-empty">还没有历史对话</p>';
|
|
1604
1655
|
return;
|
|
@@ -1618,6 +1669,43 @@ function renderAiConversationHistory() {
|
|
|
1618
1669
|
}
|
|
1619
1670
|
}
|
|
1620
1671
|
|
|
1672
|
+
function defaultAiConversationTitle(prompt) {
|
|
1673
|
+
const normalized = String(prompt ?? "").replace(/\s+/gu, " ").trim();
|
|
1674
|
+
return Array.from(normalized).slice(0, 15).join("") || "新对话";
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function upsertAiConversationSummary(conversation) {
|
|
1678
|
+
if (!conversation?.id) return;
|
|
1679
|
+
const current = state.aiConversations.find((item) => item.id === conversation.id);
|
|
1680
|
+
const lastMessage = Array.isArray(conversation.messages) ? conversation.messages.at(-1) : null;
|
|
1681
|
+
const summary = { ...current, ...conversation, ...(lastMessage ? { preview: lastMessage.content } : {}) };
|
|
1682
|
+
delete summary.messages;
|
|
1683
|
+
delete summary.messagesPage;
|
|
1684
|
+
if (!current && loadedAiConversationsWorkId === state.work?.id) {
|
|
1685
|
+
loadedAiConversationsWorkId = null;
|
|
1686
|
+
aiConversationHistoryPage = { page: 1, limit: aiConversationHistoryPageLimit, hasMore: false, nextPage: null };
|
|
1687
|
+
state.aiConversations = [summary];
|
|
1688
|
+
renderAiConversationHistory();
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
state.aiConversations = [summary, ...state.aiConversations.filter((item) => item.id !== summary.id)]
|
|
1692
|
+
.sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
|
|
1693
|
+
renderAiConversationHistory();
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function updateAiConversationSummaryFromMessage(message) {
|
|
1697
|
+
if (!message?.conversationId) return;
|
|
1698
|
+
const current = state.aiConversations.find((conversation) => conversation.id === message.conversationId);
|
|
1699
|
+
if (!current) return;
|
|
1700
|
+
upsertAiConversationSummary({
|
|
1701
|
+
...current,
|
|
1702
|
+
title: current.title === "新对话" && message.role === "user" ? defaultAiConversationTitle(message.content) : current.title,
|
|
1703
|
+
messageCount: Number(current.messageCount ?? 0) + 1,
|
|
1704
|
+
preview: message.content,
|
|
1705
|
+
updatedAt: message.createdAt ?? current.updatedAt
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1621
1709
|
function applyAiConversationTitle(title) {
|
|
1622
1710
|
if (!title || !state.aiConversationId) return;
|
|
1623
1711
|
const current = state.aiConversations.find((conversation) => conversation.id === state.aiConversationId);
|
|
@@ -1626,20 +1714,27 @@ function applyAiConversationTitle(title) {
|
|
|
1626
1714
|
renderAiConversationHistory();
|
|
1627
1715
|
}
|
|
1628
1716
|
|
|
1629
|
-
|
|
1717
|
+
function applyAiConversations(pageResult) {
|
|
1718
|
+
state.aiConversations = pageResult.items;
|
|
1719
|
+
aiConversationHistoryPage = {
|
|
1720
|
+
page: pageResult.page,
|
|
1721
|
+
limit: pageResult.limit,
|
|
1722
|
+
hasMore: pageResult.hasMore,
|
|
1723
|
+
nextPage: pageResult.nextPage
|
|
1724
|
+
};
|
|
1725
|
+
loadedAiConversationsWorkId = state.work?.id ?? null;
|
|
1726
|
+
renderAiConversationHistory();
|
|
1727
|
+
const current = state.aiConversations.find((conversation) => conversation.id === state.aiConversationId);
|
|
1728
|
+
if (current) $("#ai-conversation-title").textContent = current.title;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
async function loadAiConversations(page = 1) {
|
|
1630
1732
|
const workId = state.work?.id;
|
|
1631
1733
|
if (!workId) return;
|
|
1632
1734
|
const generation = workScopedUiGeneration;
|
|
1633
|
-
const conversations =
|
|
1735
|
+
const conversations = await apiPage(`/api/works/${workId}/ai-conversations`, page, aiConversationHistoryPageLimit);
|
|
1634
1736
|
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
|
-
}
|
|
1737
|
+
applyAiConversations(conversations);
|
|
1643
1738
|
}
|
|
1644
1739
|
|
|
1645
1740
|
async function ensureAiConversationsLoaded() {
|
|
@@ -1647,7 +1742,7 @@ async function ensureAiConversationsLoaded() {
|
|
|
1647
1742
|
if (!workId || loadedAiConversationsWorkId === workId) return;
|
|
1648
1743
|
if (aiConversationsLoadPromise && aiConversationsLoadWorkId === workId) return aiConversationsLoadPromise;
|
|
1649
1744
|
aiConversationsLoadWorkId = workId;
|
|
1650
|
-
aiConversationsLoadPromise = loadAiConversations(
|
|
1745
|
+
aiConversationsLoadPromise = loadAiConversations();
|
|
1651
1746
|
try {
|
|
1652
1747
|
await aiConversationsLoadPromise;
|
|
1653
1748
|
} finally {
|
|
@@ -1660,11 +1755,15 @@ async function ensureAiConversationsLoaded() {
|
|
|
1660
1755
|
|
|
1661
1756
|
async function openAiConversation(conversationId, hideHistory = true) {
|
|
1662
1757
|
const conversation = await api(`/api/ai-conversations/${conversationId}?page=1&limit=100`);
|
|
1758
|
+
upsertAiConversationSummary(conversation);
|
|
1663
1759
|
state.aiConversationId = conversation.id;
|
|
1664
1760
|
state.aiPromptSent = conversation.messages.some((message) => message.role === "user");
|
|
1665
1761
|
$("#ai-conversation-title").textContent = conversation.title;
|
|
1666
1762
|
resetAiFeed();
|
|
1667
1763
|
for (const message of conversation.messages) appendMessage(message.role, message.content, message.citations, message.createdAt, message.metadata, message.id);
|
|
1764
|
+
if (conversation.hasCompactedSummary) {
|
|
1765
|
+
renderConversationCompactionDivider(conversation.compactedMessageCount, conversation.messageCount);
|
|
1766
|
+
}
|
|
1668
1767
|
state.aiCitations = [];
|
|
1669
1768
|
state.aiReferences = [];
|
|
1670
1769
|
setAiPromptText("");
|
|
@@ -1680,14 +1779,15 @@ async function openAiConversation(conversationId, hideHistory = true) {
|
|
|
1680
1779
|
async function createNewAiConversation() {
|
|
1681
1780
|
if (!state.work) return;
|
|
1682
1781
|
const conversation = await api(`/api/works/${state.work.id}/ai-conversations`, { method: "POST", body: {} });
|
|
1782
|
+
upsertAiConversationSummary(conversation);
|
|
1683
1783
|
state.aiConversationId = conversation.id;
|
|
1684
1784
|
state.aiPromptSent = false;
|
|
1685
1785
|
$("#ai-conversation-title").textContent = conversation.title;
|
|
1686
1786
|
resetAiFeed();
|
|
1687
1787
|
hideAiContextWarning();
|
|
1788
|
+
setAiContextMeter(null);
|
|
1688
1789
|
renderAiQuickActions();
|
|
1689
1790
|
setAiHistoryVisible(false);
|
|
1690
|
-
await loadAiConversations(false);
|
|
1691
1791
|
}
|
|
1692
1792
|
|
|
1693
1793
|
async function ensureAiConversation() {
|
|
@@ -1700,7 +1800,7 @@ async function persistAiConversationMessage(role, content, citations = [], metad
|
|
|
1700
1800
|
const conversationId = await ensureAiConversation();
|
|
1701
1801
|
if (!conversationId) throw new Error("无法创建 AI 对话");
|
|
1702
1802
|
const message = await api(`/api/ai-conversations/${conversationId}/messages`, { method: "POST", body: { role, content, citations, metadata } });
|
|
1703
|
-
|
|
1803
|
+
updateAiConversationSummaryFromMessage(message);
|
|
1704
1804
|
return message;
|
|
1705
1805
|
}
|
|
1706
1806
|
|
|
@@ -1837,9 +1937,9 @@ async function createSelectedLineAnnotation(kind) {
|
|
|
1837
1937
|
if (!state.chapter || !chapterLineSelection || !canEditProse()) return;
|
|
1838
1938
|
const selection = selectedChapterLinePayload(chapterLineSelection.start, chapterLineSelection.end);
|
|
1839
1939
|
closeLineCitationMenu();
|
|
1840
|
-
const note = await inputToast(kind === "todo" ? "描述需要后续处理的事项" :
|
|
1841
|
-
title: kind === "todo" ? "添加正文待办" : "
|
|
1842
|
-
inputLabel: kind === "todo" ? "待办内容" : "
|
|
1940
|
+
const note = await inputToast(kind === "todo" ? "描述需要后续处理的事项" : `评论第 ${selection.safeStart + 1} 行正文`, {
|
|
1941
|
+
title: kind === "todo" ? "添加正文待办" : "添加正文评论",
|
|
1942
|
+
inputLabel: kind === "todo" ? "待办内容" : "评论内容",
|
|
1843
1943
|
confirmLabel: "添加",
|
|
1844
1944
|
maxLength: 2000
|
|
1845
1945
|
});
|
|
@@ -1849,61 +1949,107 @@ async function createSelectedLineAnnotation(kind) {
|
|
|
1849
1949
|
method: "POST",
|
|
1850
1950
|
body: { kind, startLine: selection.safeStart + 1, endLine: selection.safeEnd + 1, note }
|
|
1851
1951
|
});
|
|
1852
|
-
toast(kind === "todo" ? "正文待办已添加" : "
|
|
1952
|
+
toast(kind === "todo" ? "正文待办已添加" : "正文评论已添加");
|
|
1853
1953
|
await openChapterAnnotationsDialog();
|
|
1854
1954
|
} catch (error) {
|
|
1855
1955
|
toast(error.message, "error");
|
|
1856
1956
|
}
|
|
1857
1957
|
}
|
|
1858
1958
|
|
|
1859
|
-
function
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1959
|
+
function chapterAnnotationLineLabel(annotation) {
|
|
1960
|
+
return annotation.startLine === annotation.endLine
|
|
1961
|
+
? `第 ${annotation.startLine} 行`
|
|
1962
|
+
: `第 ${annotation.startLine}-${annotation.endLine} 行`;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
function chapterAnnotationStatusLabel(annotation) {
|
|
1966
|
+
if (annotation.status === "resolved") return annotation.kind === "todo" ? "已完成" : "已解决";
|
|
1967
|
+
return annotation.kind === "todo" ? "待处理" : "未解决";
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function chapterAnnotationCard(annotation, { showSource = false } = {}) {
|
|
1971
|
+
const lineLabel = chapterAnnotationLineLabel(annotation);
|
|
1972
|
+
const sourceLabel = `${annotation.volumeTitle ?? "正文"} / ${annotation.chapterTitle ?? "未命名章节"}`;
|
|
1973
|
+
const heading = showSource
|
|
1974
|
+
? `<strong class="chapter-annotation-source" title="${esc(sourceLabel)}">${esc(sourceLabel)}</strong><span class="chapter-annotation-line">${esc(lineLabel)}</span>`
|
|
1975
|
+
: `<strong>${esc(lineLabel)}</strong>`;
|
|
1976
|
+
return `<article class="chapter-annotation-card ${annotation.status === "resolved" ? "is-resolved" : ""}" data-annotation-id="${esc(annotation.id)}">
|
|
1977
|
+
<header><span class="chapter-annotation-kind">${annotation.kind === "todo" ? "待办" : "评论"}</span>${heading}<em>${esc(chapterAnnotationStatusLabel(annotation))}</em></header>
|
|
1863
1978
|
<blockquote>${esc(annotation.quote || "空白行")}</blockquote>
|
|
1864
|
-
<p>${esc(annotation.note)}</p>
|
|
1979
|
+
<p data-annotation-content>${esc(annotation.note)}</p>
|
|
1865
1980
|
<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
|
|
1981
|
+
<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>
|
|
1982
|
+
</article>`;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function bindChapterAnnotationCards(host, annotations, { refresh, locate, overlayDialog = null }) {
|
|
1986
|
+
const suspendOverlay = () => {
|
|
1987
|
+
if (overlayDialog?.open) overlayDialog.close();
|
|
1988
|
+
};
|
|
1989
|
+
const resumeOverlay = () => {
|
|
1990
|
+
if (overlayDialog && !overlayDialog.open) overlayDialog.showModal();
|
|
1991
|
+
};
|
|
1868
1992
|
host.querySelectorAll("[data-annotation-id]").forEach((card) => {
|
|
1869
|
-
const annotation =
|
|
1993
|
+
const annotation = annotations.find((item) => item.id === card.dataset.annotationId);
|
|
1870
1994
|
if (!annotation) return;
|
|
1871
|
-
card.querySelector("[data-annotation-locate]")?.addEventListener("click", () => {
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
|
|
1995
|
+
card.querySelector("[data-annotation-locate]")?.addEventListener("click", async () => {
|
|
1996
|
+
try { await locate(annotation); }
|
|
1997
|
+
catch (error) { toast(error.message, "error"); }
|
|
1875
1998
|
});
|
|
1876
1999
|
card.querySelector("[data-annotation-status]")?.addEventListener("click", async () => {
|
|
1877
2000
|
try {
|
|
1878
2001
|
await api(`/api/chapter-annotations/${encodeURIComponent(annotation.id)}`, { method: "PATCH", body: { status: annotation.status === "resolved" ? "open" : "resolved", expectedVersionNo: annotation.versionNo } });
|
|
1879
|
-
await
|
|
2002
|
+
await refresh();
|
|
1880
2003
|
} catch (error) { toast(error.message, "error"); }
|
|
1881
2004
|
});
|
|
1882
2005
|
card.querySelector("[data-annotation-edit]")?.addEventListener("click", async () => {
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
2006
|
+
suspendOverlay();
|
|
2007
|
+
const note = await inputToast(annotation.kind === "todo" ? "修改待办内容" : "修改评论内容", {
|
|
2008
|
+
title: annotation.kind === "todo" ? "编辑待办" : "编辑评论",
|
|
2009
|
+
inputLabel: annotation.kind === "todo" ? "待办内容" : "评论内容",
|
|
2010
|
+
value: annotation.note,
|
|
2011
|
+
confirmLabel: "保存",
|
|
2012
|
+
maxLength: 2000
|
|
2013
|
+
});
|
|
2014
|
+
if (!note) return resumeOverlay();
|
|
1887
2015
|
try {
|
|
1888
2016
|
await api(`/api/chapter-annotations/${encodeURIComponent(annotation.id)}`, { method: "PATCH", body: { note, expectedVersionNo: annotation.versionNo } });
|
|
1889
|
-
await
|
|
2017
|
+
await refresh();
|
|
1890
2018
|
} catch (error) { toast(error.message, "error"); }
|
|
1891
|
-
|
|
2019
|
+
resumeOverlay();
|
|
1892
2020
|
});
|
|
1893
2021
|
card.querySelector("[data-annotation-delete]")?.addEventListener("click", async () => {
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2022
|
+
suspendOverlay();
|
|
2023
|
+
const confirmed = await confirmToast("删除后不会在正文评论中显示,但历史版本仍会保留。", {
|
|
2024
|
+
title: annotation.kind === "todo" ? "删除待办" : "删除评论",
|
|
2025
|
+
confirmLabel: "确认删除"
|
|
2026
|
+
});
|
|
2027
|
+
if (!confirmed) return resumeOverlay();
|
|
1898
2028
|
try {
|
|
1899
2029
|
await api(`/api/chapter-annotations/${encodeURIComponent(annotation.id)}`, { method: "DELETE", body: { expectedVersionNo: annotation.versionNo } });
|
|
1900
|
-
await
|
|
1901
|
-
|
|
1902
|
-
|
|
2030
|
+
await refresh();
|
|
2031
|
+
} catch (error) { toast(error.message, "error"); }
|
|
2032
|
+
resumeOverlay();
|
|
1903
2033
|
});
|
|
1904
2034
|
});
|
|
1905
2035
|
}
|
|
1906
2036
|
|
|
2037
|
+
function renderChapterAnnotations() {
|
|
2038
|
+
const host = $("#chapter-annotations-list");
|
|
2039
|
+
host.innerHTML = chapterAnnotations.length
|
|
2040
|
+
? chapterAnnotations.map((annotation) => chapterAnnotationCard(annotation)).join("")
|
|
2041
|
+
: '<p class="entity-history-empty">本章还没有评论。在正文行上点击右键即可添加。</p>';
|
|
2042
|
+
bindChapterAnnotationCards(host, chapterAnnotations, {
|
|
2043
|
+
refresh: loadChapterAnnotations,
|
|
2044
|
+
locate: (annotation) => {
|
|
2045
|
+
$("#chapter-annotations-dialog").close();
|
|
2046
|
+
paintChapterLineSelection(annotation.startLine - 1, annotation.endLine - 1);
|
|
2047
|
+
selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
|
|
2048
|
+
},
|
|
2049
|
+
overlayDialog: $("#chapter-annotations-dialog")
|
|
2050
|
+
});
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1907
2053
|
async function loadChapterAnnotations() {
|
|
1908
2054
|
if (!state.chapter) return;
|
|
1909
2055
|
chapterAnnotations = await api(`/api/chapters/${encodeURIComponent(state.chapter.id)}/annotations`);
|
|
@@ -1912,8 +2058,8 @@ async function loadChapterAnnotations() {
|
|
|
1912
2058
|
|
|
1913
2059
|
async function openChapterAnnotationsDialog() {
|
|
1914
2060
|
if (!state.chapter) return;
|
|
1915
|
-
$("#chapter-annotations-meta").textContent = `《${state.chapter.title}
|
|
1916
|
-
$("#chapter-annotations-list").innerHTML = '<p class="entity-history-empty"
|
|
2061
|
+
$("#chapter-annotations-meta").textContent = `《${state.chapter.title}》的正文评论`;
|
|
2062
|
+
$("#chapter-annotations-list").innerHTML = '<p class="entity-history-empty">正在加载评论…</p>';
|
|
1917
2063
|
if (!$("#chapter-annotations-dialog").open) $("#chapter-annotations-dialog").showModal();
|
|
1918
2064
|
try {
|
|
1919
2065
|
await loadChapterAnnotations();
|
|
@@ -2182,6 +2328,10 @@ function createClientError(payload, fallbackMessage, fallbackStatus = null) {
|
|
|
2182
2328
|
error.details = source.details;
|
|
2183
2329
|
error.failure = typeof source.failure === "string" ? source.failure : undefined;
|
|
2184
2330
|
error.callId = typeof source.callId === "string" ? source.callId : undefined;
|
|
2331
|
+
error.providerName = typeof source.providerName === "string" ? source.providerName : undefined;
|
|
2332
|
+
error.providerId = typeof source.providerId === "string" ? source.providerId : undefined;
|
|
2333
|
+
error.modelId = typeof source.modelId === "string" ? source.modelId : undefined;
|
|
2334
|
+
error.modelRecordId = typeof source.modelRecordId === "string" ? source.modelRecordId : undefined;
|
|
2185
2335
|
return error;
|
|
2186
2336
|
}
|
|
2187
2337
|
|
|
@@ -2193,8 +2343,13 @@ function formatAiFailureMessage(error) {
|
|
|
2193
2343
|
const details = error?.details && typeof error.details === "object" && !Array.isArray(error.details) ? error.details : {};
|
|
2194
2344
|
const failure = typeof error?.failure === "string" ? error.failure : typeof details.failure === "string" ? details.failure : "";
|
|
2195
2345
|
const callId = typeof error?.callId === "string" ? error.callId : typeof details.callId === "string" ? details.callId : "";
|
|
2346
|
+
const providerName = typeof error?.providerName === "string" ? error.providerName : typeof details.providerName === "string" ? details.providerName : "";
|
|
2347
|
+
const providerId = typeof error?.providerId === "string" ? error.providerId : typeof details.providerId === "string" ? details.providerId : "";
|
|
2348
|
+
const modelId = typeof error?.modelId === "string" ? error.modelId : typeof details.modelId === "string" ? details.modelId : "";
|
|
2196
2349
|
if (code) lines.push(`错误码:${code}`);
|
|
2197
2350
|
if (status) lines.push(`服务端状态:HTTP ${status}`);
|
|
2351
|
+
if (providerName || providerId) lines.push(`模型供应商:${providerName || providerId}`);
|
|
2352
|
+
if (modelId) lines.push(`模型 ID:${modelId}`);
|
|
2198
2353
|
if (failure && failure !== message) lines.push(`详细原因:${failure}`);
|
|
2199
2354
|
if (callId) lines.push(`调用 ID:${callId}`);
|
|
2200
2355
|
return lines.join("\n\n");
|
|
@@ -2242,7 +2397,6 @@ function invalidateModuleRequestsAfterMutation(path, method) {
|
|
|
2242
2397
|
path.startsWith("/api/auth/")
|
|
2243
2398
|
|| path.includes("/presence")
|
|
2244
2399
|
|| path.includes("/context/prepare")
|
|
2245
|
-
|| path.includes("/ai-context-usage")
|
|
2246
2400
|
|| path.includes("/chat/stream")
|
|
2247
2401
|
) return;
|
|
2248
2402
|
|
|
@@ -2259,6 +2413,7 @@ function invalidateModuleRequestsAfterMutation(path, method) {
|
|
|
2259
2413
|
if (path.includes("/timeline")) affected.add("timeline");
|
|
2260
2414
|
if (path.includes("/outlines") || path.includes("/foreshadows")) affected.add("outlines");
|
|
2261
2415
|
if (path.includes("/relationships")) affected.add("relationships");
|
|
2416
|
+
if (path.includes("/chapter-annotations/") || /\/chapters\/[^/]+\/annotations(?:$|\?)/u.test(path)) affected.add("comments");
|
|
2262
2417
|
if (path.includes("/reviews")) affected.add("reviews");
|
|
2263
2418
|
if (path.includes("/entity-versions/")) {
|
|
2264
2419
|
if (path.includes("/draft/")) affected.add("drafts");
|
|
@@ -2523,7 +2678,7 @@ function confirmToast(message, { title = "请再次确认", confirmLabel = "确
|
|
|
2523
2678
|
});
|
|
2524
2679
|
}
|
|
2525
2680
|
|
|
2526
|
-
function inputToast(message, { title = "请输入", inputLabel = title, placeholder = "", confirmLabel = "确认", cancelLabel = "取消", maxLength = 500 } = {}) {
|
|
2681
|
+
function inputToast(message, { title = "请输入", inputLabel = title, value = "", placeholder = "", confirmLabel = "确认", cancelLabel = "取消", maxLength = 500 } = {}) {
|
|
2527
2682
|
const region = $("#toast-region");
|
|
2528
2683
|
const element = document.createElement("section");
|
|
2529
2684
|
element.className = "toast toast-confirmation toast-input-dialog";
|
|
@@ -2537,6 +2692,7 @@ function inputToast(message, { title = "请输入", inputLabel = title, placehol
|
|
|
2537
2692
|
input.className = "toast-input";
|
|
2538
2693
|
input.type = "text";
|
|
2539
2694
|
input.maxLength = maxLength;
|
|
2695
|
+
input.value = value;
|
|
2540
2696
|
input.placeholder = placeholder;
|
|
2541
2697
|
input.setAttribute("aria-label", inputLabel);
|
|
2542
2698
|
const actions = document.createElement("div");
|
|
@@ -2963,15 +3119,15 @@ const workAuditActionLabels = {
|
|
|
2963
3119
|
"chapter.deleted": "删除章节",
|
|
2964
3120
|
"chapter.purged": "彻底删除章节",
|
|
2965
3121
|
"chapter.restored": "恢复章节",
|
|
2966
|
-
"draft.created": "
|
|
2967
|
-
"draft.updated": "
|
|
2968
|
-
"draft.deleted": "
|
|
2969
|
-
"draft.restored": "
|
|
3122
|
+
"draft.created": "创建想法",
|
|
3123
|
+
"draft.updated": "更新想法",
|
|
3124
|
+
"draft.deleted": "删除想法",
|
|
3125
|
+
"draft.restored": "恢复想法",
|
|
2970
3126
|
"work.imported": "导入正文"
|
|
2971
3127
|
};
|
|
2972
3128
|
|
|
2973
3129
|
function workAuditEntityLabel(type) {
|
|
2974
|
-
return ({ work: "作品", volume: "分卷", chapter: "章节", draft: "
|
|
3130
|
+
return ({ work: "作品", volume: "分卷", chapter: "章节", draft: "想法", user: "用户" })[type] ?? type;
|
|
2975
3131
|
}
|
|
2976
3132
|
|
|
2977
3133
|
function workAuditDetailText(detail) {
|
|
@@ -3089,6 +3245,7 @@ async function openPlatformUiSettingsDialog() {
|
|
|
3089
3245
|
$("#page-size-timeline").value = String(pageSizes.timeline);
|
|
3090
3246
|
$("#page-size-outlines").value = String(pageSizes.outlines);
|
|
3091
3247
|
$("#page-size-relationships").value = String(pageSizes.relationships);
|
|
3248
|
+
$("#page-size-comments").value = String(pageSizes.comments);
|
|
3092
3249
|
$("#page-size-reviews").value = String(pageSizes.reviews);
|
|
3093
3250
|
$("#page-size-analysis-tasks").value = String(pageSizes.analysisTasks);
|
|
3094
3251
|
$("#page-size-file-versions").value = String(pageSizes.fileVersions);
|
|
@@ -3455,6 +3612,7 @@ function resetWorkScopedUiCaches() {
|
|
|
3455
3612
|
aiReferencesLoadWorkId = null;
|
|
3456
3613
|
aiConversationsLoadPromise = null;
|
|
3457
3614
|
aiConversationsLoadWorkId = null;
|
|
3615
|
+
aiConversationHistoryPage = { page: 1, limit: aiConversationHistoryPageLimit, hasMore: false, nextPage: null };
|
|
3458
3616
|
raceHierarchyLoadPromise = null;
|
|
3459
3617
|
raceHierarchyLoadWorkId = null;
|
|
3460
3618
|
loadedRaceHierarchyWorkId = null;
|
|
@@ -3476,9 +3634,6 @@ function resetWorkScopedUiCaches() {
|
|
|
3476
3634
|
volumeChapterRequests.clear();
|
|
3477
3635
|
state.collapsedRaceIds.clear();
|
|
3478
3636
|
lastSavedChapterSnapshot = null;
|
|
3479
|
-
if (aiContextUsageTimer !== null) clearTimeout(aiContextUsageTimer);
|
|
3480
|
-
aiContextUsageTimer = null;
|
|
3481
|
-
aiContextUsageRequest += 1;
|
|
3482
3637
|
state.aiCitations = [];
|
|
3483
3638
|
state.aiReferences = [];
|
|
3484
3639
|
state.aiPromptSent = false;
|
|
@@ -4000,7 +4155,7 @@ function markActiveModule(module) {
|
|
|
4000
4155
|
}
|
|
4001
4156
|
|
|
4002
4157
|
const moduleMeta = {
|
|
4003
|
-
drafts: ["临时想法", "
|
|
4158
|
+
drafts: ["临时想法", "创作想法", "记录可能采用、也可能永远不会写入正文或正式设定的想法。", "新建想法"],
|
|
4004
4159
|
settings: ["世界事实", "世界观与设定库", "锁定的设定会成为 AI 续写与校对的硬约束。", "新建设定"],
|
|
4005
4160
|
characters: ["人物档案", "角色与人物属性", "维护别名、属性、当前状态及不可被 AI 覆盖的字段。", "新建角色"],
|
|
4006
4161
|
races: ["物种档案", "种族与共同设定", "先维护种族档案,再由角色选择引用;角色不能临时填写种族。", "新建种族"],
|
|
@@ -4008,6 +4163,7 @@ const moduleMeta = {
|
|
|
4008
4163
|
timeline: ["剧情脉络", "大事件时间轴", "候选事件经作者确认后,才进入正式时间线。", "新建事件"],
|
|
4009
4164
|
outlines: ["创作规划", "大纲/伏笔", "为每章维护目标、冲突与转折,并持续提醒尚未回收的伏笔。", "新建伏笔"],
|
|
4010
4165
|
relationships: ["跨章证据", "人物关系", "记录关系方向、阶段、置信度与原文依据。", "新建关系"],
|
|
4166
|
+
comments: ["正文协作", "正文评论", "集中查看并处理当前作品所有章节的评论与待办。", ""],
|
|
4011
4167
|
reviews: ["作者决策", "审核队列", "集中处理冲突、候选设定、低置信度关系和时间问题。", "新增审核项"],
|
|
4012
4168
|
tasks: ["AI 深度分析", "AI 分析中心", "对全书或指定章节运行人物关系、世界观、设定、事件与一致性分析。", "开始 AI 分析"],
|
|
4013
4169
|
"ai-settings": ["书籍提示词", "本书 AI 设置", "本书系统提示词会追加在内置提示词和平台全局提示词之后;任务默认模型只作用于当前作品。", "保存设置"]
|
|
@@ -4052,7 +4208,7 @@ async function showModule(module) {
|
|
|
4052
4208
|
$("#module-description").textContent = meta[2];
|
|
4053
4209
|
$("#module-header-actions").querySelectorAll("[data-module-header-action]").forEach((action) => action.remove());
|
|
4054
4210
|
$("#module-create-button").textContent = meta[3];
|
|
4055
|
-
$("#module-create-button").classList.toggle("hidden", module === "ai-settings" || !canEditModule(module));
|
|
4211
|
+
$("#module-create-button").classList.toggle("hidden", module === "ai-settings" || module === "comments" || !canEditModule(module));
|
|
4056
4212
|
$("#module-content").innerHTML = '<div class="empty-state">正在载入……</div>';
|
|
4057
4213
|
bindModuleContentInteractions();
|
|
4058
4214
|
try {
|
|
@@ -4064,6 +4220,7 @@ async function showModule(module) {
|
|
|
4064
4220
|
if (module === "timeline") await renderTimeline();
|
|
4065
4221
|
if (module === "outlines") await renderOutlines();
|
|
4066
4222
|
if (module === "relationships") await renderRelationships();
|
|
4223
|
+
if (module === "comments") await renderWorkChapterComments();
|
|
4067
4224
|
if (module === "reviews") await renderReviews();
|
|
4068
4225
|
if (module === "tasks") await renderTasks(taskListPage);
|
|
4069
4226
|
if (module === "ai-settings") await renderBookAiSettings();
|
|
@@ -4371,7 +4528,7 @@ function mountRelationshipFilterToggle() {
|
|
|
4371
4528
|
|
|
4372
4529
|
function mountDraftFilterToggle() {
|
|
4373
4530
|
$("#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="
|
|
4531
|
+
$("#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
4532
|
$("#module-header-actions").querySelector('[data-module-header-action="draft-filter-toggle"]')?.addEventListener("click", async () => {
|
|
4376
4533
|
draftFiltersPanelOpen = !draftFiltersPanelOpen;
|
|
4377
4534
|
await renderDrafts(moduleListPages.drafts);
|
|
@@ -4467,15 +4624,15 @@ function settingRecordActions(item) {
|
|
|
4467
4624
|
}
|
|
4468
4625
|
|
|
4469
4626
|
function draftTypeLabel(draftType) {
|
|
4470
|
-
return draftType === "setting" ? "
|
|
4627
|
+
return draftType === "setting" ? "设定想法" : "正文想法";
|
|
4471
4628
|
}
|
|
4472
4629
|
|
|
4473
4630
|
async function deleteDraft(item) {
|
|
4474
4631
|
if (!item || !canEditModule("drafts")) return;
|
|
4475
4632
|
const dialog = $("#form-dialog");
|
|
4476
4633
|
dialog.close();
|
|
4477
|
-
if (!await confirmToast(
|
|
4478
|
-
title: "
|
|
4634
|
+
if (!await confirmToast(`确认删除想法“${item.title}”吗?想法将从当前列表移除。`, {
|
|
4635
|
+
title: "删除想法",
|
|
4479
4636
|
confirmLabel: "确认删除"
|
|
4480
4637
|
})) {
|
|
4481
4638
|
openDraftDialog(item);
|
|
@@ -4484,7 +4641,7 @@ async function deleteDraft(item) {
|
|
|
4484
4641
|
try {
|
|
4485
4642
|
await api(`/api/drafts/${encodeURIComponent(item.id)}`, { method: "DELETE", body: { expectedVersionNo: item.versionNo } });
|
|
4486
4643
|
await renderDrafts(moduleListPages.drafts);
|
|
4487
|
-
toast("
|
|
4644
|
+
toast("想法已删除");
|
|
4488
4645
|
} catch (error) {
|
|
4489
4646
|
toast(error.message, "error");
|
|
4490
4647
|
try {
|
|
@@ -4497,21 +4654,21 @@ async function deleteDraft(item) {
|
|
|
4497
4654
|
|
|
4498
4655
|
function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
4499
4656
|
const viewOnly = readOnly || !canEditModule("drafts");
|
|
4500
|
-
const management = item && !viewOnly ? `<section class="entity-dialog-management" aria-label="
|
|
4501
|
-
<div><strong
|
|
4502
|
-
<div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-draft-delete
|
|
4657
|
+
const management = item && !viewOnly ? `<section class="entity-dialog-management" aria-label="想法操作">
|
|
4658
|
+
<div><strong>想法操作</strong><small>删除后将从想法列表移除,版本历史仍会保留。</small></div>
|
|
4659
|
+
<div class="entity-dialog-management-actions"><button class="danger-button" type="button" data-dialog-draft-delete>删除想法</button></div>
|
|
4503
4660
|
</section>` : "";
|
|
4504
|
-
const fields = `<p class="form-field-note"
|
|
4505
|
-
+ field("draftType", "
|
|
4661
|
+
const fields = `<p class="form-field-note">这里记录未确认的临时想法,可能采用,也可能永远不会写入正文或正式设定。</p>`
|
|
4662
|
+
+ field("draftType", "想法类型", "select", item?.draftType ?? "prose", [["prose", "正文想法"], ["setting", "设定想法"]])
|
|
4506
4663
|
+ field("title", "标题", "text", item?.title ?? "")
|
|
4507
4664
|
+ field("content", "内容", "markdown", item?.content ?? "", {
|
|
4508
4665
|
placeholder: "记录尚未定稿的片段、方向或设定想法……",
|
|
4509
4666
|
readOnly: viewOnly
|
|
4510
4667
|
}) + management;
|
|
4511
|
-
openDialog(item ? viewOnly ? "
|
|
4668
|
+
openDialog(item ? viewOnly ? "查看想法" : "编辑想法" : "新建想法", fields, async (form) => {
|
|
4512
4669
|
if (viewOnly) return;
|
|
4513
4670
|
const title = String(form.get("title") ?? "").trim();
|
|
4514
|
-
if (!title) throw new Error("
|
|
4671
|
+
if (!title) throw new Error("请填写想法标题");
|
|
4515
4672
|
const body = {
|
|
4516
4673
|
draftType: form.get("draftType"),
|
|
4517
4674
|
title,
|
|
@@ -4523,12 +4680,12 @@ function openDraftDialog(item = null, { readOnly = false } = {}) {
|
|
|
4523
4680
|
body
|
|
4524
4681
|
});
|
|
4525
4682
|
await renderDrafts(moduleListPages.drafts);
|
|
4526
|
-
toast(item ? "
|
|
4683
|
+
toast(item ? "想法已保存" : "想法已创建");
|
|
4527
4684
|
}, item ? draftTypeLabel(item.draftType) : "未确认想法", {
|
|
4528
|
-
submitLabel: viewOnly ? "关闭" : "
|
|
4685
|
+
submitLabel: viewOnly ? "关闭" : "保存想法",
|
|
4529
4686
|
hideCancel: viewOnly,
|
|
4530
4687
|
editor: true,
|
|
4531
|
-
errorPrefix: "
|
|
4688
|
+
errorPrefix: "想法保存失败:"
|
|
4532
4689
|
});
|
|
4533
4690
|
if (viewOnly) {
|
|
4534
4691
|
$("#dialog-fields").querySelectorAll("input, select, textarea").forEach((control) => {
|
|
@@ -4550,23 +4707,23 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
4550
4707
|
const pageResult = paginateModuleItems(drafts, page, "drafts");
|
|
4551
4708
|
moduleListPages.drafts = pageResult.page;
|
|
4552
4709
|
const layout = readModuleLayout();
|
|
4553
|
-
if (drafts.length) mountModuleLayoutToggle(layout, "
|
|
4710
|
+
if (drafts.length) mountModuleLayoutToggle(layout, "想法列表样式");
|
|
4554
4711
|
else $("#module-header-actions").querySelector('[data-module-header-action="layout-toggle"]')?.remove();
|
|
4555
4712
|
mountDraftFilterToggle();
|
|
4556
|
-
const filterToolbar = `<section id="draft-filter-panel" class="draft-filter-toolbar${draftFiltersPanelOpen ? "" : " hidden"}" aria-label="
|
|
4557
|
-
<label for="draft-type-filter"
|
|
4558
|
-
<select id="draft-type-filter" aria-label="
|
|
4559
|
-
<option value="all" ${draftTypeFilter === "all" ? "selected" : ""}
|
|
4560
|
-
<option value="prose" ${draftTypeFilter === "prose" ? "selected" : ""}
|
|
4561
|
-
<option value="setting" ${draftTypeFilter === "setting" ? "selected" : ""}
|
|
4713
|
+
const filterToolbar = `<section id="draft-filter-panel" class="draft-filter-toolbar${draftFiltersPanelOpen ? "" : " hidden"}" aria-label="想法筛选">
|
|
4714
|
+
<label for="draft-type-filter">想法类型</label>
|
|
4715
|
+
<select id="draft-type-filter" aria-label="按想法类型筛选">
|
|
4716
|
+
<option value="all" ${draftTypeFilter === "all" ? "selected" : ""}>全部想法</option>
|
|
4717
|
+
<option value="prose" ${draftTypeFilter === "prose" ? "selected" : ""}>正文想法</option>
|
|
4718
|
+
<option value="setting" ${draftTypeFilter === "setting" ? "selected" : ""}>设定想法</option>
|
|
4562
4719
|
</select>
|
|
4563
|
-
${draftTypeFilter === "all" ? "" : `<span aria-live="polite">筛选后剩余 ${drafts.length}
|
|
4720
|
+
${draftTypeFilter === "all" ? "" : `<span aria-live="polite">筛选后剩余 ${drafts.length} 条想法</span>`}
|
|
4564
4721
|
</section>`;
|
|
4565
4722
|
const actions = (item) => canEditModule("drafts")
|
|
4566
|
-
? `${recordCardEditButton("edit-draft", item.id,
|
|
4723
|
+
? `${recordCardEditButton("edit-draft", item.id, `想法“${item.title}”`)}${recordHistoryButton("draft", item.id, item.title)}`
|
|
4567
4724
|
: recordHistoryButton("draft", item.id, item.title);
|
|
4568
4725
|
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="
|
|
4726
|
+
<article class="record-card preview-record-card" data-open-draft="${esc(item.id)}" role="button" tabindex="0" aria-label="查看想法 ${esc(item.title)}">
|
|
4570
4727
|
<small>${esc(draftTypeLabel(item.draftType))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
4571
4728
|
<h3>${esc(item.title)}</h3>
|
|
4572
4729
|
<div class="record-markdown-preview">${esc(item.contentPreview || "暂无内容")}</div>
|
|
@@ -4574,17 +4731,17 @@ async function renderDrafts(page = moduleListPages.drafts) {
|
|
|
4574
4731
|
</article>`).join("")}</div>`;
|
|
4575
4732
|
const rows = `<div class="module-row-list">${pageResult.items.map((item) => {
|
|
4576
4733
|
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="
|
|
4734
|
+
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)}">
|
|
4578
4735
|
<small>${esc(draftTypeLabel(item.draftType))} · 更新于 ${esc(formatDateTime(item.updatedAt))}</small>
|
|
4579
4736
|
<h3>${esc(item.title)}</h3><p class="module-row-preview" title="${esc(preview)}">${esc(preview)}</p>
|
|
4580
4737
|
<div class="card-actions">${actions(item)}</div>
|
|
4581
4738
|
</article>`;
|
|
4582
4739
|
}).join("")}</div>`;
|
|
4583
4740
|
const emptyDrafts = allDrafts.length
|
|
4584
|
-
? emptyModule("
|
|
4585
|
-
: emptyModule("
|
|
4741
|
+
? emptyModule("没有符合筛选条件的想法", "可以切换想法类型查看其他内容。")
|
|
4742
|
+
: emptyModule("还没有想法", "把不一定会进入正文或设定的片段、方向和备选想法先记在这里。");
|
|
4586
4743
|
$("#module-content").innerHTML = filterToolbar + (drafts.length
|
|
4587
|
-
? `${layout === "rows" ? rows : cards}${renderModulePagination(pageResult, "drafts", "
|
|
4744
|
+
? `${layout === "rows" ? rows : cards}${renderModulePagination(pageResult, "drafts", "想法列表")}`
|
|
4588
4745
|
: emptyDrafts);
|
|
4589
4746
|
$("#draft-type-filter").addEventListener("change", async (event) => {
|
|
4590
4747
|
draftTypeFilter = ["prose", "setting"].includes(event.currentTarget.value) ? event.currentTarget.value : "all";
|
|
@@ -5095,6 +5252,35 @@ async function renderRelationships(page = moduleListPages.relationships) {
|
|
|
5095
5252
|
bindEntityHistoryButtons(async () => { await renderRelationships(pageResult.page); await loadAiReferences(); });
|
|
5096
5253
|
}
|
|
5097
5254
|
|
|
5255
|
+
async function renderWorkChapterComments(page = moduleListPages.comments) {
|
|
5256
|
+
const pageSize = pageSizeFor("comments");
|
|
5257
|
+
const result = await moduleApiPage(
|
|
5258
|
+
"comments",
|
|
5259
|
+
`/api/works/${encodeURIComponent(state.work.id)}/chapter-annotations`,
|
|
5260
|
+
page,
|
|
5261
|
+
pageSize
|
|
5262
|
+
);
|
|
5263
|
+
if (!result.items.length && page > 1) return renderWorkChapterComments(page - 1);
|
|
5264
|
+
const total = Number(result.total ?? result.items.length);
|
|
5265
|
+
const pageCount = Math.max(1, Math.ceil(total / result.limit));
|
|
5266
|
+
const pageResult = { ...result, total, pageCount, itemCount: result.items.length };
|
|
5267
|
+
moduleListPages.comments = pageResult.page;
|
|
5268
|
+
mountModuleCount(total);
|
|
5269
|
+
$("#module-content").innerHTML = result.items.length
|
|
5270
|
+
? `<div class="chapter-comment-module-list">${result.items.map((annotation) => chapterAnnotationCard(annotation, { showSource: true })).join("")}</div>${renderModulePagination(pageResult, "comments", "正文评论列表")}`
|
|
5271
|
+
: emptyModule("还没有正文评论", "在任一正文行上点击右键,即可添加评论或待办。");
|
|
5272
|
+
bindModulePagination("comments", renderWorkChapterComments);
|
|
5273
|
+
bindChapterAnnotationCards($("#module-content"), result.items, {
|
|
5274
|
+
refresh: () => renderWorkChapterComments(pageResult.page),
|
|
5275
|
+
locate: async (annotation) => {
|
|
5276
|
+
await selectChapter(annotation.chapterId);
|
|
5277
|
+
await new Promise((resolve) => window.requestAnimationFrame(resolve));
|
|
5278
|
+
paintChapterLineSelection(annotation.startLine - 1, annotation.endLine - 1);
|
|
5279
|
+
selectChapterLines(annotation.startLine - 1, annotation.endLine - 1);
|
|
5280
|
+
}
|
|
5281
|
+
});
|
|
5282
|
+
}
|
|
5283
|
+
|
|
5098
5284
|
async function renderReviews(page = moduleListPages.reviews) {
|
|
5099
5285
|
const canReadCharacters = canReadModule("characters");
|
|
5100
5286
|
const canResolveReview = canEditModule("reviews");
|
|
@@ -5943,9 +6129,9 @@ function renderProviderCards(providers, models) {
|
|
|
5943
6129
|
: provider.connectionStatus !== "success"
|
|
5944
6130
|
? `<span class="model-status-badge is-unavailable">连接不可用</span>`
|
|
5945
6131
|
: "";
|
|
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}
|
|
6132
|
+
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
6133
|
}).join("")}</div>
|
|
5948
|
-
<div class="card-actions"><button data-edit-provider="${esc(provider.id)}">编辑配置</button
|
|
6134
|
+
<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
6135
|
}).join("")}</div>`
|
|
5950
6136
|
: emptyModule("尚未配置 AI 供应商", "添加 OpenAI 或 Anthropic 兼容接口地址和密钥,测试成功后再添加模型。");
|
|
5951
6137
|
}
|
|
@@ -5959,14 +6145,6 @@ function bindPlatformProviderActions(host, providers, models) {
|
|
|
5959
6145
|
await renderPlatformAiConfig();
|
|
5960
6146
|
await loadModels();
|
|
5961
6147
|
}));
|
|
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
6148
|
host.querySelectorAll("[data-add-model]").forEach((button) => button.addEventListener("click", () => openModelDialog(button.dataset.addModel)));
|
|
5971
6149
|
host.querySelectorAll("[data-edit-model]").forEach((button) => button.addEventListener("click", () => openModelDialog(undefined, models.find((model) => model.id === button.dataset.editModel))));
|
|
5972
6150
|
host.querySelectorAll("[data-edit-provider]").forEach((button) => button.addEventListener("click", () => openProviderDialog(providers.find((provider) => provider.id === button.dataset.editProvider))));
|
|
@@ -6408,7 +6586,7 @@ async function renderBookAiSettings() {
|
|
|
6408
6586
|
);
|
|
6409
6587
|
host.querySelector(".ai-agent-tools").insertAdjacentHTML(
|
|
6410
6588
|
"beforeend",
|
|
6411
|
-
`<label><input name="agent-tool" type="checkbox" value="search_drafts" ${agentTools.has("search_drafts") ? "checked" : ""}><span><strong
|
|
6589
|
+
`<label><input name="agent-tool" type="checkbox" value="search_drafts" ${agentTools.has("search_drafts") ? "checked" : ""}><span><strong>搜索想法</strong><small>查询正文想法和设定想法。这些内容只是可能采用、也可能永远不会进入正文或正式设定的临时方向,Agent 不会把它当作已确认事实。</small></span></label>`
|
|
6412
6590
|
);
|
|
6413
6591
|
if (!canEditModule("ai-settings")) {
|
|
6414
6592
|
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
@@ -6447,7 +6625,7 @@ async function renderBookAiSettings() {
|
|
|
6447
6625
|
try {
|
|
6448
6626
|
await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { systemPrompt: $("#work-system-prompt").value } });
|
|
6449
6627
|
toast("本书系统提示词已保存");
|
|
6450
|
-
|
|
6628
|
+
setAiContextMeter(null);
|
|
6451
6629
|
} catch (error) {
|
|
6452
6630
|
toast(error.message, "error");
|
|
6453
6631
|
} finally {
|
|
@@ -6500,7 +6678,7 @@ async function renderBookAiSettings() {
|
|
|
6500
6678
|
try {
|
|
6501
6679
|
await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { bookSummaryContextPercent: Number($("#book-summary-context-percent").value) } });
|
|
6502
6680
|
toast("全书概要引用配额已保存");
|
|
6503
|
-
|
|
6681
|
+
setAiContextMeter(null);
|
|
6504
6682
|
} catch (error) {
|
|
6505
6683
|
toast(error.message, "error");
|
|
6506
6684
|
} finally {
|
|
@@ -6513,7 +6691,7 @@ async function renderBookAiSettings() {
|
|
|
6513
6691
|
try {
|
|
6514
6692
|
await api(`/api/works/${state.work.id}/ai-settings`, { method: "PATCH", body: { contextCompactThreshold: Number($("#context-compact-threshold").value) } });
|
|
6515
6693
|
toast("对话上下文 compact 阈值已保存");
|
|
6516
|
-
|
|
6694
|
+
setAiContextMeter(null);
|
|
6517
6695
|
} catch (error) {
|
|
6518
6696
|
toast(error.message, "error");
|
|
6519
6697
|
} finally {
|
|
@@ -6564,13 +6742,17 @@ async function loadModels() {
|
|
|
6564
6742
|
const generation = workScopedUiGeneration;
|
|
6565
6743
|
const models = await api(`/api/works/${workId}/models`);
|
|
6566
6744
|
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
6567
|
-
|
|
6745
|
+
applyAiModels(models);
|
|
6568
6746
|
loadedAiModelsWorkId = workId;
|
|
6747
|
+
}
|
|
6748
|
+
|
|
6749
|
+
function applyAiModels(models) {
|
|
6750
|
+
state.models = models.filter((model) => isSelectableModel(model));
|
|
6569
6751
|
const select = $("#ai-model");
|
|
6570
6752
|
select.innerHTML = state.models.length
|
|
6571
6753
|
? state.models.map((model) => `<option value="${esc(model.id)}">${esc(modelOptionLabel(model))}</option>`).join("")
|
|
6572
6754
|
: '<option value="">请先配置模型</option>';
|
|
6573
|
-
|
|
6755
|
+
setAiContextMeter(null);
|
|
6574
6756
|
}
|
|
6575
6757
|
|
|
6576
6758
|
async function ensureAiModelsLoaded() {
|
|
@@ -6594,9 +6776,6 @@ async function ensureAiModelsLoaded() {
|
|
|
6594
6776
|
}
|
|
6595
6777
|
}
|
|
6596
6778
|
|
|
6597
|
-
let aiContextUsageTimer = null;
|
|
6598
|
-
let aiContextUsageRequest = 0;
|
|
6599
|
-
|
|
6600
6779
|
function currentAiRequestScope() {
|
|
6601
6780
|
if (!state.work) return null;
|
|
6602
6781
|
const taskType = $("#ai-task").value;
|
|
@@ -6622,7 +6801,7 @@ function renderAiContextDistribution(usage) {
|
|
|
6622
6801
|
const distribution = normalizeAiContextTokenDistribution(usage);
|
|
6623
6802
|
const contextWindow = distribution.contextWindow.toLocaleString("zh-CN");
|
|
6624
6803
|
$("#ai-context-popover-description").textContent = usage
|
|
6625
|
-
? `已占用 ${distribution.occupiedTokens.toLocaleString("zh-CN")} / ${contextWindow} tok`
|
|
6804
|
+
? `已占用 ${distribution.occupiedTokens.toLocaleString("zh-CN")} / ${contextWindow} tok · ${formatAiContextUsagePercent(distribution.occupiedTokens, distribution.contextWindow)}`
|
|
6626
6805
|
: "选择可用模型后显示当前上下文用量";
|
|
6627
6806
|
host.replaceChildren(...distribution.items.map((item) => {
|
|
6628
6807
|
const row = document.createElement("div");
|
|
@@ -6635,9 +6814,9 @@ function renderAiContextDistribution(usage) {
|
|
|
6635
6814
|
label.className = "ai-context-distribution-label";
|
|
6636
6815
|
const title = document.createElement("span");
|
|
6637
6816
|
title.textContent = item.label;
|
|
6638
|
-
if (item.key === "context") {
|
|
6817
|
+
if (item.key === "skills" || item.key === "context") {
|
|
6639
6818
|
const description = document.createElement("small");
|
|
6640
|
-
description.textContent = "用户和 agent 的交互";
|
|
6819
|
+
description.textContent = item.key === "skills" ? "待加入" : "用户和 agent 的交互";
|
|
6641
6820
|
title.append(" ", description);
|
|
6642
6821
|
}
|
|
6643
6822
|
const value = document.createElement("strong");
|
|
@@ -6701,57 +6880,6 @@ function hideAiContextWarning() {
|
|
|
6701
6880
|
$("#ai-context-warning").classList.add("hidden");
|
|
6702
6881
|
}
|
|
6703
6882
|
|
|
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
6883
|
async function loadAiReferences() {
|
|
6756
6884
|
const workId = state.work?.id;
|
|
6757
6885
|
if (!workId) return;
|
|
@@ -9026,16 +9154,30 @@ function openProviderDialog(item) {
|
|
|
9026
9154
|
|
|
9027
9155
|
function openModelDialog(providerId, item = null) {
|
|
9028
9156
|
const values = modelFormValues(item);
|
|
9157
|
+
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
9158
|
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
|
-
|
|
9159
|
+
const connectionTest = item && item.providerStatus === "enabled"
|
|
9160
|
+
? `<section class="model-connection-test">
|
|
9161
|
+
<div><strong>模型连接测试</strong><p>使用当前已保存的模型标识符和供应商凭据发起最小请求。</p></div>
|
|
9162
|
+
<button class="ghost-button" type="button" data-test-model="${esc(item.id)}">测试连接</button>
|
|
9163
|
+
</section>`
|
|
9164
|
+
: "";
|
|
9165
|
+
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
9166
|
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
9167
|
await api(item ? `/api/models/${item.id}` : `/api/providers/${providerId}/models`, { method: item ? "PATCH" : "POST", body });
|
|
9033
9168
|
await renderPlatformAiConfig();
|
|
9034
9169
|
await loadModels();
|
|
9035
9170
|
}, item ? "模型配置" : "供应商模型");
|
|
9036
9171
|
const modelIdInput = $("#dialog-fields input[name='modelId']");
|
|
9172
|
+
const contextWindowInput = $("#model-context-window");
|
|
9173
|
+
const contextWindowHint = $("#model-context-window-hint");
|
|
9037
9174
|
const temperatureInput = $("#dialog-fields input[name='temperature']");
|
|
9038
9175
|
const temperatureHint = $("#model-temperature-hint");
|
|
9176
|
+
const syncModelContextWindowGuidance = () => {
|
|
9177
|
+
const guidance = modelContextWindowGuidance(contextWindowInput.value);
|
|
9178
|
+
contextWindowInput.setCustomValidity(guidance.belowMinimum ? "模型上下文不能低于 32K(32768 Token)。" : "");
|
|
9179
|
+
contextWindowHint.hidden = !guidance.showRecommendation;
|
|
9180
|
+
};
|
|
9039
9181
|
const syncKimiTemperature = () => {
|
|
9040
9182
|
const isKimi = isKimiModelId(modelIdInput.value);
|
|
9041
9183
|
temperatureHint.hidden = !isKimi;
|
|
@@ -9044,13 +9186,31 @@ function openModelDialog(providerId, item = null) {
|
|
|
9044
9186
|
if (isKimiModelId(modelIdInput.value)) temperatureInput.value = "1";
|
|
9045
9187
|
syncKimiTemperature();
|
|
9046
9188
|
});
|
|
9189
|
+
contextWindowInput.addEventListener("input", syncModelContextWindowGuidance);
|
|
9190
|
+
$("#dialog-fields [data-test-model]")?.addEventListener("click", async (event) => {
|
|
9191
|
+
const button = event.currentTarget;
|
|
9192
|
+
button.disabled = true;
|
|
9193
|
+
button.textContent = "测试中";
|
|
9194
|
+
try {
|
|
9195
|
+
const result = await api(`/api/models/${button.dataset.testModel}/test`, { method: "POST", body: {} });
|
|
9196
|
+
toast(result.ok ? "模型连接测试成功" : `模型连接失败:${result.error}`, result.ok ? "info" : "error");
|
|
9197
|
+
await renderPlatformAiConfig();
|
|
9198
|
+
await loadModels();
|
|
9199
|
+
} catch (error) {
|
|
9200
|
+
toast(`模型连接测试失败:${error.message}`, "error");
|
|
9201
|
+
} finally {
|
|
9202
|
+
button.disabled = false;
|
|
9203
|
+
button.textContent = "测试连接";
|
|
9204
|
+
}
|
|
9205
|
+
});
|
|
9206
|
+
syncModelContextWindowGuidance();
|
|
9047
9207
|
syncKimiTemperature();
|
|
9048
9208
|
}
|
|
9049
9209
|
|
|
9050
9210
|
async function sendAi() {
|
|
9051
9211
|
if (!state.work) return toast("请先选择作品", "error");
|
|
9052
9212
|
try {
|
|
9053
|
-
await
|
|
9213
|
+
await ensureAiModelsLoaded();
|
|
9054
9214
|
} catch (error) {
|
|
9055
9215
|
return toast(`创作助手加载失败:${error.message}`, "error");
|
|
9056
9216
|
}
|
|
@@ -9063,30 +9223,18 @@ async function sendAi() {
|
|
|
9063
9223
|
const { taskType, scope, selection } = requestScope;
|
|
9064
9224
|
if (taskType === "polish" && !selection) return toast("请先在正文中选中一段文本", "error");
|
|
9065
9225
|
const citations = state.aiCitations.map(({ chapterId, chapterTitle, startLine, endLine, text }) => ({ chapterId, chapterTitle, startLine, endLine, text }));
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
$("#ai-send").textContent = "检查上下文";
|
|
9226
|
+
let persistedUserMessage = null;
|
|
9227
|
+
if (taskType !== "chat") {
|
|
9069
9228
|
try {
|
|
9070
|
-
|
|
9071
|
-
if (!mayContinue) return;
|
|
9229
|
+
persistedUserMessage = await persistAiConversationMessage("user", instruction, citations);
|
|
9072
9230
|
} catch (error) {
|
|
9073
|
-
toast(
|
|
9074
|
-
return;
|
|
9075
|
-
} finally {
|
|
9076
|
-
$("#ai-send").disabled = false;
|
|
9077
|
-
$("#ai-send").textContent = "发送";
|
|
9231
|
+
return toast(`对话记录创建失败:${error.message}`, "error");
|
|
9078
9232
|
}
|
|
9233
|
+
state.aiPromptSent = true;
|
|
9234
|
+
renderAiQuickActions();
|
|
9235
|
+
appendMessage("user", instruction, citations, persistedUserMessage.createdAt, {}, persistedUserMessage.id);
|
|
9236
|
+
clearAiPromptComposer();
|
|
9079
9237
|
}
|
|
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
9238
|
$("#ai-send").disabled = true;
|
|
9091
9239
|
$("#ai-send").textContent = "发送中";
|
|
9092
9240
|
try {
|
|
@@ -9096,7 +9244,8 @@ async function sendAi() {
|
|
|
9096
9244
|
let persistedStreamMessage = null;
|
|
9097
9245
|
let suggestion = null;
|
|
9098
9246
|
if (taskType === "chat") {
|
|
9099
|
-
const streamed = await streamChat({ instruction, scope, modelId, citations, conversationId: state.aiConversationId
|
|
9247
|
+
const streamed = await streamChat({ instruction, scope, modelId, citations, conversationId: state.aiConversationId || undefined });
|
|
9248
|
+
if (streamed.action === "warn") return;
|
|
9100
9249
|
assistantContent = streamed.content;
|
|
9101
9250
|
assistantMessage = streamed.message;
|
|
9102
9251
|
assistantMetadata = streamed.metadata;
|
|
@@ -9104,11 +9253,18 @@ async function sendAi() {
|
|
|
9104
9253
|
applyAiConversationTitle(streamed.conversationTitle);
|
|
9105
9254
|
} else {
|
|
9106
9255
|
suggestion = await api(`/api/works/${state.work.id}/suggestions`, { method: "POST", body: { taskType, instruction, scope, modelId, citations } });
|
|
9256
|
+
setAiContextMeter(suggestion.contextUsage);
|
|
9107
9257
|
assistantContent = suggestion.content;
|
|
9108
9258
|
assistantMetadata = { modelDisplayName: suggestion.model?.displayName, outputTokens: suggestion.outputTokens, cacheHitPercent: suggestion.cacheHitPercent };
|
|
9109
9259
|
}
|
|
9110
9260
|
try {
|
|
9111
9261
|
if (persistedStreamMessage) {
|
|
9262
|
+
updateAiConversationSummaryFromMessage({
|
|
9263
|
+
conversationId: state.aiConversationId,
|
|
9264
|
+
role: "assistant",
|
|
9265
|
+
content: assistantContent,
|
|
9266
|
+
createdAt: persistedStreamMessage.createdAt
|
|
9267
|
+
});
|
|
9112
9268
|
updateMessageCreatedAt(assistantMessage, persistedStreamMessage.createdAt);
|
|
9113
9269
|
attachMessageIdentity(assistantMessage, persistedStreamMessage.id);
|
|
9114
9270
|
} else {
|
|
@@ -9138,13 +9294,19 @@ async function streamChat(body) {
|
|
|
9138
9294
|
message.className = "assistant-message is-streaming";
|
|
9139
9295
|
message.dataset.testid = "ai-stream-message";
|
|
9140
9296
|
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
9297
|
const content = message.querySelector(".message-body");
|
|
9145
9298
|
const meta = message.querySelector(".message-meta");
|
|
9299
|
+
let messageMounted = false;
|
|
9300
|
+
const mountAssistantMessage = () => {
|
|
9301
|
+
if (messageMounted) return;
|
|
9302
|
+
attachMessageHeading(message, "助手 · 正在生成");
|
|
9303
|
+
$("#ai-feed").append(message);
|
|
9304
|
+
messageMounted = true;
|
|
9305
|
+
scrollAiFeedToBottom();
|
|
9306
|
+
};
|
|
9146
9307
|
const typewriter = createStreamTypewriter({
|
|
9147
9308
|
onRender: (text, progress) => {
|
|
9309
|
+
mountAssistantMessage();
|
|
9148
9310
|
content.innerHTML = renderMarkdown(text);
|
|
9149
9311
|
const receivedCharacters = progress.visibleCharacters + progress.pendingCharacters;
|
|
9150
9312
|
meta.textContent = progress.pendingCharacters > 0
|
|
@@ -9160,6 +9322,8 @@ async function streamChat(body) {
|
|
|
9160
9322
|
let persistedMessageId = null;
|
|
9161
9323
|
let persistedMessageCreatedAt = null;
|
|
9162
9324
|
let conversationTitle = null;
|
|
9325
|
+
let persistedUserMessage = null;
|
|
9326
|
+
let contextAction = "ready";
|
|
9163
9327
|
let finalAnswerStarted = false;
|
|
9164
9328
|
const processStartedAt = Date.now();
|
|
9165
9329
|
const elapsedProcessTime = () => Math.max(0, Date.now() - processStartedAt);
|
|
@@ -9186,7 +9350,35 @@ async function streamChat(body) {
|
|
|
9186
9350
|
}
|
|
9187
9351
|
if (!dataLines.length) return;
|
|
9188
9352
|
const payload = JSON.parse(dataLines.join("\n"));
|
|
9189
|
-
if (eventName === "
|
|
9353
|
+
if (eventName === "context") {
|
|
9354
|
+
contextAction = typeof payload.action === "string" ? payload.action : "ready";
|
|
9355
|
+
setAiContextMeter(payload.usage);
|
|
9356
|
+
if (payload.conversation?.id) {
|
|
9357
|
+
state.aiConversationId = payload.conversation.id;
|
|
9358
|
+
upsertAiConversationSummary(payload.conversation);
|
|
9359
|
+
$("#ai-conversation-title").textContent = payload.conversation.title;
|
|
9360
|
+
}
|
|
9361
|
+
if (contextAction === "warn") showAiContextWarning(payload.usage);
|
|
9362
|
+
else {
|
|
9363
|
+
hideAiContextWarning();
|
|
9364
|
+
if (contextAction === "compacted") {
|
|
9365
|
+
renderConversationCompactionDivider(payload.compaction?.compactedMessageCount, payload.conversation?.messageCount);
|
|
9366
|
+
toast("已自动整理较早对话为长期记忆并继续发送");
|
|
9367
|
+
}
|
|
9368
|
+
}
|
|
9369
|
+
} else if (eventName === "user_message") {
|
|
9370
|
+
persistedUserMessage = payload.message;
|
|
9371
|
+
if (persistedUserMessage?.id) {
|
|
9372
|
+
state.aiConversationId = persistedUserMessage.conversationId;
|
|
9373
|
+
updateAiConversationSummaryFromMessage(persistedUserMessage);
|
|
9374
|
+
state.aiPromptSent = true;
|
|
9375
|
+
renderAiQuickActions();
|
|
9376
|
+
appendMessage("user", persistedUserMessage.content, persistedUserMessage.citations, persistedUserMessage.createdAt, {}, persistedUserMessage.id);
|
|
9377
|
+
clearAiPromptComposer();
|
|
9378
|
+
mountAssistantMessage();
|
|
9379
|
+
}
|
|
9380
|
+
} else if (eventName === "delta") {
|
|
9381
|
+
mountAssistantMessage();
|
|
9190
9382
|
const firstFinalDelta = streamedText.length === 0;
|
|
9191
9383
|
const delta = typeof payload.delta === "string" ? payload.delta : "";
|
|
9192
9384
|
streamedText += delta;
|
|
@@ -9195,6 +9387,7 @@ async function streamChat(body) {
|
|
|
9195
9387
|
if (firstFinalDelta && processSteps.length) renderAiProcessSteps(message, processSteps, true, elapsedProcessTime());
|
|
9196
9388
|
meta.textContent = "正在生成回复……";
|
|
9197
9389
|
} else if (eventName === "process_step") {
|
|
9390
|
+
mountAssistantMessage();
|
|
9198
9391
|
const step = { ...payload };
|
|
9199
9392
|
const append = step.append === true;
|
|
9200
9393
|
delete step.append;
|
|
@@ -9202,9 +9395,14 @@ async function streamChat(body) {
|
|
|
9202
9395
|
if (existing && typeof step.content === "string") existing.content += step.content;
|
|
9203
9396
|
else processSteps.push(step);
|
|
9204
9397
|
renderAiProcessSteps(message, processSteps, finalAnswerStarted, elapsedProcessTime());
|
|
9205
|
-
meta.textContent = step.type === "thinking"
|
|
9398
|
+
meta.textContent = step.type === "thinking"
|
|
9399
|
+
? `正在思考 · 第 ${Number(step.round) || 1} 轮`
|
|
9400
|
+
: step.type === "context_compaction"
|
|
9401
|
+
? `已压缩第 ${Number(step.round) || 1} 轮工具上下文`
|
|
9402
|
+
: `正在处理第 ${Number(step.round) || 1} 轮中间结果`;
|
|
9206
9403
|
scrollAiFeedToBottom();
|
|
9207
9404
|
} else if (eventName === "tool_call") {
|
|
9405
|
+
mountAssistantMessage();
|
|
9208
9406
|
const toolCall = { ...payload };
|
|
9209
9407
|
const round = toolCall.round;
|
|
9210
9408
|
delete toolCall.round;
|
|
@@ -9213,10 +9411,15 @@ async function streamChat(body) {
|
|
|
9213
9411
|
renderAiProcessSteps(message, processSteps, finalAnswerStarted, elapsedProcessTime());
|
|
9214
9412
|
meta.textContent = `已调用 ${toolCalls.length} 个工具,正在等待模型处理结果`;
|
|
9215
9413
|
scrollAiFeedToBottom();
|
|
9414
|
+
} else if (eventName === "context_compacted") {
|
|
9415
|
+
setAiContextMeter(payload.contextUsage);
|
|
9416
|
+
if (messageMounted) meta.textContent = "已压缩工具上下文,正在继续生成";
|
|
9216
9417
|
} else if (eventName === "complete") {
|
|
9418
|
+
mountAssistantMessage();
|
|
9217
9419
|
persistedMessageId = typeof payload.messageId === "string" ? payload.messageId : null;
|
|
9218
9420
|
persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
|
|
9219
9421
|
conversationTitle = typeof payload.conversationTitle === "string" ? payload.conversationTitle : null;
|
|
9422
|
+
setAiContextMeter(payload.contextUsage);
|
|
9220
9423
|
await typewriter.finish();
|
|
9221
9424
|
message.classList.remove("is-streaming");
|
|
9222
9425
|
content.setAttribute("aria-busy", "false");
|
|
@@ -9244,8 +9447,9 @@ async function streamChat(body) {
|
|
|
9244
9447
|
if (buffer.trim()) await consume(buffer);
|
|
9245
9448
|
await typewriter.finish();
|
|
9246
9449
|
if (streamError) throw streamError;
|
|
9247
|
-
return { content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle };
|
|
9450
|
+
return { action: contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, userMessage: persistedUserMessage };
|
|
9248
9451
|
} catch (error) {
|
|
9452
|
+
mountAssistantMessage();
|
|
9249
9453
|
typewriter.reveal();
|
|
9250
9454
|
message.classList.remove("is-streaming");
|
|
9251
9455
|
content.setAttribute("aria-busy", "false");
|
|
@@ -9265,7 +9469,15 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
|
|
|
9265
9469
|
? `<p class="ai-error-text">${esc(text)}</p>`
|
|
9266
9470
|
: renderMarkdown(text);
|
|
9267
9471
|
message.innerHTML = `<div class="message-body">${messageBody}</div>`;
|
|
9268
|
-
attachMessageHeading(message, role === "user" ? "作者" : "助手", createdAt ?? undefined);
|
|
9472
|
+
const heading = attachMessageHeading(message, role === "user" ? "作者" : "助手", createdAt ?? undefined);
|
|
9473
|
+
if (isFailure) {
|
|
9474
|
+
message.dataset.status = "failed";
|
|
9475
|
+
const failureBadge = document.createElement("strong");
|
|
9476
|
+
failureBadge.className = "ai-message-status is-error";
|
|
9477
|
+
failureBadge.textContent = "失败";
|
|
9478
|
+
failureBadge.setAttribute("aria-label", "消息状态:失败");
|
|
9479
|
+
heading.firstElementChild?.append(failureBadge);
|
|
9480
|
+
}
|
|
9269
9481
|
if (citations.length) {
|
|
9270
9482
|
const references = document.createElement("div");
|
|
9271
9483
|
references.className = "message-citations";
|
|
@@ -10238,6 +10450,7 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
10238
10450
|
timeline: Number($("#page-size-timeline").value),
|
|
10239
10451
|
outlines: Number($("#page-size-outlines").value),
|
|
10240
10452
|
relationships: Number($("#page-size-relationships").value),
|
|
10453
|
+
comments: Number($("#page-size-comments").value),
|
|
10241
10454
|
reviews: Number($("#page-size-reviews").value),
|
|
10242
10455
|
analysisTasks: Number($("#page-size-analysis-tasks").value),
|
|
10243
10456
|
fileVersions: Number($("#page-size-file-versions").value)
|
|
@@ -10383,10 +10596,14 @@ $("#chapter-content").addEventListener("input", (event) => {
|
|
|
10383
10596
|
scheduleChapterAutoSave();
|
|
10384
10597
|
clearChapterLineSelection();
|
|
10385
10598
|
scheduleChapterLineNumbers();
|
|
10386
|
-
|
|
10599
|
+
setAiContextMeter(null);
|
|
10387
10600
|
});
|
|
10388
|
-
$("#chapter-content").addEventListener("select",
|
|
10601
|
+
$("#chapter-content").addEventListener("select", () => setAiContextMeter(null));
|
|
10389
10602
|
$("#chapter-content").addEventListener("scroll", syncChapterLineNumberScroll);
|
|
10603
|
+
$("#chapter-content").addEventListener("contextmenu", (event) => {
|
|
10604
|
+
if (!state.chapter) return;
|
|
10605
|
+
showLineCitationMenu(event, lineIndexAtPointer(event.clientY));
|
|
10606
|
+
});
|
|
10390
10607
|
$("#chapter-line-numbers-inner").addEventListener("pointerdown", (event) => {
|
|
10391
10608
|
const row = event.target.closest(".chapter-line-number");
|
|
10392
10609
|
if (!row || event.button !== 0) return;
|
|
@@ -10466,7 +10683,7 @@ $("#module-more-button").addEventListener("click", () => setModuleNavExpanded(!m
|
|
|
10466
10683
|
$("#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
10684
|
$("#ai-prompt").addEventListener("input", async () => {
|
|
10468
10685
|
updateAiMentionMenu();
|
|
10469
|
-
|
|
10686
|
+
setAiContextMeter(null);
|
|
10470
10687
|
if (!findAiMention(aiPromptTextBeforeCursor())) return;
|
|
10471
10688
|
try {
|
|
10472
10689
|
await ensureAiReferencesLoaded();
|
|
@@ -10481,9 +10698,9 @@ $("#ai-prompt").addEventListener("focus", () => {
|
|
|
10481
10698
|
$("#ai-model").addEventListener("focus", () => {
|
|
10482
10699
|
ensureAiModelsLoaded().catch((error) => toast(`模型加载失败:${error.message}`, "error"));
|
|
10483
10700
|
});
|
|
10484
|
-
$("#ai-model").addEventListener("change",
|
|
10485
|
-
$("#ai-task").addEventListener("change",
|
|
10486
|
-
$("#ai-scope").addEventListener("change",
|
|
10701
|
+
$("#ai-model").addEventListener("change", () => setAiContextMeter(null));
|
|
10702
|
+
$("#ai-task").addEventListener("change", () => setAiContextMeter(null));
|
|
10703
|
+
$("#ai-scope").addEventListener("change", () => setAiContextMeter(null));
|
|
10487
10704
|
$("#ai-mention-menu").addEventListener("click", (event) => {
|
|
10488
10705
|
const button = event.target.closest("[data-ai-reference-id]");
|
|
10489
10706
|
if (button) selectAiMention(button);
|
|
@@ -10686,7 +10903,15 @@ $("#ai-context-compact").addEventListener("click", async () => {
|
|
|
10686
10903
|
});
|
|
10687
10904
|
hideAiContextWarning();
|
|
10688
10905
|
toast(result.changed ? `已整理 ${result.compactedMessageCount} 条较早消息为长期记忆` : "当前没有需要整理的较早消息");
|
|
10689
|
-
|
|
10906
|
+
setAiContextMeter(result.usage);
|
|
10907
|
+
if (result.changed) {
|
|
10908
|
+
const current = state.aiConversations.find((conversation) => conversation.id === conversationId);
|
|
10909
|
+
if (current) {
|
|
10910
|
+
current.compactedMessageCount = result.compactedMessageCount;
|
|
10911
|
+
current.hasCompactedSummary = true;
|
|
10912
|
+
}
|
|
10913
|
+
renderConversationCompactionDivider(result.compactedMessageCount, current?.messageCount);
|
|
10914
|
+
}
|
|
10690
10915
|
} catch (error) {
|
|
10691
10916
|
toast(`上下文压缩失败:${error.message}`, "error");
|
|
10692
10917
|
} finally {
|
|
@@ -10712,6 +10937,30 @@ $("#ai-history-toggle").addEventListener("click", async () => {
|
|
|
10712
10937
|
toast(`对话历史加载失败:${error.message}`, "error");
|
|
10713
10938
|
}
|
|
10714
10939
|
});
|
|
10940
|
+
$("#ai-history-previous").addEventListener("click", async () => {
|
|
10941
|
+
const targetPage = aiConversationHistoryPage.page - 1;
|
|
10942
|
+
if (targetPage < 1) return;
|
|
10943
|
+
$("#ai-history-previous").disabled = true;
|
|
10944
|
+
$("#ai-history-next").disabled = true;
|
|
10945
|
+
try {
|
|
10946
|
+
await loadAiConversations(targetPage);
|
|
10947
|
+
} catch (error) {
|
|
10948
|
+
renderAiConversationHistory();
|
|
10949
|
+
toast(`对话历史加载失败:${error.message}`, "error");
|
|
10950
|
+
}
|
|
10951
|
+
});
|
|
10952
|
+
$("#ai-history-next").addEventListener("click", async () => {
|
|
10953
|
+
const targetPage = aiConversationHistoryPage.nextPage;
|
|
10954
|
+
if (!targetPage) return;
|
|
10955
|
+
$("#ai-history-previous").disabled = true;
|
|
10956
|
+
$("#ai-history-next").disabled = true;
|
|
10957
|
+
try {
|
|
10958
|
+
await loadAiConversations(targetPage);
|
|
10959
|
+
} catch (error) {
|
|
10960
|
+
renderAiConversationHistory();
|
|
10961
|
+
toast(`对话历史加载失败:${error.message}`, "error");
|
|
10962
|
+
}
|
|
10963
|
+
});
|
|
10715
10964
|
$("#ai-history-close").addEventListener("click", () => setAiHistoryVisible(false));
|
|
10716
10965
|
$("#ai-history-dialog").addEventListener("close", () => {
|
|
10717
10966
|
$("#ai-history-toggle").setAttribute("aria-expanded", "false");
|