@musnows/scriverse 0.3.9 → 0.3.11
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/app.js +167 -31
- package/dist/app.js.map +1 -1
- package/dist/public/app.js +387 -125
- package/dist/public/index.html +31 -6
- package/dist/public/styles.css +40 -22
- package/dist/public/work-permissions.d.ts +14 -0
- package/dist/public/work-permissions.js +71 -0
- package/dist/store.js +89 -23
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +203 -52
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/work-permissions.js +129 -0
- package/dist/work-permissions.js.map +1 -0
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -19,6 +19,7 @@ import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelatio
|
|
|
19
19
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
20
20
|
import { buildRaceForest, eligibleRaceParents, racePathLabel } from "/race-hierarchy.js?v=20260721-race-hierarchy";
|
|
21
21
|
import { ANALYSIS_TYPES, analysisTypeDescription } from "/analysis-types.js?v=20260721-analysis-descriptions";
|
|
22
|
+
import { WORK_PERMISSION_MODULES, canReadUiModule, canWriteUiModule, emptyModulePermissions, firstReadableUiModule, normalizeModulePermissions, permissionSummary } from "/work-permissions.js?v=20260722-module-permissions";
|
|
22
23
|
|
|
23
24
|
const state = {
|
|
24
25
|
user: null,
|
|
@@ -81,11 +82,17 @@ function analysisTaskStatusLabel(status) {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
function canEditWork(work = state.work) {
|
|
84
|
-
return
|
|
85
|
+
return WORK_PERMISSION_MODULES.some((item) => canWriteUiModule(work, item.uiModule));
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
function canEditProse(work = state.work) {
|
|
88
|
-
return
|
|
89
|
+
return canWriteUiModule(work, "editor");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function canReplaceProse(work = state.work) {
|
|
93
|
+
return WORK_PERMISSION_MODULES
|
|
94
|
+
.filter((item) => item.id !== "ai-settings")
|
|
95
|
+
.every((item) => canWriteUiModule(work, item.uiModule));
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
function canManageWork(work = state.work) {
|
|
@@ -93,23 +100,47 @@ function canManageWork(work = state.work) {
|
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
function canEditModule(module, work = state.work) {
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
return canWriteUiModule(work, module);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function canReadModule(module, work = state.work) {
|
|
107
|
+
return canReadUiModule(work, module);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function canReadAggregateContent(work = state.work) {
|
|
111
|
+
return ["editor", "settings", "characters", "races", "organizations", "timeline", "relationships", "outlines"]
|
|
112
|
+
.every((module) => canReadModule(module, work));
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
function applyWorkAccessMode() {
|
|
101
116
|
const viewOnly = Boolean(state.work) && !canEditWork();
|
|
102
|
-
const settingsOnly = String(state.work?.accessRole) === "settings-editor";
|
|
103
117
|
const proseReadOnly = Boolean(state.work) && !canEditProse();
|
|
118
|
+
const proseHidden = Boolean(state.work) && !canReadModule("editor");
|
|
119
|
+
const aiHidden = Boolean(state.work) && !canReadModule("tasks");
|
|
120
|
+
const aiReadOnly = Boolean(state.work) && !canEditModule("tasks");
|
|
121
|
+
const moduleReadOnly = Boolean(state.work) && !canEditModule(state.module);
|
|
104
122
|
$("#app").classList.toggle("view-only-mode", viewOnly);
|
|
105
|
-
$("#app").classList.toggle("settings-only-mode", settingsOnly);
|
|
106
123
|
$("#app").classList.toggle("prose-read-only-mode", proseReadOnly);
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
$("#app").classList.toggle("prose-hidden-mode", proseHidden);
|
|
125
|
+
$("#app").classList.toggle("ai-hidden-mode", aiHidden);
|
|
126
|
+
document.body.classList.toggle("work-viewer-mode", moduleReadOnly);
|
|
127
|
+
for (const item of WORK_PERMISSION_MODULES) {
|
|
128
|
+
const button = $(`#module-nav [data-module="${item.uiModule}"]`);
|
|
129
|
+
if (button) button.classList.toggle("permission-hidden", !canReadModule(item.uiModule));
|
|
130
|
+
}
|
|
131
|
+
$("#module-nav [data-work-settings]").classList.toggle("permission-hidden", Boolean(state.work) && !canManageWork());
|
|
132
|
+
$("#import-file-button").classList.toggle("permission-hidden", proseReadOnly);
|
|
133
|
+
$("#import-file-button").setAttribute("aria-hidden", String(proseReadOnly));
|
|
134
|
+
$("#import-file").disabled = proseReadOnly;
|
|
135
|
+
$("#import-history-button").classList.toggle("permission-hidden", Boolean(state.work) && !canReplaceProse());
|
|
136
|
+
$(".ai-panel").classList.toggle("permission-hidden", aiHidden);
|
|
109
137
|
$("#chapter-title").readOnly = proseReadOnly;
|
|
110
138
|
$("#chapter-content").readOnly = proseReadOnly;
|
|
111
139
|
$("#chapter-title").setAttribute("aria-readonly", String(proseReadOnly));
|
|
112
140
|
$("#chapter-content").setAttribute("aria-readonly", String(proseReadOnly));
|
|
141
|
+
$("#ai-prompt").readOnly = aiReadOnly;
|
|
142
|
+
$("#ai-prompt").setAttribute("aria-readonly", String(aiReadOnly));
|
|
143
|
+
$("#ai-send").classList.toggle("permission-hidden", aiReadOnly);
|
|
113
144
|
if (proseReadOnly) {
|
|
114
145
|
cancelChapterAutoSave();
|
|
115
146
|
state.dirty = false;
|
|
@@ -162,6 +193,8 @@ const panelLayoutStorageKey = "ai-novel-panel-layout-v1";
|
|
|
162
193
|
const panelLayoutDefaults = Object.freeze({ leftWidth: 280, aiWidth: 360, leftCollapsed: false, aiCollapsed: false });
|
|
163
194
|
let restoringPageRoute = true;
|
|
164
195
|
let memberDialogWork = null;
|
|
196
|
+
let memberDialogMembers = [];
|
|
197
|
+
let memberDialogDirectory = [];
|
|
165
198
|
let onboardingStep = 0;
|
|
166
199
|
let onboardingAutoScheduled = false;
|
|
167
200
|
let onboardingPositionFrame = null;
|
|
@@ -175,6 +208,10 @@ let aiReferencesLoadPromise = null;
|
|
|
175
208
|
let aiReferencesLoadWorkId = null;
|
|
176
209
|
let aiConversationsLoadPromise = null;
|
|
177
210
|
let aiConversationsLoadWorkId = null;
|
|
211
|
+
let workScopedUiGeneration = 0;
|
|
212
|
+
let importHistoryRecords = [];
|
|
213
|
+
let importHistoryNextPage = null;
|
|
214
|
+
let importHistoryRequestId = 0;
|
|
178
215
|
|
|
179
216
|
const shelfOnboardingSteps = [
|
|
180
217
|
{ selector: "#home-button", eyebrow: "作品入口", title: "这里是你的创作书架", description: "点击左上角的叙界标志,可以随时回到书架,在不同作品之间切换。", placement: "bottom" },
|
|
@@ -510,7 +547,8 @@ function showEntityEditorPage(type) {
|
|
|
510
547
|
}
|
|
511
548
|
|
|
512
549
|
function markEntityEditorDirty() {
|
|
513
|
-
|
|
550
|
+
const module = entityEditorType === "setting" ? "settings" : "characters";
|
|
551
|
+
if (entityEditorType && canEditModule(module)) entityEditorDirty = true;
|
|
514
552
|
}
|
|
515
553
|
|
|
516
554
|
function confirmEntityEditorDiscard(message) {
|
|
@@ -1082,8 +1120,9 @@ function renderAiConversationHistory() {
|
|
|
1082
1120
|
async function loadAiConversations(openLatest = true) {
|
|
1083
1121
|
const workId = state.work?.id;
|
|
1084
1122
|
if (!workId) return;
|
|
1123
|
+
const generation = workScopedUiGeneration;
|
|
1085
1124
|
const conversations = (await apiPage(`/api/works/${workId}/ai-conversations`)).items;
|
|
1086
|
-
if (state.work?.id !== workId) return;
|
|
1125
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
1087
1126
|
state.aiConversations = conversations;
|
|
1088
1127
|
loadedAiConversationsWorkId = workId;
|
|
1089
1128
|
renderAiConversationHistory();
|
|
@@ -1774,8 +1813,11 @@ function confirmDiscardChanges(message = "当前章节有未保存修改,继
|
|
|
1774
1813
|
|
|
1775
1814
|
function chooseExistingWorkImportMode(file) {
|
|
1776
1815
|
const dialog = $("#import-mode-dialog");
|
|
1816
|
+
const canOverwrite = canReplaceProse();
|
|
1777
1817
|
$("#import-mode-file-summary").textContent = `文件:${file.name};当前作品:《${state.work.title}》`;
|
|
1778
1818
|
$("#import-mode-unsaved-warning").classList.toggle("hidden", !state.dirty);
|
|
1819
|
+
$("#import-mode-overwrite").disabled = !canOverwrite;
|
|
1820
|
+
$("#import-mode-overwrite-permission").classList.toggle("hidden", canOverwrite);
|
|
1779
1821
|
dialog.returnValue = "cancel";
|
|
1780
1822
|
dialog.showModal();
|
|
1781
1823
|
return new Promise((resolve) => {
|
|
@@ -1905,13 +1947,14 @@ function captureSettingsReturnContext() {
|
|
|
1905
1947
|
function renderSettingsHub() {
|
|
1906
1948
|
const hasWork = Boolean(state.work);
|
|
1907
1949
|
const canManageWork = hasWork && ["admin", "owner"].includes(String(state.work.accessRole));
|
|
1950
|
+
const canReadAggregate = hasWork && canReadAggregateContent();
|
|
1908
1951
|
const isAdmin = state.user?.role === "admin";
|
|
1909
1952
|
$("#platform-ai-button").classList.toggle("hidden", !isAdmin);
|
|
1910
1953
|
$("#user-management-button").classList.toggle("hidden", !isAdmin);
|
|
1911
1954
|
$("#platform-ui-settings-button").classList.toggle("hidden", !isAdmin);
|
|
1912
1955
|
$("#collaboration-button").disabled = !canManageWork;
|
|
1913
|
-
$("#top-search-button").disabled = !
|
|
1914
|
-
$("#export-button").disabled = !
|
|
1956
|
+
$("#top-search-button").disabled = !canReadAggregate;
|
|
1957
|
+
$("#export-button").disabled = !canReadAggregate;
|
|
1915
1958
|
$("#settings-return").textContent = settingsReturnContext?.view === "shelf" || !hasWork ? "返回书架" : "返回当前作品";
|
|
1916
1959
|
$("#settings-work-note").textContent = hasWork
|
|
1917
1960
|
? `当前作品:《${state.work.title}》。导出将作用于这部作品。`
|
|
@@ -1973,61 +2016,99 @@ async function openPlatformUiSettingsDialog() {
|
|
|
1973
2016
|
}
|
|
1974
2017
|
}
|
|
1975
2018
|
|
|
2019
|
+
function renderMemberPermissionGrid(value) {
|
|
2020
|
+
const permissions = normalizeModulePermissions(value, "custom");
|
|
2021
|
+
$("#member-permission-grid").innerHTML = WORK_PERMISSION_MODULES.map((item) => `<label class="member-permission-row">
|
|
2022
|
+
<span>${esc(item.label)}</span>
|
|
2023
|
+
<select data-member-permission="${esc(item.id)}" aria-label="${esc(item.label)}权限">
|
|
2024
|
+
<option value="none" ${permissions[item.id] === "none" ? "selected" : ""}>无权限</option>
|
|
2025
|
+
<option value="read" ${permissions[item.id] === "read" ? "selected" : ""}>只读</option>
|
|
2026
|
+
<option value="write" ${permissions[item.id] === "write" ? "selected" : ""}>可编辑</option>
|
|
2027
|
+
</select>
|
|
2028
|
+
</label>`).join("");
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function selectedMemberPermissions() {
|
|
2032
|
+
const permissions = emptyModulePermissions();
|
|
2033
|
+
$("#member-permission-grid").querySelectorAll("[data-member-permission]").forEach((select) => {
|
|
2034
|
+
permissions[select.dataset.memberPermission] = select.value;
|
|
2035
|
+
});
|
|
2036
|
+
return permissions;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
function selectMemberForConfiguration(userId) {
|
|
2040
|
+
const fieldset = $("#member-permission-fieldset");
|
|
2041
|
+
const member = memberDialogMembers.find((item) => item.userId === userId);
|
|
2042
|
+
fieldset.disabled = !userId;
|
|
2043
|
+
renderMemberPermissionGrid(member?.permissions ?? emptyModulePermissions());
|
|
2044
|
+
$("#member-permission-submit").textContent = member ? "更新模块权限" : "添加成员并保存";
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function renderMemberSelector(selectedUserId = "") {
|
|
2048
|
+
const ownerIds = new Set(memberDialogMembers.filter((member) => member.role === "owner").map((member) => member.userId));
|
|
2049
|
+
const people = new Map(memberDialogDirectory.map((user) => [user.userId, user]));
|
|
2050
|
+
for (const member of memberDialogMembers) if (member.role !== "owner") people.set(member.userId, member);
|
|
2051
|
+
const options = [...people.values()].filter((user) => !ownerIds.has(user.userId));
|
|
2052
|
+
$("#member-user-select").innerHTML = options.length
|
|
2053
|
+
? `<option value="">选择用户</option>${options.map((user) => {
|
|
2054
|
+
const existing = memberDialogMembers.some((member) => member.userId === user.userId && member.role !== "owner");
|
|
2055
|
+
return `<option value="${esc(user.userId)}" ${selectedUserId === user.userId ? "selected" : ""}>${esc(user.displayName)} · @${esc(user.username)}${existing ? " · 已加入" : ""}</option>`;
|
|
2056
|
+
}).join("")}`
|
|
2057
|
+
: '<option value="">没有可配置的用户</option>';
|
|
2058
|
+
$("#member-user-select").disabled = !options.length;
|
|
2059
|
+
selectMemberForConfiguration(selectedUserId);
|
|
2060
|
+
}
|
|
2061
|
+
|
|
1976
2062
|
function renderMembers(members) {
|
|
2063
|
+
memberDialogMembers = members;
|
|
1977
2064
|
const work = memberDialogWork ?? state.work;
|
|
1978
2065
|
const canManage = ["admin", "owner"].includes(String(work?.accessRole));
|
|
1979
|
-
$("#members-list").innerHTML = members.map((member) =>
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2066
|
+
$("#members-list").innerHTML = members.map((member) => {
|
|
2067
|
+
const descriptionId = `member-role-summary-${member.userId}`;
|
|
2068
|
+
return `<article class="access-row">
|
|
2069
|
+
<div class="access-person">${userAvatarHtml(member, "access-avatar")}<div class="access-person-copy"><strong>${esc(member.displayName)} · @${esc(member.username)}</strong><small id="${esc(descriptionId)}">${member.role === "owner" ? "作品创建者 · 拥有全部模块管理权限" : esc(permissionSummary(member.permissions))}${member.status === "disabled" ? " · 已停用" : ""}</small></div></div>
|
|
2070
|
+
${member.role === "owner" || !canManage ? "<span></span>" : `<button type="button" data-configure-member="${esc(member.userId)}" aria-describedby="${esc(descriptionId)}">配置权限</button>`}
|
|
2071
|
+
${member.role === "owner" || !canManage ? "<span></span>" : `<button type="button" data-remove-member="${esc(member.userId)}">移除</button>`}
|
|
2072
|
+
</article>`;
|
|
2073
|
+
}).join("");
|
|
1984
2074
|
bindUserAvatarFallbacks($("#members-list"));
|
|
1985
|
-
$("#members-list").querySelectorAll("[data-member
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
const updated = await api(`/api/works/${encodeURIComponent(work.id)}/members/${encodeURIComponent(select.dataset.memberRole)}`, { method: "PATCH", body: { role: select.value } });
|
|
1990
|
-
renderMembers(updated);
|
|
1991
|
-
toast("成员权限已更新");
|
|
1992
|
-
} catch (error) {
|
|
1993
|
-
select.value = previousRole;
|
|
1994
|
-
toast(error.message, "error");
|
|
1995
|
-
}
|
|
2075
|
+
$("#members-list").querySelectorAll("[data-configure-member]").forEach((button) => button.addEventListener("click", () => {
|
|
2076
|
+
$("#member-user-select").value = button.dataset.configureMember;
|
|
2077
|
+
selectMemberForConfiguration(button.dataset.configureMember);
|
|
2078
|
+
$("#member-user-select").focus();
|
|
1996
2079
|
}));
|
|
1997
2080
|
$("#members-list").querySelectorAll("[data-remove-member]").forEach((button) => button.addEventListener("click", async () => {
|
|
1998
2081
|
if (!work) return;
|
|
1999
2082
|
try {
|
|
2000
2083
|
const updated = await api(`/api/works/${encodeURIComponent(work.id)}/members/${encodeURIComponent(button.dataset.removeMember)}`, { method: "DELETE" });
|
|
2001
2084
|
renderMembers(updated);
|
|
2002
|
-
|
|
2085
|
+
renderMemberSelector();
|
|
2003
2086
|
toast("协作者已移除");
|
|
2004
2087
|
} catch (error) { toast(error.message, "error"); }
|
|
2005
2088
|
}));
|
|
2006
2089
|
}
|
|
2007
2090
|
|
|
2008
|
-
async function fillMemberCandidates(members) {
|
|
2009
|
-
const directory = await api("/api/users/directory");
|
|
2010
|
-
const memberIds = new Set(members.map((member) => member.userId));
|
|
2011
|
-
const candidates = directory.filter((user) => !memberIds.has(user.userId));
|
|
2012
|
-
$("#member-user-select").innerHTML = candidates.length
|
|
2013
|
-
? `<option value="">选择用户</option>${candidates.map((user) => `<option value="${esc(user.userId)}">${esc(user.displayName)} · @${esc(user.username)}</option>`).join("")}`
|
|
2014
|
-
: '<option value="">没有可邀请的用户</option>';
|
|
2015
|
-
$("#member-user-select").disabled = !candidates.length;
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
2091
|
async function openMembersDialog(targetWork = state.work) {
|
|
2019
2092
|
if (!targetWork) return;
|
|
2020
2093
|
memberDialogWork = targetWork;
|
|
2021
2094
|
const canManage = ["admin", "owner"].includes(String(targetWork.accessRole));
|
|
2022
2095
|
$("#members-dialog-eyebrow").textContent = `作品权限 · 《${targetWork.title}》`;
|
|
2023
|
-
$("#members-dialog-title").textContent = "
|
|
2096
|
+
$("#members-dialog-title").textContent = "成员模块权限";
|
|
2024
2097
|
$("#members-list").innerHTML = '<p class="empty-state">正在读取成员……</p>';
|
|
2025
|
-
$("#member-
|
|
2098
|
+
$("#member-permission-form").classList.toggle("hidden", !canManage);
|
|
2099
|
+
memberDialogMembers = [];
|
|
2100
|
+
memberDialogDirectory = [];
|
|
2101
|
+
renderMemberPermissionGrid(emptyModulePermissions());
|
|
2102
|
+
$("#member-permission-fieldset").disabled = true;
|
|
2026
2103
|
$("#members-dialog").showModal();
|
|
2027
2104
|
try {
|
|
2028
|
-
const members = await
|
|
2105
|
+
const [members, directory] = await Promise.all([
|
|
2106
|
+
api(`/api/works/${encodeURIComponent(targetWork.id)}/members`),
|
|
2107
|
+
canManage ? api("/api/users/directory") : Promise.resolve([])
|
|
2108
|
+
]);
|
|
2109
|
+
memberDialogDirectory = directory;
|
|
2029
2110
|
renderMembers(members);
|
|
2030
|
-
if (canManage)
|
|
2111
|
+
if (canManage) renderMemberSelector();
|
|
2031
2112
|
} catch (error) { $("#members-dialog").close(); toast(error.message, "error"); }
|
|
2032
2113
|
}
|
|
2033
2114
|
|
|
@@ -2181,7 +2262,7 @@ function renderShelf() {
|
|
|
2181
2262
|
<span class="book-cover-fallback">${esc(Array.from(work.title)[0] ?? "书")}</span>
|
|
2182
2263
|
${work.coverUrl ? `<img src="${esc(work.coverUrl)}" alt="${esc(work.title)} 封面">` : ""}
|
|
2183
2264
|
</span>
|
|
2184
|
-
<span class="book-info"><strong>${esc(work.title)}</strong><small>${esc(work.author || "未署名")} · ${work.chapterCount} 章 · ${work.wordCount} 字</small><span>${esc(work.description || "尚未填写作品简介")}</span><em class="book-access-badge">${work.accessRole === "viewer" ? "
|
|
2265
|
+
<span class="book-info"><strong>${esc(work.title)}</strong><small>${esc(work.author || "未署名")} · ${work.chapterCount} 章 · ${work.wordCount} 字</small><span>${esc(work.description || "尚未填写作品简介")}</span><em class="book-access-badge">${work.accessRole === "viewer" ? "全部只读" : work.accessRole === "settings-editor" ? "设定协作" : work.accessRole === "editor" ? "全部可编辑" : work.accessRole === "custom" ? "自定义权限" : work.accessRole === "admin" ? "管理员访问" : "我的作品"}</em></span>
|
|
2185
2266
|
</button>
|
|
2186
2267
|
${canManageWork(work) ? `<button class="book-card-settings" type="button" data-edit-work="${esc(work.id)}" aria-label="作品设置" title="作品设置">设置</button>` : ""}
|
|
2187
2268
|
</article>`).join("")}
|
|
@@ -2194,34 +2275,45 @@ function renderShelf() {
|
|
|
2194
2275
|
$("#book-add-card").addEventListener("click", openWorkDialog);
|
|
2195
2276
|
}
|
|
2196
2277
|
|
|
2278
|
+
function resetWorkScopedUiCaches() {
|
|
2279
|
+
workScopedUiGeneration += 1;
|
|
2280
|
+
loadedAiModelsWorkId = null;
|
|
2281
|
+
loadedAiReferencesWorkId = null;
|
|
2282
|
+
loadedAiConversationsWorkId = null;
|
|
2283
|
+
aiModelsLoadPromise = null;
|
|
2284
|
+
aiModelsLoadWorkId = null;
|
|
2285
|
+
aiReferencesLoadPromise = null;
|
|
2286
|
+
aiReferencesLoadWorkId = null;
|
|
2287
|
+
aiConversationsLoadPromise = null;
|
|
2288
|
+
aiConversationsLoadWorkId = null;
|
|
2289
|
+
state.models = [];
|
|
2290
|
+
state.characters = [];
|
|
2291
|
+
state.settings = [];
|
|
2292
|
+
state.collapsedVolumeIds.clear();
|
|
2293
|
+
lastSavedChapterSnapshot = null;
|
|
2294
|
+
if (aiContextUsageTimer !== null) clearTimeout(aiContextUsageTimer);
|
|
2295
|
+
aiContextUsageTimer = null;
|
|
2296
|
+
aiContextUsageRequest += 1;
|
|
2297
|
+
state.aiCitations = [];
|
|
2298
|
+
state.aiReferences = [];
|
|
2299
|
+
state.aiPromptSent = false;
|
|
2300
|
+
state.aiConversationId = null;
|
|
2301
|
+
state.aiConversations = [];
|
|
2302
|
+
renderAiCitations();
|
|
2303
|
+
renderAiReferences();
|
|
2304
|
+
renderAiQuickActions();
|
|
2305
|
+
resetAiFeed();
|
|
2306
|
+
$("#ai-conversation-title").textContent = "新对话";
|
|
2307
|
+
$("#ai-model").innerHTML = '<option value="">使用创作助手时加载模型</option>';
|
|
2308
|
+
setAiContextMeter(null);
|
|
2309
|
+
renderAiConversationHistory();
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2197
2312
|
async function selectWork(workId, preferredChapterId = null) {
|
|
2198
2313
|
const discarding = state.work?.id !== workId && state.dirty;
|
|
2199
2314
|
if (discarding && !confirmDiscardChanges()) return false;
|
|
2200
2315
|
const nextWork = await api(`/api/works/${workId}?page=1&limit=100`);
|
|
2201
|
-
if (state.work?.id !== nextWork.id)
|
|
2202
|
-
loadedAiModelsWorkId = null;
|
|
2203
|
-
loadedAiReferencesWorkId = null;
|
|
2204
|
-
loadedAiConversationsWorkId = null;
|
|
2205
|
-
state.models = [];
|
|
2206
|
-
state.characters = [];
|
|
2207
|
-
state.settings = [];
|
|
2208
|
-
if (aiContextUsageTimer !== null) clearTimeout(aiContextUsageTimer);
|
|
2209
|
-
aiContextUsageTimer = null;
|
|
2210
|
-
aiContextUsageRequest += 1;
|
|
2211
|
-
state.aiCitations = [];
|
|
2212
|
-
state.aiReferences = [];
|
|
2213
|
-
state.aiPromptSent = false;
|
|
2214
|
-
state.aiConversationId = null;
|
|
2215
|
-
state.aiConversations = [];
|
|
2216
|
-
renderAiCitations();
|
|
2217
|
-
renderAiReferences();
|
|
2218
|
-
renderAiQuickActions();
|
|
2219
|
-
resetAiFeed();
|
|
2220
|
-
$("#ai-conversation-title").textContent = "新对话";
|
|
2221
|
-
$("#ai-model").innerHTML = '<option value="">使用创作助手时加载模型</option>';
|
|
2222
|
-
setAiContextMeter(null);
|
|
2223
|
-
renderAiConversationHistory();
|
|
2224
|
-
}
|
|
2316
|
+
if (state.work?.id !== nextWork.id) resetWorkScopedUiCaches();
|
|
2225
2317
|
if (discarding) setSaveState("就绪");
|
|
2226
2318
|
$("#app").classList.remove("shelf-mode");
|
|
2227
2319
|
$("#shelf-view").classList.add("hidden");
|
|
@@ -2231,16 +2323,18 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
2231
2323
|
settingsReturnContext = null;
|
|
2232
2324
|
state.work = nextWork;
|
|
2233
2325
|
state.chapter = null;
|
|
2326
|
+
if (!canReadModule(state.module)) state.module = firstReadableUiModule(state.work) ?? "editor";
|
|
2234
2327
|
applyWorkAccessMode();
|
|
2235
2328
|
updateDocumentTitle(state.work);
|
|
2236
2329
|
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${state.work.wordCount} 字`;
|
|
2237
|
-
$("#top-search-button").disabled =
|
|
2330
|
+
$("#top-search-button").disabled = !canReadAggregateContent();
|
|
2238
2331
|
renderTree();
|
|
2239
2332
|
const chapters = state.work.volumes.flatMap((volume) => volume.chapters);
|
|
2240
2333
|
const targetChapter = chapters.find((chapter) => chapter.id === preferredChapterId) ?? chapters[0];
|
|
2241
2334
|
if (state.module === "editor" && preferredChapterId) await selectChapter(preferredChapterId);
|
|
2242
2335
|
else if (state.module === "editor" && targetChapter) await selectChapter(targetChapter.id);
|
|
2243
|
-
else if (state.module === "editor") showWelcome(true);
|
|
2336
|
+
else if (state.module === "editor" && canReadModule("editor")) showWelcome(true);
|
|
2337
|
+
else if (!canReadModule(state.module)) showWelcome(true);
|
|
2244
2338
|
else await showModule(state.module);
|
|
2245
2339
|
return true;
|
|
2246
2340
|
}
|
|
@@ -2310,6 +2404,7 @@ async function selectChapter(chapterId) {
|
|
|
2310
2404
|
state.chapter = await api(`/api/chapters/${chapterId}`);
|
|
2311
2405
|
lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
|
|
2312
2406
|
state.module = "editor";
|
|
2407
|
+
applyWorkAccessMode();
|
|
2313
2408
|
markActiveModule("editor");
|
|
2314
2409
|
$("#welcome-view").classList.add("hidden");
|
|
2315
2410
|
$("#module-view").classList.add("hidden");
|
|
@@ -2326,7 +2421,7 @@ async function selectChapter(chapterId) {
|
|
|
2326
2421
|
scheduleChapterLineNumbers();
|
|
2327
2422
|
$("#chapter-insight").classList.add("hidden");
|
|
2328
2423
|
updateChapterStats();
|
|
2329
|
-
if (!canEditProse()) setSaveState(
|
|
2424
|
+
if (!canEditProse()) setSaveState("正文只读");
|
|
2330
2425
|
else if (spacingChanged) scheduleChapterAutoSave(120);
|
|
2331
2426
|
else setSaveState("已保存");
|
|
2332
2427
|
renderTree();
|
|
@@ -2384,10 +2479,18 @@ const moduleMeta = {
|
|
|
2384
2479
|
|
|
2385
2480
|
async function showModule(module) {
|
|
2386
2481
|
if (!state.work) return showWelcome();
|
|
2387
|
-
if (!
|
|
2482
|
+
if (!canReadModule(module)) {
|
|
2483
|
+
const fallback = firstReadableUiModule(state.work);
|
|
2484
|
+
if (!fallback) {
|
|
2485
|
+
showWelcome(true);
|
|
2486
|
+
return toast("当前账户尚未获授权访问任何作品模块", "error");
|
|
2487
|
+
}
|
|
2488
|
+
module = fallback;
|
|
2489
|
+
}
|
|
2388
2490
|
if (module !== "editor" && state.module === "editor" && !confirmDiscardChanges()) return;
|
|
2389
2491
|
if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
|
|
2390
2492
|
state.module = module;
|
|
2493
|
+
applyWorkAccessMode();
|
|
2391
2494
|
markActiveModule(module);
|
|
2392
2495
|
if (module === "editor") {
|
|
2393
2496
|
if (state.chapter) await selectChapter(state.chapter.id);
|
|
@@ -2545,10 +2648,10 @@ async function renderSettings() {
|
|
|
2545
2648
|
async function renderCharacters() {
|
|
2546
2649
|
[state.characters, state.races, state.organizations] = await Promise.all([
|
|
2547
2650
|
apiPage(`/api/works/${state.work.id}/characters`).then((result) => result.items),
|
|
2548
|
-
apiAllPages(`/api/works/${state.work.id}/races`),
|
|
2549
|
-
apiAllPages(`/api/works/${state.work.id}/organizations`)
|
|
2651
|
+
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
2652
|
+
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
2550
2653
|
]);
|
|
2551
|
-
const auditPanel = `<section class="character-audit-panel"><div><strong>角色身份确认</strong><small>让 AI 查询角色档案并搜索正文,找出可能被误建成两个档案的同一角色。AI 只提交审核建议,不会自动合并。</small></div><button id="create-character-audit-task" class="ghost-button" type="button" ${state.characters.length < 2 ? "disabled" : ""}>AI 角色查重</button></section
|
|
2654
|
+
const auditPanel = canEditModule("tasks") ? `<section class="character-audit-panel"><div><strong>角色身份确认</strong><small>让 AI 查询角色档案并搜索正文,找出可能被误建成两个档案的同一角色。AI 只提交审核建议,不会自动合并。</small></div><button id="create-character-audit-task" class="ghost-button" type="button" ${state.characters.length < 2 ? "disabled" : ""}>AI 角色查重</button></section>` : "";
|
|
2552
2655
|
$("#module-content").innerHTML = auditPanel + (state.characters.length ? `<div class="card-grid">${state.characters.map((item) => {
|
|
2553
2656
|
const details = normalizeCharacterDetails(item.attributes?.details);
|
|
2554
2657
|
return `
|
|
@@ -2560,7 +2663,7 @@ async function renderCharacters() {
|
|
|
2560
2663
|
<div class="organization-links"><b>所属组织</b>${(item.organizations ?? []).length ? item.organizations.map((organization) => `<span class="pill organization-pill">${esc(organization.name)}</span>`).join("") : '<span class="organization-empty">未加入组织</span>'}</div>
|
|
2561
2664
|
${item.profile?.summary ? `<p class="character-summary">${esc(item.profile.summary)}</p>` : `<p>${esc(Object.entries(item.currentState).map(([key, value]) => `${key}:${value}`).join("\n") || "尚未记录当前状态")}</p>`}
|
|
2562
2665
|
${item.profileSectionCount ? `<small class="character-section-count">${item.profileSectionCount} 个设定章节</small>` : ""}
|
|
2563
|
-
<div class="card-actions"><button data-edit-character="${esc(item.id)}">编辑</button>${
|
|
2666
|
+
<div class="card-actions"><button data-edit-character="${esc(item.id)}">编辑</button>${canEditModule("characters") && state.characters.length > 1 ? `<button data-merge-character="${esc(item.id)}">合并</button>` : ""}${canEditModule("characters") ? `<button class="danger-button" data-delete-character="${esc(item.id)}">删除</button>` : ""}</div></article>`;
|
|
2564
2667
|
}).join("")}</div>`
|
|
2565
2668
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
2566
2669
|
$("#create-character-audit-task")?.addEventListener("click", async () => {
|
|
@@ -2618,7 +2721,7 @@ async function renderCharacters() {
|
|
|
2618
2721
|
async function renderRaces() {
|
|
2619
2722
|
[state.races, state.characters] = await Promise.all([
|
|
2620
2723
|
apiAllPages(`/api/works/${state.work.id}/races`),
|
|
2621
|
-
apiAllPages(`/api/works/${state.work.id}/characters`)
|
|
2724
|
+
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
2622
2725
|
]);
|
|
2623
2726
|
const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
|
|
2624
2727
|
<summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
|
|
@@ -2628,7 +2731,7 @@ async function renderRaces() {
|
|
|
2628
2731
|
<p>${esc(item.description || "尚未填写种族简介")}</p>
|
|
2629
2732
|
<div class="race-settings">${item.effectiveSettings.length ? item.effectiveSettings.map((setting) => `<span class="pill${setting.inherited ? " inherited" : ""}" title="${esc(setting.inherited ? `继承自 ${setting.sourceRaceName}` : `定义于 ${setting.sourceRaceName}`)}">${esc(setting.value)}<small>${esc(setting.sourceRaceName)}</small></span>`).join("") : '<span class="pill">暂无共同设定</span>'}</div>
|
|
2630
2733
|
<p class="race-members">直接角色:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
2631
|
-
<div class="card-actions"><button data-edit-race="${esc(item.id)}">编辑</button><button data-entity-history="race" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${
|
|
2734
|
+
<div class="card-actions"><button data-edit-race="${esc(item.id)}">编辑</button><button data-entity-history="race" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${canEditModule("races") && state.races.length > 1 ? `<button data-merge-race="${esc(item.id)}">合并</button>` : ""}${canEditModule("races") ? `<button class="danger-button" data-delete-race="${esc(item.id)}">删除</button>` : ""}</div>
|
|
2632
2735
|
</article>
|
|
2633
2736
|
${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
|
|
2634
2737
|
</div>
|
|
@@ -2665,14 +2768,14 @@ async function renderRaces() {
|
|
|
2665
2768
|
async function renderOrganizations() {
|
|
2666
2769
|
[state.organizations, state.characters] = await Promise.all([
|
|
2667
2770
|
apiAllPages(`/api/works/${state.work.id}/organizations`),
|
|
2668
|
-
apiAllPages(`/api/works/${state.work.id}/characters`)
|
|
2771
|
+
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
2669
2772
|
]);
|
|
2670
2773
|
$("#module-content").innerHTML = state.organizations.length ? `<div class="card-grid organization-grid">${state.organizations.map((item) => `
|
|
2671
2774
|
<article class="record-card organization-card"><small>${item.memberIds.length} 位成员 · ${item.settings.length} 条设定</small>
|
|
2672
2775
|
<h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
2673
2776
|
<div class="organization-settings">${item.settings.map((setting) => `<span class="pill">${esc(setting)}</span>`).join("") || '<span class="pill">暂无组织设定</span>'}</div>
|
|
2674
2777
|
<p class="organization-members">成员:${item.members.length ? item.members.map((member) => esc(member.name)).join("、") : "暂无绑定角色"}</p>
|
|
2675
|
-
<div class="card-actions"><button data-edit-organization="${esc(item.id)}">编辑</button><button data-entity-history="organization" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${
|
|
2778
|
+
<div class="card-actions"><button data-edit-organization="${esc(item.id)}">编辑</button><button data-entity-history="organization" data-entity-id="${esc(item.id)}" data-entity-title="${esc(item.name)}">版本历史</button>${canEditModule("organizations") && state.organizations.length > 1 ? `<button data-merge-organization="${esc(item.id)}">合并</button>` : ""}${canEditModule("organizations") ? `<button class="danger-button" data-delete-organization="${esc(item.id)}">删除</button>` : ""}</div>
|
|
2676
2779
|
</article>`).join("")}</div>` : emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
|
|
2677
2780
|
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => openOrganizationDialog(state.organizations.find((item) => item.id === button.dataset.editOrganization))));
|
|
2678
2781
|
$("#module-content").querySelectorAll("[data-merge-organization]").forEach((button) => button.addEventListener("click", () => {
|
|
@@ -2767,7 +2870,7 @@ async function renderOutlines() {
|
|
|
2767
2870
|
}
|
|
2768
2871
|
|
|
2769
2872
|
async function renderRelationships() {
|
|
2770
|
-
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
2873
|
+
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
2771
2874
|
const relationships = (await apiPage(`/api/works/${state.work.id}/relationships`)).items;
|
|
2772
2875
|
const nameOf = (id) => state.characters.find((item) => item.id === id)?.name ?? "未知角色";
|
|
2773
2876
|
state.galaxy?.destroy();
|
|
@@ -2798,9 +2901,13 @@ async function renderRelationships() {
|
|
|
2798
2901
|
}
|
|
2799
2902
|
|
|
2800
2903
|
async function renderReviews() {
|
|
2904
|
+
const canReadCharacters = canReadModule("characters");
|
|
2905
|
+
const canResolveReview = canEditModule("reviews");
|
|
2906
|
+
const canMergeCharacters = canResolveReview
|
|
2907
|
+
&& ["characters", "races", "organizations", "timeline", "relationships"].every((module) => canEditModule(module));
|
|
2801
2908
|
const [reviews, characters] = await Promise.all([
|
|
2802
2909
|
apiPage(`/api/works/${state.work.id}/reviews`).then((result) => result.items),
|
|
2803
|
-
apiAllPages(`/api/works/${state.work.id}/characters?includeMerged=1`)
|
|
2910
|
+
canReadCharacters ? apiAllPages(`/api/works/${state.work.id}/characters?includeMerged=1`) : Promise.resolve([])
|
|
2804
2911
|
]);
|
|
2805
2912
|
const characterById = new Map(characters.map((character) => [character.id, character]));
|
|
2806
2913
|
const duplicateCard = (item) => {
|
|
@@ -2808,17 +2915,21 @@ async function renderReviews() {
|
|
|
2808
2915
|
const sides = refs.map((reference) => ({ reference, character: characterById.get(reference.id) }));
|
|
2809
2916
|
const sideHtml = sides.map(({ character }) => `<section><strong>${esc(character.name)}</strong><small>v${esc(String(character.versionNo))} · ${esc(character.species || "种族未知")}</small><div>${character.aliases.map((alias) => `<span class="pill">${esc(alias)}</span>`).join("") || '<span class="organization-empty">无别名</span>'}</div><p>${esc(character.attributes?.identity || character.profile?.summary || "尚未记录身份说明")}</p></section>`).join("");
|
|
2810
2917
|
const evidenceHtml = (item.evidence ?? []).map((evidence) => `<li><strong>${esc(evidence.chapterTitle || evidence.chapterId || "原文")}</strong><q>${esc(evidence.quote || "")}</q>${evidence.supports ? `<small>${esc(evidence.supports)}</small>` : ""}</li>`).join("");
|
|
2811
|
-
const
|
|
2918
|
+
const mergeActions = item.status === "pending" && sides.length === 2 && canMergeCharacters ? `
|
|
2812
2919
|
<button data-merge-review="${esc(item.id)}" data-merge-target="${esc(sides[0].character.id)}" data-merge-source="${esc(sides[1].character.id)}" data-target-version="${esc(String(sides[0].reference.versionNo))}" data-source-version="${esc(String(sides[1].reference.versionNo))}">合并为 ${esc(sides[0].character.name)}</button>
|
|
2813
|
-
<button data-merge-review="${esc(item.id)}" data-merge-target="${esc(sides[1].character.id)}" data-merge-source="${esc(sides[0].character.id)}" data-target-version="${esc(String(sides[1].reference.versionNo))}" data-source-version="${esc(String(sides[0].reference.versionNo))}">合并为 ${esc(sides[1].character.name)}</button
|
|
2814
|
-
|
|
2815
|
-
|
|
2920
|
+
<button data-merge-review="${esc(item.id)}" data-merge-target="${esc(sides[1].character.id)}" data-merge-source="${esc(sides[0].character.id)}" data-target-version="${esc(String(sides[1].reference.versionNo))}" data-source-version="${esc(String(sides[0].reference.versionNo))}">合并为 ${esc(sides[1].character.name)}</button>` : "";
|
|
2921
|
+
const keepSeparateAction = item.status === "pending" && canResolveReview
|
|
2922
|
+
? `<button data-keep-characters-separate="${esc(item.id)}">确认是不同角色</button>`
|
|
2923
|
+
: "";
|
|
2924
|
+
const actions = mergeActions || keepSeparateAction
|
|
2925
|
+
? `<div class="card-actions character-duplicate-actions">${mergeActions}${keepSeparateAction}</div>`
|
|
2926
|
+
: "";
|
|
2816
2927
|
return `<article class="record-card character-duplicate-review"><small>角色查重 · ${esc(item.severity)} · ${esc(item.status)}</small><h3>${esc(item.title)}</h3><div class="character-duplicate-pair">${sideHtml}</div><p>${esc(item.description)}${item.suggestion ? `\n建议:${esc(item.suggestion)}` : ""}</p>${evidenceHtml ? `<ul class="character-duplicate-evidence">${evidenceHtml}</ul>` : ""}${actions}${item.resolutionNote ? `<p class="review-resolution-note">处理结果:${esc(item.resolutionNote)}</p>` : ""}</article>`;
|
|
2817
2928
|
};
|
|
2818
2929
|
$("#module-content").innerHTML = reviews.length ? `<div class="card-grid">${reviews.map((item) => item.itemType === "character-duplicate" ? duplicateCard(item) : `
|
|
2819
2930
|
<article class="record-card"><small>${esc(item.itemType)} · ${esc(item.severity)} · ${esc(item.status)}</small><h3>${esc(item.title)}</h3>
|
|
2820
2931
|
<p>${esc(item.description)}${item.suggestion ? `\n建议:${esc(item.suggestion)}` : ""}</p>
|
|
2821
|
-
${item.status === "pending" ? `<div class="card-actions"><button data-review-status="fixed" data-review-id="${esc(item.id)}">标为已修复</button><button data-review-status="ignored" data-review-id="${esc(item.id)}">忽略</button></div>` : ""}</article>`).join("")}</div>`
|
|
2932
|
+
${item.status === "pending" && canResolveReview ? `<div class="card-actions"><button data-review-status="fixed" data-review-id="${esc(item.id)}">标为已修复</button><button data-review-status="ignored" data-review-id="${esc(item.id)}">忽略</button></div>` : ""}</article>`).join("")}</div>`
|
|
2822
2933
|
: emptyModule("没有待审核事项", "候选设定、冲突与低置信度结论会集中显示在这里。");
|
|
2823
2934
|
$("#module-content").querySelectorAll("[data-review-id]").forEach((button) => button.addEventListener("click", async () => {
|
|
2824
2935
|
await api(`/api/reviews/${button.dataset.reviewId}`, { method: "PATCH", body: { status: button.dataset.reviewStatus } });
|
|
@@ -2861,12 +2972,15 @@ async function renderReviews() {
|
|
|
2861
2972
|
async function renderTasks() {
|
|
2862
2973
|
const [tasks, settings] = await Promise.all([
|
|
2863
2974
|
apiPage(`/api/works/${state.work.id}/tasks`).then((result) => result.items),
|
|
2864
|
-
|
|
2975
|
+
canReadModule("ai-settings")
|
|
2976
|
+
? api(`/api/works/${state.work.id}/ai-settings`)
|
|
2977
|
+
: Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunBatchLimit: 20 })
|
|
2865
2978
|
]);
|
|
2979
|
+
const canConfigureAutoRun = canEditModule("tasks") && canEditModule("ai-settings");
|
|
2866
2980
|
const pendingCount = tasks.filter((item) => item.status === "pending").length;
|
|
2867
2981
|
const runningCount = tasks.filter((item) => item.status === "running").length;
|
|
2868
2982
|
$("#module-content").innerHTML = `
|
|
2869
|
-
<section class="task-auto-run-panel" aria-labelledby="task-auto-run-title">
|
|
2983
|
+
<section class="task-auto-run-panel ${canConfigureAutoRun ? "" : "hidden"}" aria-labelledby="task-auto-run-title">
|
|
2870
2984
|
<div class="task-auto-run-copy">
|
|
2871
2985
|
<strong id="task-auto-run-title">自动执行待分析任务</strong>
|
|
2872
2986
|
<small>只执行已经进入“待执行”队列的任务,不会自动创建人物关系、世界观或其他分析。</small>
|
|
@@ -3084,6 +3198,10 @@ async function renderBookAiSettings() {
|
|
|
3084
3198
|
"afterend",
|
|
3085
3199
|
`<label><input name="agent-tool" type="checkbox" value="read_character_sections" ${agentTools.has("read_character_sections") ? "checked" : ""}><span><strong>读取人物 Markdown 章节</strong><small>根据知识查询返回的章节 ID 精读人物背景、能力与经历原文。</small></span></label>`
|
|
3086
3200
|
);
|
|
3201
|
+
if (!canEditModule("ai-settings")) {
|
|
3202
|
+
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
3203
|
+
host.querySelectorAll(".primary-button").forEach((button) => button.classList.add("permission-hidden"));
|
|
3204
|
+
}
|
|
3087
3205
|
$("#save-work-system-prompt").addEventListener("click", async () => {
|
|
3088
3206
|
const button = $("#save-work-system-prompt");
|
|
3089
3207
|
button.disabled = true;
|
|
@@ -3152,8 +3270,9 @@ async function renderBookAiSettings() {
|
|
|
3152
3270
|
async function loadModels() {
|
|
3153
3271
|
const workId = state.work?.id;
|
|
3154
3272
|
if (!workId) return;
|
|
3273
|
+
const generation = workScopedUiGeneration;
|
|
3155
3274
|
const models = await api(`/api/works/${workId}/models`);
|
|
3156
|
-
if (state.work?.id !== workId) return;
|
|
3275
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
3157
3276
|
state.models = models;
|
|
3158
3277
|
loadedAiModelsWorkId = workId;
|
|
3159
3278
|
const select = $("#ai-model");
|
|
@@ -3294,11 +3413,12 @@ async function refreshAiContextUsage() {
|
|
|
3294
3413
|
async function loadAiReferences() {
|
|
3295
3414
|
const workId = state.work?.id;
|
|
3296
3415
|
if (!workId) return;
|
|
3416
|
+
const generation = workScopedUiGeneration;
|
|
3297
3417
|
const [characters, settings] = await Promise.all([
|
|
3298
|
-
apiAllPages(`/api/works/${workId}/characters`),
|
|
3299
|
-
apiAllPages(`/api/works/${workId}/settings`)
|
|
3418
|
+
canReadModule("characters") ? apiAllPages(`/api/works/${workId}/characters`) : Promise.resolve([]),
|
|
3419
|
+
canReadModule("settings") ? apiAllPages(`/api/works/${workId}/settings`) : Promise.resolve([])
|
|
3300
3420
|
]);
|
|
3301
|
-
if (state.work?.id !== workId) return;
|
|
3421
|
+
if (state.work?.id !== workId || generation !== workScopedUiGeneration) return;
|
|
3302
3422
|
state.characters = characters;
|
|
3303
3423
|
state.settings = settings;
|
|
3304
3424
|
loadedAiReferencesWorkId = workId;
|
|
@@ -3533,8 +3653,8 @@ function openWorkSettingsDialog(work) {
|
|
|
3533
3653
|
if (!work) return;
|
|
3534
3654
|
const canManageAccess = ["admin", "owner"].includes(String(work.accessRole));
|
|
3535
3655
|
const accessField = `<section class="work-access-field" aria-labelledby="work-access-title">
|
|
3536
|
-
<div><strong id="work-access-title"
|
|
3537
|
-
${canManageAccess ? '<button id="work-access-manage" class="ghost-button" type="button"
|
|
3656
|
+
<div><strong id="work-access-title">成员权限</strong><small>选择成员后,可为每个作品模块单独设置无权限、只读或可编辑。</small><div class="work-access-options" aria-label="成员权限配置方式"><span>按成员配置</span><span>按模块授权</span><span>读写分离</span></div></div>
|
|
3657
|
+
${canManageAccess ? '<button id="work-access-manage" class="ghost-button" type="button">配置成员权限</button>' : '<small>仅作品创建者或系统管理员可以调整访问权限。</small>'}
|
|
3538
3658
|
</section>`;
|
|
3539
3659
|
openDialog("作品信息",
|
|
3540
3660
|
workCoverFieldHtml(work) + field("title", "作品名称", "text", work.title) + field("author", "作者", "text", work.author) + field("description", "简介", "textarea", work.description) + accessField,
|
|
@@ -3610,13 +3730,13 @@ function openSettingEditor(item = null) {
|
|
|
3610
3730
|
$("#setting-change-note").value = "";
|
|
3611
3731
|
$("#setting-change-note-field").classList.toggle("hidden", !item);
|
|
3612
3732
|
$("#setting-editor-submit").textContent = item ? "保存新版本" : "创建设定";
|
|
3613
|
-
const viewOnly = !
|
|
3733
|
+
const viewOnly = !canEditModule("settings");
|
|
3614
3734
|
$("#setting-editor-form").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = viewOnly; });
|
|
3615
3735
|
$("#setting-editor-form").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = viewOnly; });
|
|
3616
3736
|
$("#setting-editor-submit").classList.toggle("hidden", viewOnly);
|
|
3617
3737
|
$("#setting-editor-form").onsubmit = async (event) => {
|
|
3618
3738
|
event.preventDefault();
|
|
3619
|
-
if (!
|
|
3739
|
+
if (!canEditModule("settings")) return;
|
|
3620
3740
|
const form = new FormData(event.currentTarget);
|
|
3621
3741
|
const submit = $("#setting-editor-submit");
|
|
3622
3742
|
submit.disabled = true;
|
|
@@ -3698,11 +3818,11 @@ function renderCharacterEditorRelationships() {
|
|
|
3698
3818
|
const relationLabel = [category, relationship.subtype].filter(Boolean).join(" · ") || "未细分";
|
|
3699
3819
|
const keywords = Array.isArray(relationship.keywords) ? relationship.keywords : [];
|
|
3700
3820
|
return `<article class="character-relationship-row">
|
|
3701
|
-
<div class="character-relationship-heading"><div><strong>${esc(nameOf(otherCharacterId))}</strong><span>${direction} ${esc(relationLabel)}</span></div>${
|
|
3821
|
+
<div class="character-relationship-heading"><div><strong>${esc(nameOf(otherCharacterId))}</strong><span>${direction} ${esc(relationLabel)}</span></div>${canEditModule("relationships") ? `<button type="button" data-character-relationship-edit="${esc(relationship.id)}">编辑关系</button>` : ""}</div>
|
|
3702
3822
|
<div class="character-relationship-keywords"><small>关系关键词</small><div>${keywords.map((keyword) => `<span class="pill relationship-keyword">${esc(keyword)}</span>`).join("") || '<span class="character-relationship-empty-keywords">未填写关键词</span>'}</div></div>
|
|
3703
3823
|
</article>`;
|
|
3704
3824
|
}).join("");
|
|
3705
|
-
host.innerHTML = `<div class="character-relationship-toolbar"><p>与 ${esc(characterEditorItem.name)} 有关的其他人物及关系关键词。</p>${
|
|
3825
|
+
host.innerHTML = `<div class="character-relationship-toolbar"><p>与 ${esc(characterEditorItem.name)} 有关的其他人物及关系关键词。</p>${canEditModule("relationships") ? '<button type="button" class="ghost-button" data-character-relationship-create>新建关系</button>' : ""}</div>${rows || '<p class="character-relationship-status">暂未记录与其他人物的关系。</p>'}`;
|
|
3706
3826
|
host.querySelectorAll("[data-character-relationship-edit]").forEach((button) => button.addEventListener("click", () => {
|
|
3707
3827
|
const relationship = characterEditorRelationships.find((item) => item.id === button.dataset.characterRelationshipEdit);
|
|
3708
3828
|
if (relationship) void openRelationshipDialog(relationship, { characterId });
|
|
@@ -3932,9 +4052,9 @@ function renderCharacterMarkdownSections() {
|
|
|
3932
4052
|
host.innerHTML = '<div class="character-editor-empty-field"><b>Markdown 档案章节</b><span>创建人物档案后即可添加背景故事、能力、经历和研究记录。</span></div>';
|
|
3933
4053
|
return;
|
|
3934
4054
|
}
|
|
3935
|
-
const toolbar = `<div class="character-markdown-list-toolbar"><div><b>Markdown 档案章节</b><span>长篇内容独立保存、渲染、检索和版本管理。</span></div>${
|
|
4055
|
+
const toolbar = `<div class="character-markdown-list-toolbar"><div><b>Markdown 档案章节</b><span>长篇内容独立保存、渲染、检索和版本管理。</span></div>${canEditModule("characters") ? '<button type="button" class="primary-button" data-character-section-create>新建章节</button>' : ""}</div>`;
|
|
3936
4056
|
const sections = characterEditorSections.map((section) => `<article class="character-markdown-section">
|
|
3937
|
-
<header><div><span>${esc(characterSectionTypeLabels[section.sectionType] ?? section.sectionType)}</span><h4>${esc(section.title)}</h4>${section.summary ? `<p>${esc(section.summary)}</p>` : ""}</div><div>${
|
|
4057
|
+
<header><div><span>${esc(characterSectionTypeLabels[section.sectionType] ?? section.sectionType)}</span><h4>${esc(section.title)}</h4>${section.summary ? `<p>${esc(section.summary)}</p>` : ""}</div><div>${canEditModule("characters") ? `<button type="button" data-character-section-edit="${esc(section.id)}">编辑</button>` : ""}<button type="button" data-character-section-versions="${esc(section.id)}">版本</button>${canEditModule("characters") ? `<button type="button" data-character-section-delete="${esc(section.id)}">删除</button>` : ""}</div></header>
|
|
3938
4058
|
<div class="character-markdown-document message-body">${renderMarkdown(section.contentMarkdown) || '<p class="character-markdown-empty">本章节暂无正文。</p>'}</div>
|
|
3939
4059
|
<div data-character-section-versions-host="${esc(section.id)}"></div>
|
|
3940
4060
|
</article>`).join("");
|
|
@@ -3992,14 +4112,20 @@ function renderCharacterEditorFields(item) {
|
|
|
3992
4112
|
characterEditorSection("basic", "基础资料", "用于检索、去重和建立人物在作品中的基本归属。",
|
|
3993
4113
|
field("name", "标准名", "text", item?.name) +
|
|
3994
4114
|
field("aliases", "别名", "item-list", item?.aliases ?? []) +
|
|
3995
|
-
(
|
|
4115
|
+
(!canReadModule("races")
|
|
4116
|
+
? '<div class="character-editor-empty-field"><b>种族</b><span>当前账户没有种族模块读取权限,原有绑定不会被修改。</span></div>'
|
|
4117
|
+
: state.races.length
|
|
3996
4118
|
? field("raceId", "种族", "select", item?.raceId ?? "", raceOptions)
|
|
3997
4119
|
: '<div class="character-editor-empty-field"><b>种族</b><span>尚未创建种族,请先在“种族”模块建立档案。</span></div>') +
|
|
3998
|
-
(
|
|
4120
|
+
(!canReadModule("organizations")
|
|
4121
|
+
? '<div class="character-editor-empty-field"><b>所属组织</b><span>当前账户没有组织模块读取权限,原有绑定不会被修改。</span></div>'
|
|
4122
|
+
: organizationOptions.length
|
|
3999
4123
|
? field("organizationIds", "所属组织(可多选)", "chips", item?.organizationIds ?? [], organizationOptions)
|
|
4000
4124
|
: '<div class="character-editor-empty-field"><b>所属组织</b><span>尚未创建组织,可稍后在“组织”模块中补充。</span></div>') +
|
|
4001
4125
|
field("visibility", "可见范围", "select", item?.visibility ?? "author", [["author", "仅作者"], ["collaborators", "协作者"], ["public", "公开"]]) +
|
|
4002
|
-
|
|
4126
|
+
(canReadModule("editor")
|
|
4127
|
+
? field("firstChapterId", "首次登场章节", "select", item?.firstChapterId ?? "", chapterOptions)
|
|
4128
|
+
: '<div class="character-editor-empty-field"><b>首次登场章节</b><span>当前账户没有正文读取权限,原有绑定不会被修改。</span></div>')),
|
|
4003
4129
|
characterEditorSection("profile", "人物档案", "记录人物定位、行为动力和便于创作时快速理解的简介。",
|
|
4004
4130
|
field("identity", "身份与定位", "text", item?.attributes?.identity) +
|
|
4005
4131
|
field("motivation", "核心动机", "textarea", item?.profile?.motivation) +
|
|
@@ -4035,11 +4161,9 @@ function collectCharacterBody(form) {
|
|
|
4035
4161
|
const item = characterEditorItem;
|
|
4036
4162
|
const profile = { ...(item?.profile ?? {}) };
|
|
4037
4163
|
delete profile.sections;
|
|
4038
|
-
|
|
4164
|
+
const body = {
|
|
4039
4165
|
name: String(form.get("name") ?? "").trim(),
|
|
4040
4166
|
aliases: form.getAll("aliases").map((value) => String(value).trim()).filter(Boolean),
|
|
4041
|
-
raceId: form.get("raceId") || null,
|
|
4042
|
-
organizationIds: form.getAll("organizationIds").map(String),
|
|
4043
4167
|
attributes: {
|
|
4044
4168
|
...(item?.attributes ?? {}),
|
|
4045
4169
|
identity: String(form.get("identity") ?? "").trim(),
|
|
@@ -4053,9 +4177,12 @@ function collectCharacterBody(form) {
|
|
|
4053
4177
|
currentState: buildCharacterState(form.getAll("stateKey"), form.getAll("stateValue"), item?.currentState ?? {}),
|
|
4054
4178
|
lockedFields: form.getAll("lockedFields").map((value) => String(value).trim()).filter(Boolean),
|
|
4055
4179
|
visibility: String(form.get("visibility") ?? "author"),
|
|
4056
|
-
firstChapterId: form.get("firstChapterId") || null,
|
|
4057
4180
|
changeNote: String(form.get("changeNote") ?? "").trim()
|
|
4058
4181
|
};
|
|
4182
|
+
if (canReadModule("races")) body.raceId = form.get("raceId") || null;
|
|
4183
|
+
if (canReadModule("organizations")) body.organizationIds = form.getAll("organizationIds").map(String);
|
|
4184
|
+
if (canReadModule("editor")) body.firstChapterId = form.get("firstChapterId") || null;
|
|
4185
|
+
return body;
|
|
4059
4186
|
}
|
|
4060
4187
|
|
|
4061
4188
|
function renderCharacterHistory() {
|
|
@@ -4127,8 +4254,8 @@ async function showCharacterHistory() {
|
|
|
4127
4254
|
|
|
4128
4255
|
async function openCharacterEditor(item = null) {
|
|
4129
4256
|
[state.races, state.organizations] = await Promise.all([
|
|
4130
|
-
apiAllPages(`/api/works/${state.work.id}/races`),
|
|
4131
|
-
apiAllPages(`/api/works/${state.work.id}/organizations`)
|
|
4257
|
+
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
4258
|
+
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
4132
4259
|
]);
|
|
4133
4260
|
characterEditorItem = item ?? null;
|
|
4134
4261
|
characterEditorVersions = [];
|
|
@@ -4144,7 +4271,7 @@ async function openCharacterEditor(item = null) {
|
|
|
4144
4271
|
$("#character-history-button").title = item ? "查看、比较和回滚历史版本" : "创建人物档案后即可查看版本历史";
|
|
4145
4272
|
setCharacterHistoryVisible(false);
|
|
4146
4273
|
renderCharacterEditorFields(item);
|
|
4147
|
-
const viewOnly = !
|
|
4274
|
+
const viewOnly = !canEditModule("characters");
|
|
4148
4275
|
if (viewOnly) {
|
|
4149
4276
|
$("#character-editor-eyebrow").textContent = "人物档案";
|
|
4150
4277
|
$("#character-editor-fields").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = true; });
|
|
@@ -4156,12 +4283,12 @@ async function openCharacterEditor(item = null) {
|
|
|
4156
4283
|
button.onclick = () => activateCharacterEditorTab(button.dataset.characterEditorTab);
|
|
4157
4284
|
});
|
|
4158
4285
|
const relationshipTab = document.querySelector("[data-character-editor-tab='relationships']");
|
|
4159
|
-
relationshipTab.disabled = !item;
|
|
4160
|
-
relationshipTab.title = item ? "查看和编辑人物关系" : "创建人物档案后即可维护人物关系";
|
|
4286
|
+
relationshipTab.disabled = !item || !canReadModule("relationships");
|
|
4287
|
+
relationshipTab.title = !canReadModule("relationships") ? "当前账户没有关系模块读取权限" : item ? "查看和编辑人物关系" : "创建人物档案后即可维护人物关系";
|
|
4161
4288
|
const form = $("#character-editor-form");
|
|
4162
4289
|
form.onsubmit = async (event) => {
|
|
4163
4290
|
event.preventDefault();
|
|
4164
|
-
if (!
|
|
4291
|
+
if (!canEditModule("characters")) return;
|
|
4165
4292
|
const submit = $("#character-editor-submit");
|
|
4166
4293
|
submit.disabled = true;
|
|
4167
4294
|
try {
|
|
@@ -4183,13 +4310,13 @@ async function openCharacterEditor(item = null) {
|
|
|
4183
4310
|
};
|
|
4184
4311
|
showEntityEditorPage("character");
|
|
4185
4312
|
if (item) {
|
|
4186
|
-
void loadCharacterEditorRelationships(item.id);
|
|
4313
|
+
if (canReadModule("relationships")) void loadCharacterEditorRelationships(item.id);
|
|
4187
4314
|
void loadCharacterMarkdownSections(item.id);
|
|
4188
4315
|
}
|
|
4189
4316
|
}
|
|
4190
4317
|
|
|
4191
4318
|
async function openRaceDialog(item) {
|
|
4192
|
-
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
4319
|
+
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
4193
4320
|
const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
|
|
4194
4321
|
const parentOptions = [["", "无(根种族)"], ...eligibleRaceParents(state.races, item?.id)
|
|
4195
4322
|
.sort((left, right) => racePathLabel(left).localeCompare(racePathLabel(right), "zh-CN"))
|
|
@@ -4202,7 +4329,8 @@ async function openRaceDialog(item) {
|
|
|
4202
4329
|
(memberOptions.length ? field("memberIds", "属于该种族的角色(可多选)", "chips", item?.memberIds ?? [], memberOptions) : ""),
|
|
4203
4330
|
async (form) => {
|
|
4204
4331
|
const settings = form.getAll("settings").map((value) => String(value).trim()).filter(Boolean);
|
|
4205
|
-
const body = { name: form.get("name"), parentRaceId: form.get("parentRaceId") || null, description: form.get("description"), settings
|
|
4332
|
+
const body = { name: form.get("name"), parentRaceId: form.get("parentRaceId") || null, description: form.get("description"), settings };
|
|
4333
|
+
if (canReadModule("characters")) body.memberIds = form.getAll("memberIds").map(String);
|
|
4206
4334
|
await api(item ? `/api/races/${item.id}` : `/api/works/${state.work.id}/races`, { method: item ? "PATCH" : "POST", body });
|
|
4207
4335
|
await renderRaces();
|
|
4208
4336
|
await loadAiReferences();
|
|
@@ -4210,7 +4338,7 @@ async function openRaceDialog(item) {
|
|
|
4210
4338
|
}
|
|
4211
4339
|
|
|
4212
4340
|
async function openOrganizationDialog(item) {
|
|
4213
|
-
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
4341
|
+
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
4214
4342
|
const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
|
|
4215
4343
|
openDialog(item ? "编辑组织" : "新建组织",
|
|
4216
4344
|
field("name", "组织名称", "text", item?.name) +
|
|
@@ -4219,7 +4347,8 @@ async function openOrganizationDialog(item) {
|
|
|
4219
4347
|
(memberOptions.length ? field("memberIds", "组织成员(可多选)", "chips", item?.memberIds ?? [], memberOptions) : ""),
|
|
4220
4348
|
async (form) => {
|
|
4221
4349
|
const settings = form.getAll("settings").map((value) => String(value).trim()).filter(Boolean);
|
|
4222
|
-
const body = { name: form.get("name"), description: form.get("description"), settings
|
|
4350
|
+
const body = { name: form.get("name"), description: form.get("description"), settings };
|
|
4351
|
+
if (canReadModule("characters")) body.memberIds = form.getAll("memberIds").map(String);
|
|
4223
4352
|
await api(item ? `/api/organizations/${item.id}` : `/api/works/${state.work.id}/organizations`, { method: item ? "PATCH" : "POST", body });
|
|
4224
4353
|
await renderOrganizations();
|
|
4225
4354
|
await loadAiReferences();
|
|
@@ -4316,6 +4445,7 @@ function openTimelineSplitDialog(item) {
|
|
|
4316
4445
|
}
|
|
4317
4446
|
|
|
4318
4447
|
async function openRelationshipDialog(item, options = {}) {
|
|
4448
|
+
if (!canReadModule("characters")) return toast("配置人物关系前需要角色模块读取权限", "error");
|
|
4319
4449
|
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
4320
4450
|
if (state.characters.length < 2) return toast("至少需要两个角色才能创建关系", "error");
|
|
4321
4451
|
const characterOptions = state.characters.map((item) => [item.id, item.name]);
|
|
@@ -4642,6 +4772,120 @@ async function showVersions() {
|
|
|
4642
4772
|
$("#versions-dialog").showModal();
|
|
4643
4773
|
}
|
|
4644
4774
|
|
|
4775
|
+
function importHistoryRecordLabel(version) {
|
|
4776
|
+
const restorePrefix = "before-restore:";
|
|
4777
|
+
if (version.fileType === "snapshot" && version.fileName.startsWith(restorePrefix)) {
|
|
4778
|
+
return {
|
|
4779
|
+
title: `恢复操作前备份:${version.fileName.slice(restorePrefix.length)}`,
|
|
4780
|
+
kind: "自动备份",
|
|
4781
|
+
action: "恢复此操作前备份"
|
|
4782
|
+
};
|
|
4783
|
+
}
|
|
4784
|
+
return { title: version.fileName, kind: String(version.fileType).toUpperCase(), action: "恢复到此次导入前" };
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
function renderImportHistory(versions, nextPage = null) {
|
|
4788
|
+
const host = $("#import-history-list");
|
|
4789
|
+
if (!versions.length) {
|
|
4790
|
+
host.innerHTML = '<p class="entity-history-empty">还没有正文导入记录。首次导入后会自动保存导入前快照。</p>';
|
|
4791
|
+
return;
|
|
4792
|
+
}
|
|
4793
|
+
host.innerHTML = versions.map((version) => {
|
|
4794
|
+
const label = importHistoryRecordLabel(version);
|
|
4795
|
+
return `<article class="entity-version-card import-history-card" data-file-version="${esc(version.id)}">
|
|
4796
|
+
<header><strong title="${esc(label.title)}">${esc(label.title)}</strong><span class="import-history-kind">${esc(label.kind)}</span></header>
|
|
4797
|
+
<time>${esc(formatDateTime(version.createdAt))} · ${esc(version.actor || "历史数据")}</time>
|
|
4798
|
+
<p>${version.fileType === "snapshot" ? "恢复操作执行前自动保存的完整正文。" : "保存的是这次文件导入开始前的完整正文。"}</p>
|
|
4799
|
+
<small>自动备份仅包含正文,不能撤销章节关联信息的变化。</small>
|
|
4800
|
+
<button type="button" data-file-version-restore="${esc(version.id)}" data-default-label="${esc(label.action)}">${esc(label.action)}</button>
|
|
4801
|
+
</article>`;
|
|
4802
|
+
}).join("") + (nextPage ? '<button class="import-history-load-more" type="button" data-import-history-load-more>加载更多记录</button>' : "");
|
|
4803
|
+
host.querySelectorAll("[data-file-version-restore]").forEach((button) => button.addEventListener("click", async () => {
|
|
4804
|
+
if (!state.work || !canReplaceProse()) return;
|
|
4805
|
+
const defaultLabel = button.dataset.defaultLabel;
|
|
4806
|
+
if (button.dataset.confirmed !== "true") {
|
|
4807
|
+
host.querySelectorAll("[data-file-version-restore]").forEach((other) => {
|
|
4808
|
+
other.dataset.confirmed = "false";
|
|
4809
|
+
other.classList.remove("is-confirming");
|
|
4810
|
+
other.textContent = other.dataset.defaultLabel;
|
|
4811
|
+
});
|
|
4812
|
+
button.dataset.confirmed = "true";
|
|
4813
|
+
button.classList.add("is-confirming");
|
|
4814
|
+
button.textContent = "再次点击确认恢复";
|
|
4815
|
+
window.setTimeout(() => {
|
|
4816
|
+
if (!button.isConnected || button.dataset.confirmed !== "true") return;
|
|
4817
|
+
button.dataset.confirmed = "false";
|
|
4818
|
+
button.classList.remove("is-confirming");
|
|
4819
|
+
button.textContent = defaultLabel;
|
|
4820
|
+
}, 5000);
|
|
4821
|
+
return;
|
|
4822
|
+
}
|
|
4823
|
+
if (!confirmDiscardChanges("当前章节有未保存修改,恢复正文会丢弃这些本地修改。是否继续?")) return;
|
|
4824
|
+
button.disabled = true;
|
|
4825
|
+
cancelChapterAutoSave();
|
|
4826
|
+
const workId = state.work.id;
|
|
4827
|
+
try {
|
|
4828
|
+
await api(`/api/works/${encodeURIComponent(workId)}/file-versions/${encodeURIComponent(button.dataset.fileVersionRestore)}/restore`, {
|
|
4829
|
+
method: "POST",
|
|
4830
|
+
body: { expectedVersionNo: state.work.versionNo }
|
|
4831
|
+
});
|
|
4832
|
+
state.dirty = false;
|
|
4833
|
+
resetWorkScopedUiCaches();
|
|
4834
|
+
$("#import-history-dialog").close();
|
|
4835
|
+
await loadWorks(workId);
|
|
4836
|
+
toast("正文已恢复;恢复前正文已自动备份");
|
|
4837
|
+
} catch (error) {
|
|
4838
|
+
button.disabled = false;
|
|
4839
|
+
button.dataset.confirmed = "false";
|
|
4840
|
+
button.classList.remove("is-confirming");
|
|
4841
|
+
button.textContent = defaultLabel;
|
|
4842
|
+
if (state.dirty) scheduleChapterAutoSave();
|
|
4843
|
+
toast(error.message, "error");
|
|
4844
|
+
}
|
|
4845
|
+
}));
|
|
4846
|
+
host.querySelector("[data-import-history-load-more]")?.addEventListener("click", async (event) => {
|
|
4847
|
+
const button = event.currentTarget;
|
|
4848
|
+
button.disabled = true;
|
|
4849
|
+
button.textContent = "正在加载…";
|
|
4850
|
+
try {
|
|
4851
|
+
await loadImportHistoryPage(nextPage);
|
|
4852
|
+
} catch (error) {
|
|
4853
|
+
button.disabled = false;
|
|
4854
|
+
button.textContent = "加载更多记录";
|
|
4855
|
+
toast(error.message, "error");
|
|
4856
|
+
}
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
async function loadImportHistoryPage(page) {
|
|
4861
|
+
const workId = state.work?.id;
|
|
4862
|
+
if (!workId || !page) return;
|
|
4863
|
+
const requestId = ++importHistoryRequestId;
|
|
4864
|
+
const result = await apiPage(`/api/works/${encodeURIComponent(workId)}/file-versions`, page, 25);
|
|
4865
|
+
if (requestId !== importHistoryRequestId || state.work?.id !== workId || !$("#import-history-dialog").open) return;
|
|
4866
|
+
importHistoryRecords = page === 1 ? result.items : [...importHistoryRecords, ...result.items];
|
|
4867
|
+
importHistoryNextPage = result.nextPage;
|
|
4868
|
+
renderImportHistory(importHistoryRecords, importHistoryNextPage);
|
|
4869
|
+
}
|
|
4870
|
+
|
|
4871
|
+
async function openImportHistory() {
|
|
4872
|
+
if (!state.work || !canReplaceProse()) {
|
|
4873
|
+
toast("恢复整本正文需要所有受影响模块的编辑权限", "error");
|
|
4874
|
+
return;
|
|
4875
|
+
}
|
|
4876
|
+
importHistoryRecords = [];
|
|
4877
|
+
importHistoryNextPage = null;
|
|
4878
|
+
importHistoryRequestId += 1;
|
|
4879
|
+
$("#import-history-list").innerHTML = '<p class="entity-history-empty">正在读取导入历史…</p>';
|
|
4880
|
+
$("#import-history-dialog").showModal();
|
|
4881
|
+
try {
|
|
4882
|
+
await loadImportHistoryPage(1);
|
|
4883
|
+
} catch (error) {
|
|
4884
|
+
$("#import-history-dialog").close();
|
|
4885
|
+
toast(error.message, "error");
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4645
4889
|
async function showChapterInsight() {
|
|
4646
4890
|
if (!state.chapter) return;
|
|
4647
4891
|
const panel = $("#chapter-insight");
|
|
@@ -4918,18 +5162,34 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
4918
5162
|
}
|
|
4919
5163
|
});
|
|
4920
5164
|
$("#members-dialog-close").addEventListener("click", () => $("#members-dialog").close());
|
|
4921
|
-
$("#members-dialog").addEventListener("close", () => {
|
|
4922
|
-
|
|
5165
|
+
$("#members-dialog").addEventListener("close", () => {
|
|
5166
|
+
memberDialogWork = null;
|
|
5167
|
+
memberDialogMembers = [];
|
|
5168
|
+
memberDialogDirectory = [];
|
|
5169
|
+
});
|
|
5170
|
+
$("#member-user-select").addEventListener("change", () => selectMemberForConfiguration($("#member-user-select").value));
|
|
5171
|
+
$("#member-permission-form").querySelectorAll("[data-permission-preset]").forEach((button) => button.addEventListener("click", () => {
|
|
5172
|
+
$("#member-permission-grid").querySelectorAll("[data-member-permission]").forEach((select) => {
|
|
5173
|
+
select.value = button.dataset.permissionPreset;
|
|
5174
|
+
});
|
|
5175
|
+
}));
|
|
5176
|
+
$("#member-permission-form").addEventListener("submit", async (event) => {
|
|
4923
5177
|
event.preventDefault();
|
|
4924
5178
|
const userId = $("#member-user-select").value;
|
|
4925
5179
|
const work = memberDialogWork ?? state.work;
|
|
4926
5180
|
if (!work || !userId) return;
|
|
4927
5181
|
try {
|
|
4928
|
-
const
|
|
4929
|
-
const
|
|
5182
|
+
const existing = memberDialogMembers.some((member) => member.userId === userId && member.role !== "owner");
|
|
5183
|
+
const permissions = selectedMemberPermissions();
|
|
5184
|
+
const members = await api(existing
|
|
5185
|
+
? `/api/works/${encodeURIComponent(work.id)}/members/${encodeURIComponent(userId)}`
|
|
5186
|
+
: `/api/works/${encodeURIComponent(work.id)}/members`, {
|
|
5187
|
+
method: existing ? "PATCH" : "POST",
|
|
5188
|
+
body: existing ? { permissions } : { userId, permissions }
|
|
5189
|
+
});
|
|
4930
5190
|
renderMembers(members);
|
|
4931
|
-
|
|
4932
|
-
toast(
|
|
5191
|
+
renderMemberSelector(userId);
|
|
5192
|
+
toast(existing ? "成员模块权限已更新" : "成员已添加并保存模块权限");
|
|
4933
5193
|
} catch (error) { toast(error.message, "error"); }
|
|
4934
5194
|
});
|
|
4935
5195
|
$("#platform-new-provider").addEventListener("click", () => openProviderDialog());
|
|
@@ -4942,6 +5202,8 @@ $("#new-volume-button").addEventListener("click", () => openVolumeDialog());
|
|
|
4942
5202
|
$("#insight-button").addEventListener("click", () => showChapterInsight().catch((error) => toast(error.message, "error")));
|
|
4943
5203
|
$("#versions-button").addEventListener("click", showVersions);
|
|
4944
5204
|
$("#versions-close").addEventListener("click", () => $("#versions-dialog").close());
|
|
5205
|
+
$("#import-history-button").addEventListener("click", () => openImportHistory());
|
|
5206
|
+
$("#import-history-close").addEventListener("click", () => $("#import-history-dialog").close());
|
|
4945
5207
|
$("#entity-history-close").addEventListener("click", () => $("#entity-history-dialog").close());
|
|
4946
5208
|
$("#ai-tool-call-close").addEventListener("click", () => $("#ai-tool-call-dialog").close());
|
|
4947
5209
|
$("#setting-editor-back").addEventListener("click", () => { void closeEntityEditor(); });
|