@musnows/scriverse 0.3.9 → 0.3.10
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 +163 -30
- package/dist/app.js.map +1 -1
- package/dist/public/app.js +213 -98
- package/dist/public/index.html +17 -4
- package/dist/public/styles.css +31 -22
- package/dist/public/work-permissions.d.ts +14 -0
- package/dist/public/work-permissions.js +71 -0
- package/dist/store.js +19 -7
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +200 -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 +128 -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,11 @@ 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");
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
function canManageWork(work = state.work) {
|
|
@@ -93,23 +94,43 @@ function canManageWork(work = state.work) {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
function canEditModule(module, work = state.work) {
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
return canWriteUiModule(work, module);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function canReadModule(module, work = state.work) {
|
|
101
|
+
return canReadUiModule(work, module);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function canReadAggregateContent(work = state.work) {
|
|
105
|
+
return ["editor", "settings", "characters", "races", "organizations", "timeline", "relationships", "outlines"]
|
|
106
|
+
.every((module) => canReadModule(module, work));
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
function applyWorkAccessMode() {
|
|
101
110
|
const viewOnly = Boolean(state.work) && !canEditWork();
|
|
102
|
-
const settingsOnly = String(state.work?.accessRole) === "settings-editor";
|
|
103
111
|
const proseReadOnly = Boolean(state.work) && !canEditProse();
|
|
112
|
+
const proseHidden = Boolean(state.work) && !canReadModule("editor");
|
|
113
|
+
const aiHidden = Boolean(state.work) && !canReadModule("tasks");
|
|
114
|
+
const aiReadOnly = Boolean(state.work) && !canEditModule("tasks");
|
|
115
|
+
const moduleReadOnly = Boolean(state.work) && !canEditModule(state.module);
|
|
104
116
|
$("#app").classList.toggle("view-only-mode", viewOnly);
|
|
105
|
-
$("#app").classList.toggle("settings-only-mode", settingsOnly);
|
|
106
117
|
$("#app").classList.toggle("prose-read-only-mode", proseReadOnly);
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
$("#app").classList.toggle("prose-hidden-mode", proseHidden);
|
|
119
|
+
$("#app").classList.toggle("ai-hidden-mode", aiHidden);
|
|
120
|
+
document.body.classList.toggle("work-viewer-mode", moduleReadOnly);
|
|
121
|
+
for (const item of WORK_PERMISSION_MODULES) {
|
|
122
|
+
const button = $(`#module-nav [data-module="${item.uiModule}"]`);
|
|
123
|
+
if (button) button.classList.toggle("permission-hidden", !canReadModule(item.uiModule));
|
|
124
|
+
}
|
|
125
|
+
$("#module-nav [data-work-settings]").classList.toggle("permission-hidden", Boolean(state.work) && !canManageWork());
|
|
126
|
+
$(".ai-panel").classList.toggle("permission-hidden", aiHidden);
|
|
109
127
|
$("#chapter-title").readOnly = proseReadOnly;
|
|
110
128
|
$("#chapter-content").readOnly = proseReadOnly;
|
|
111
129
|
$("#chapter-title").setAttribute("aria-readonly", String(proseReadOnly));
|
|
112
130
|
$("#chapter-content").setAttribute("aria-readonly", String(proseReadOnly));
|
|
131
|
+
$("#ai-prompt").readOnly = aiReadOnly;
|
|
132
|
+
$("#ai-prompt").setAttribute("aria-readonly", String(aiReadOnly));
|
|
133
|
+
$("#ai-send").classList.toggle("permission-hidden", aiReadOnly);
|
|
113
134
|
if (proseReadOnly) {
|
|
114
135
|
cancelChapterAutoSave();
|
|
115
136
|
state.dirty = false;
|
|
@@ -162,6 +183,8 @@ const panelLayoutStorageKey = "ai-novel-panel-layout-v1";
|
|
|
162
183
|
const panelLayoutDefaults = Object.freeze({ leftWidth: 280, aiWidth: 360, leftCollapsed: false, aiCollapsed: false });
|
|
163
184
|
let restoringPageRoute = true;
|
|
164
185
|
let memberDialogWork = null;
|
|
186
|
+
let memberDialogMembers = [];
|
|
187
|
+
let memberDialogDirectory = [];
|
|
165
188
|
let onboardingStep = 0;
|
|
166
189
|
let onboardingAutoScheduled = false;
|
|
167
190
|
let onboardingPositionFrame = null;
|
|
@@ -510,7 +533,8 @@ function showEntityEditorPage(type) {
|
|
|
510
533
|
}
|
|
511
534
|
|
|
512
535
|
function markEntityEditorDirty() {
|
|
513
|
-
|
|
536
|
+
const module = entityEditorType === "setting" ? "settings" : "characters";
|
|
537
|
+
if (entityEditorType && canEditModule(module)) entityEditorDirty = true;
|
|
514
538
|
}
|
|
515
539
|
|
|
516
540
|
function confirmEntityEditorDiscard(message) {
|
|
@@ -1905,13 +1929,14 @@ function captureSettingsReturnContext() {
|
|
|
1905
1929
|
function renderSettingsHub() {
|
|
1906
1930
|
const hasWork = Boolean(state.work);
|
|
1907
1931
|
const canManageWork = hasWork && ["admin", "owner"].includes(String(state.work.accessRole));
|
|
1932
|
+
const canReadAggregate = hasWork && canReadAggregateContent();
|
|
1908
1933
|
const isAdmin = state.user?.role === "admin";
|
|
1909
1934
|
$("#platform-ai-button").classList.toggle("hidden", !isAdmin);
|
|
1910
1935
|
$("#user-management-button").classList.toggle("hidden", !isAdmin);
|
|
1911
1936
|
$("#platform-ui-settings-button").classList.toggle("hidden", !isAdmin);
|
|
1912
1937
|
$("#collaboration-button").disabled = !canManageWork;
|
|
1913
|
-
$("#top-search-button").disabled = !
|
|
1914
|
-
$("#export-button").disabled = !
|
|
1938
|
+
$("#top-search-button").disabled = !canReadAggregate;
|
|
1939
|
+
$("#export-button").disabled = !canReadAggregate;
|
|
1915
1940
|
$("#settings-return").textContent = settingsReturnContext?.view === "shelf" || !hasWork ? "返回书架" : "返回当前作品";
|
|
1916
1941
|
$("#settings-work-note").textContent = hasWork
|
|
1917
1942
|
? `当前作品:《${state.work.title}》。导出将作用于这部作品。`
|
|
@@ -1973,61 +1998,99 @@ async function openPlatformUiSettingsDialog() {
|
|
|
1973
1998
|
}
|
|
1974
1999
|
}
|
|
1975
2000
|
|
|
2001
|
+
function renderMemberPermissionGrid(value) {
|
|
2002
|
+
const permissions = normalizeModulePermissions(value, "custom");
|
|
2003
|
+
$("#member-permission-grid").innerHTML = WORK_PERMISSION_MODULES.map((item) => `<label class="member-permission-row">
|
|
2004
|
+
<span>${esc(item.label)}</span>
|
|
2005
|
+
<select data-member-permission="${esc(item.id)}" aria-label="${esc(item.label)}权限">
|
|
2006
|
+
<option value="none" ${permissions[item.id] === "none" ? "selected" : ""}>无权限</option>
|
|
2007
|
+
<option value="read" ${permissions[item.id] === "read" ? "selected" : ""}>只读</option>
|
|
2008
|
+
<option value="write" ${permissions[item.id] === "write" ? "selected" : ""}>可编辑</option>
|
|
2009
|
+
</select>
|
|
2010
|
+
</label>`).join("");
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
function selectedMemberPermissions() {
|
|
2014
|
+
const permissions = emptyModulePermissions();
|
|
2015
|
+
$("#member-permission-grid").querySelectorAll("[data-member-permission]").forEach((select) => {
|
|
2016
|
+
permissions[select.dataset.memberPermission] = select.value;
|
|
2017
|
+
});
|
|
2018
|
+
return permissions;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
function selectMemberForConfiguration(userId) {
|
|
2022
|
+
const fieldset = $("#member-permission-fieldset");
|
|
2023
|
+
const member = memberDialogMembers.find((item) => item.userId === userId);
|
|
2024
|
+
fieldset.disabled = !userId;
|
|
2025
|
+
renderMemberPermissionGrid(member?.permissions ?? emptyModulePermissions());
|
|
2026
|
+
$("#member-permission-submit").textContent = member ? "更新模块权限" : "添加成员并保存";
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
function renderMemberSelector(selectedUserId = "") {
|
|
2030
|
+
const ownerIds = new Set(memberDialogMembers.filter((member) => member.role === "owner").map((member) => member.userId));
|
|
2031
|
+
const people = new Map(memberDialogDirectory.map((user) => [user.userId, user]));
|
|
2032
|
+
for (const member of memberDialogMembers) if (member.role !== "owner") people.set(member.userId, member);
|
|
2033
|
+
const options = [...people.values()].filter((user) => !ownerIds.has(user.userId));
|
|
2034
|
+
$("#member-user-select").innerHTML = options.length
|
|
2035
|
+
? `<option value="">选择用户</option>${options.map((user) => {
|
|
2036
|
+
const existing = memberDialogMembers.some((member) => member.userId === user.userId && member.role !== "owner");
|
|
2037
|
+
return `<option value="${esc(user.userId)}" ${selectedUserId === user.userId ? "selected" : ""}>${esc(user.displayName)} · @${esc(user.username)}${existing ? " · 已加入" : ""}</option>`;
|
|
2038
|
+
}).join("")}`
|
|
2039
|
+
: '<option value="">没有可配置的用户</option>';
|
|
2040
|
+
$("#member-user-select").disabled = !options.length;
|
|
2041
|
+
selectMemberForConfiguration(selectedUserId);
|
|
2042
|
+
}
|
|
2043
|
+
|
|
1976
2044
|
function renderMembers(members) {
|
|
2045
|
+
memberDialogMembers = members;
|
|
1977
2046
|
const work = memberDialogWork ?? state.work;
|
|
1978
2047
|
const canManage = ["admin", "owner"].includes(String(work?.accessRole));
|
|
1979
|
-
$("#members-list").innerHTML = members.map((member) =>
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
2048
|
+
$("#members-list").innerHTML = members.map((member) => {
|
|
2049
|
+
const descriptionId = `member-role-summary-${member.userId}`;
|
|
2050
|
+
return `<article class="access-row">
|
|
2051
|
+
<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>
|
|
2052
|
+
${member.role === "owner" || !canManage ? "<span></span>" : `<button type="button" data-configure-member="${esc(member.userId)}" aria-describedby="${esc(descriptionId)}">配置权限</button>`}
|
|
2053
|
+
${member.role === "owner" || !canManage ? "<span></span>" : `<button type="button" data-remove-member="${esc(member.userId)}">移除</button>`}
|
|
2054
|
+
</article>`;
|
|
2055
|
+
}).join("");
|
|
1984
2056
|
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
|
-
}
|
|
2057
|
+
$("#members-list").querySelectorAll("[data-configure-member]").forEach((button) => button.addEventListener("click", () => {
|
|
2058
|
+
$("#member-user-select").value = button.dataset.configureMember;
|
|
2059
|
+
selectMemberForConfiguration(button.dataset.configureMember);
|
|
2060
|
+
$("#member-user-select").focus();
|
|
1996
2061
|
}));
|
|
1997
2062
|
$("#members-list").querySelectorAll("[data-remove-member]").forEach((button) => button.addEventListener("click", async () => {
|
|
1998
2063
|
if (!work) return;
|
|
1999
2064
|
try {
|
|
2000
2065
|
const updated = await api(`/api/works/${encodeURIComponent(work.id)}/members/${encodeURIComponent(button.dataset.removeMember)}`, { method: "DELETE" });
|
|
2001
2066
|
renderMembers(updated);
|
|
2002
|
-
|
|
2067
|
+
renderMemberSelector();
|
|
2003
2068
|
toast("协作者已移除");
|
|
2004
2069
|
} catch (error) { toast(error.message, "error"); }
|
|
2005
2070
|
}));
|
|
2006
2071
|
}
|
|
2007
2072
|
|
|
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
2073
|
async function openMembersDialog(targetWork = state.work) {
|
|
2019
2074
|
if (!targetWork) return;
|
|
2020
2075
|
memberDialogWork = targetWork;
|
|
2021
2076
|
const canManage = ["admin", "owner"].includes(String(targetWork.accessRole));
|
|
2022
2077
|
$("#members-dialog-eyebrow").textContent = `作品权限 · 《${targetWork.title}》`;
|
|
2023
|
-
$("#members-dialog-title").textContent = "
|
|
2078
|
+
$("#members-dialog-title").textContent = "成员模块权限";
|
|
2024
2079
|
$("#members-list").innerHTML = '<p class="empty-state">正在读取成员……</p>';
|
|
2025
|
-
$("#member-
|
|
2080
|
+
$("#member-permission-form").classList.toggle("hidden", !canManage);
|
|
2081
|
+
memberDialogMembers = [];
|
|
2082
|
+
memberDialogDirectory = [];
|
|
2083
|
+
renderMemberPermissionGrid(emptyModulePermissions());
|
|
2084
|
+
$("#member-permission-fieldset").disabled = true;
|
|
2026
2085
|
$("#members-dialog").showModal();
|
|
2027
2086
|
try {
|
|
2028
|
-
const members = await
|
|
2087
|
+
const [members, directory] = await Promise.all([
|
|
2088
|
+
api(`/api/works/${encodeURIComponent(targetWork.id)}/members`),
|
|
2089
|
+
canManage ? api("/api/users/directory") : Promise.resolve([])
|
|
2090
|
+
]);
|
|
2091
|
+
memberDialogDirectory = directory;
|
|
2029
2092
|
renderMembers(members);
|
|
2030
|
-
if (canManage)
|
|
2093
|
+
if (canManage) renderMemberSelector();
|
|
2031
2094
|
} catch (error) { $("#members-dialog").close(); toast(error.message, "error"); }
|
|
2032
2095
|
}
|
|
2033
2096
|
|
|
@@ -2181,7 +2244,7 @@ function renderShelf() {
|
|
|
2181
2244
|
<span class="book-cover-fallback">${esc(Array.from(work.title)[0] ?? "书")}</span>
|
|
2182
2245
|
${work.coverUrl ? `<img src="${esc(work.coverUrl)}" alt="${esc(work.title)} 封面">` : ""}
|
|
2183
2246
|
</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" ? "
|
|
2247
|
+
<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
2248
|
</button>
|
|
2186
2249
|
${canManageWork(work) ? `<button class="book-card-settings" type="button" data-edit-work="${esc(work.id)}" aria-label="作品设置" title="作品设置">设置</button>` : ""}
|
|
2187
2250
|
</article>`).join("")}
|
|
@@ -2231,16 +2294,18 @@ async function selectWork(workId, preferredChapterId = null) {
|
|
|
2231
2294
|
settingsReturnContext = null;
|
|
2232
2295
|
state.work = nextWork;
|
|
2233
2296
|
state.chapter = null;
|
|
2297
|
+
if (!canReadModule(state.module)) state.module = firstReadableUiModule(state.work) ?? "editor";
|
|
2234
2298
|
applyWorkAccessMode();
|
|
2235
2299
|
updateDocumentTitle(state.work);
|
|
2236
2300
|
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${state.work.wordCount} 字`;
|
|
2237
|
-
$("#top-search-button").disabled =
|
|
2301
|
+
$("#top-search-button").disabled = !canReadAggregateContent();
|
|
2238
2302
|
renderTree();
|
|
2239
2303
|
const chapters = state.work.volumes.flatMap((volume) => volume.chapters);
|
|
2240
2304
|
const targetChapter = chapters.find((chapter) => chapter.id === preferredChapterId) ?? chapters[0];
|
|
2241
2305
|
if (state.module === "editor" && preferredChapterId) await selectChapter(preferredChapterId);
|
|
2242
2306
|
else if (state.module === "editor" && targetChapter) await selectChapter(targetChapter.id);
|
|
2243
|
-
else if (state.module === "editor") showWelcome(true);
|
|
2307
|
+
else if (state.module === "editor" && canReadModule("editor")) showWelcome(true);
|
|
2308
|
+
else if (!canReadModule(state.module)) showWelcome(true);
|
|
2244
2309
|
else await showModule(state.module);
|
|
2245
2310
|
return true;
|
|
2246
2311
|
}
|
|
@@ -2310,6 +2375,7 @@ async function selectChapter(chapterId) {
|
|
|
2310
2375
|
state.chapter = await api(`/api/chapters/${chapterId}`);
|
|
2311
2376
|
lastSavedChapterSnapshot = { chapterId: state.chapter.id, title: state.chapter.title, content: state.chapter.content };
|
|
2312
2377
|
state.module = "editor";
|
|
2378
|
+
applyWorkAccessMode();
|
|
2313
2379
|
markActiveModule("editor");
|
|
2314
2380
|
$("#welcome-view").classList.add("hidden");
|
|
2315
2381
|
$("#module-view").classList.add("hidden");
|
|
@@ -2326,7 +2392,7 @@ async function selectChapter(chapterId) {
|
|
|
2326
2392
|
scheduleChapterLineNumbers();
|
|
2327
2393
|
$("#chapter-insight").classList.add("hidden");
|
|
2328
2394
|
updateChapterStats();
|
|
2329
|
-
if (!canEditProse()) setSaveState(
|
|
2395
|
+
if (!canEditProse()) setSaveState("正文只读");
|
|
2330
2396
|
else if (spacingChanged) scheduleChapterAutoSave(120);
|
|
2331
2397
|
else setSaveState("已保存");
|
|
2332
2398
|
renderTree();
|
|
@@ -2384,10 +2450,18 @@ const moduleMeta = {
|
|
|
2384
2450
|
|
|
2385
2451
|
async function showModule(module) {
|
|
2386
2452
|
if (!state.work) return showWelcome();
|
|
2387
|
-
if (!
|
|
2453
|
+
if (!canReadModule(module)) {
|
|
2454
|
+
const fallback = firstReadableUiModule(state.work);
|
|
2455
|
+
if (!fallback) {
|
|
2456
|
+
showWelcome(true);
|
|
2457
|
+
return toast("当前账户尚未获授权访问任何作品模块", "error");
|
|
2458
|
+
}
|
|
2459
|
+
module = fallback;
|
|
2460
|
+
}
|
|
2388
2461
|
if (module !== "editor" && state.module === "editor" && !confirmDiscardChanges()) return;
|
|
2389
2462
|
if (module !== "editor" && state.module === "editor" && state.dirty) setSaveState("已放弃修改");
|
|
2390
2463
|
state.module = module;
|
|
2464
|
+
applyWorkAccessMode();
|
|
2391
2465
|
markActiveModule(module);
|
|
2392
2466
|
if (module === "editor") {
|
|
2393
2467
|
if (state.chapter) await selectChapter(state.chapter.id);
|
|
@@ -2545,10 +2619,10 @@ async function renderSettings() {
|
|
|
2545
2619
|
async function renderCharacters() {
|
|
2546
2620
|
[state.characters, state.races, state.organizations] = await Promise.all([
|
|
2547
2621
|
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`)
|
|
2622
|
+
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
2623
|
+
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
2550
2624
|
]);
|
|
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
|
|
2625
|
+
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
2626
|
$("#module-content").innerHTML = auditPanel + (state.characters.length ? `<div class="card-grid">${state.characters.map((item) => {
|
|
2553
2627
|
const details = normalizeCharacterDetails(item.attributes?.details);
|
|
2554
2628
|
return `
|
|
@@ -2560,7 +2634,7 @@ async function renderCharacters() {
|
|
|
2560
2634
|
<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
2635
|
${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
2636
|
${item.profileSectionCount ? `<small class="character-section-count">${item.profileSectionCount} 个设定章节</small>` : ""}
|
|
2563
|
-
<div class="card-actions"><button data-edit-character="${esc(item.id)}">编辑</button>${
|
|
2637
|
+
<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
2638
|
}).join("")}</div>`
|
|
2565
2639
|
: emptyModule("还没有角色档案", "创建主要人物,并维护别名、身份、动机和当前状态。"));
|
|
2566
2640
|
$("#create-character-audit-task")?.addEventListener("click", async () => {
|
|
@@ -2618,7 +2692,7 @@ async function renderCharacters() {
|
|
|
2618
2692
|
async function renderRaces() {
|
|
2619
2693
|
[state.races, state.characters] = await Promise.all([
|
|
2620
2694
|
apiAllPages(`/api/works/${state.work.id}/races`),
|
|
2621
|
-
apiAllPages(`/api/works/${state.work.id}/characters`)
|
|
2695
|
+
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
2622
2696
|
]);
|
|
2623
2697
|
const renderRaceNode = (item) => `<details class="race-tree-node" open data-race-node="${esc(item.id)}">
|
|
2624
2698
|
<summary><span>${esc(item.name)}</span><small>${item.children.length} 个直接子种族</small></summary>
|
|
@@ -2628,7 +2702,7 @@ async function renderRaces() {
|
|
|
2628
2702
|
<p>${esc(item.description || "尚未填写种族简介")}</p>
|
|
2629
2703
|
<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
2704
|
<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>${
|
|
2705
|
+
<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
2706
|
</article>
|
|
2633
2707
|
${item.children.length ? `<div class="race-tree-children">${item.children.map(renderRaceNode).join("")}</div>` : ""}
|
|
2634
2708
|
</div>
|
|
@@ -2665,14 +2739,14 @@ async function renderRaces() {
|
|
|
2665
2739
|
async function renderOrganizations() {
|
|
2666
2740
|
[state.organizations, state.characters] = await Promise.all([
|
|
2667
2741
|
apiAllPages(`/api/works/${state.work.id}/organizations`),
|
|
2668
|
-
apiAllPages(`/api/works/${state.work.id}/characters`)
|
|
2742
|
+
canReadModule("characters") ? apiAllPages(`/api/works/${state.work.id}/characters`) : Promise.resolve([])
|
|
2669
2743
|
]);
|
|
2670
2744
|
$("#module-content").innerHTML = state.organizations.length ? `<div class="card-grid organization-grid">${state.organizations.map((item) => `
|
|
2671
2745
|
<article class="record-card organization-card"><small>${item.memberIds.length} 位成员 · ${item.settings.length} 条设定</small>
|
|
2672
2746
|
<h3>${esc(item.name)}</h3><p>${esc(item.description || "尚未填写组织简介")}</p>
|
|
2673
2747
|
<div class="organization-settings">${item.settings.map((setting) => `<span class="pill">${esc(setting)}</span>`).join("") || '<span class="pill">暂无组织设定</span>'}</div>
|
|
2674
2748
|
<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>${
|
|
2749
|
+
<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
2750
|
</article>`).join("")}</div>` : emptyModule("还没有组织", "创建国家、机构、阵营或团队,并维护组织设定与成员。");
|
|
2677
2751
|
$("#module-content").querySelectorAll("[data-edit-organization]").forEach((button) => button.addEventListener("click", () => openOrganizationDialog(state.organizations.find((item) => item.id === button.dataset.editOrganization))));
|
|
2678
2752
|
$("#module-content").querySelectorAll("[data-merge-organization]").forEach((button) => button.addEventListener("click", () => {
|
|
@@ -2767,7 +2841,7 @@ async function renderOutlines() {
|
|
|
2767
2841
|
}
|
|
2768
2842
|
|
|
2769
2843
|
async function renderRelationships() {
|
|
2770
|
-
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
2844
|
+
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
2771
2845
|
const relationships = (await apiPage(`/api/works/${state.work.id}/relationships`)).items;
|
|
2772
2846
|
const nameOf = (id) => state.characters.find((item) => item.id === id)?.name ?? "未知角色";
|
|
2773
2847
|
state.galaxy?.destroy();
|
|
@@ -2798,9 +2872,13 @@ async function renderRelationships() {
|
|
|
2798
2872
|
}
|
|
2799
2873
|
|
|
2800
2874
|
async function renderReviews() {
|
|
2875
|
+
const canReadCharacters = canReadModule("characters");
|
|
2876
|
+
const canResolveReview = canEditModule("reviews");
|
|
2877
|
+
const canMergeCharacters = canResolveReview
|
|
2878
|
+
&& ["characters", "races", "organizations", "timeline", "relationships"].every((module) => canEditModule(module));
|
|
2801
2879
|
const [reviews, characters] = await Promise.all([
|
|
2802
2880
|
apiPage(`/api/works/${state.work.id}/reviews`).then((result) => result.items),
|
|
2803
|
-
apiAllPages(`/api/works/${state.work.id}/characters?includeMerged=1`)
|
|
2881
|
+
canReadCharacters ? apiAllPages(`/api/works/${state.work.id}/characters?includeMerged=1`) : Promise.resolve([])
|
|
2804
2882
|
]);
|
|
2805
2883
|
const characterById = new Map(characters.map((character) => [character.id, character]));
|
|
2806
2884
|
const duplicateCard = (item) => {
|
|
@@ -2808,17 +2886,21 @@ async function renderReviews() {
|
|
|
2808
2886
|
const sides = refs.map((reference) => ({ reference, character: characterById.get(reference.id) }));
|
|
2809
2887
|
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
2888
|
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
|
|
2889
|
+
const mergeActions = item.status === "pending" && sides.length === 2 && canMergeCharacters ? `
|
|
2812
2890
|
<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
|
-
|
|
2891
|
+
<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>` : "";
|
|
2892
|
+
const keepSeparateAction = item.status === "pending" && canResolveReview
|
|
2893
|
+
? `<button data-keep-characters-separate="${esc(item.id)}">确认是不同角色</button>`
|
|
2894
|
+
: "";
|
|
2895
|
+
const actions = mergeActions || keepSeparateAction
|
|
2896
|
+
? `<div class="card-actions character-duplicate-actions">${mergeActions}${keepSeparateAction}</div>`
|
|
2897
|
+
: "";
|
|
2816
2898
|
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
2899
|
};
|
|
2818
2900
|
$("#module-content").innerHTML = reviews.length ? `<div class="card-grid">${reviews.map((item) => item.itemType === "character-duplicate" ? duplicateCard(item) : `
|
|
2819
2901
|
<article class="record-card"><small>${esc(item.itemType)} · ${esc(item.severity)} · ${esc(item.status)}</small><h3>${esc(item.title)}</h3>
|
|
2820
2902
|
<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>`
|
|
2903
|
+
${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
2904
|
: emptyModule("没有待审核事项", "候选设定、冲突与低置信度结论会集中显示在这里。");
|
|
2823
2905
|
$("#module-content").querySelectorAll("[data-review-id]").forEach((button) => button.addEventListener("click", async () => {
|
|
2824
2906
|
await api(`/api/reviews/${button.dataset.reviewId}`, { method: "PATCH", body: { status: button.dataset.reviewStatus } });
|
|
@@ -2861,12 +2943,15 @@ async function renderReviews() {
|
|
|
2861
2943
|
async function renderTasks() {
|
|
2862
2944
|
const [tasks, settings] = await Promise.all([
|
|
2863
2945
|
apiPage(`/api/works/${state.work.id}/tasks`).then((result) => result.items),
|
|
2864
|
-
|
|
2946
|
+
canReadModule("ai-settings")
|
|
2947
|
+
? api(`/api/works/${state.work.id}/ai-settings`)
|
|
2948
|
+
: Promise.resolve({ autoRunEnabled: false, autoRunConcurrency: 2, autoRunBatchLimit: 20 })
|
|
2865
2949
|
]);
|
|
2950
|
+
const canConfigureAutoRun = canEditModule("tasks") && canEditModule("ai-settings");
|
|
2866
2951
|
const pendingCount = tasks.filter((item) => item.status === "pending").length;
|
|
2867
2952
|
const runningCount = tasks.filter((item) => item.status === "running").length;
|
|
2868
2953
|
$("#module-content").innerHTML = `
|
|
2869
|
-
<section class="task-auto-run-panel" aria-labelledby="task-auto-run-title">
|
|
2954
|
+
<section class="task-auto-run-panel ${canConfigureAutoRun ? "" : "hidden"}" aria-labelledby="task-auto-run-title">
|
|
2870
2955
|
<div class="task-auto-run-copy">
|
|
2871
2956
|
<strong id="task-auto-run-title">自动执行待分析任务</strong>
|
|
2872
2957
|
<small>只执行已经进入“待执行”队列的任务,不会自动创建人物关系、世界观或其他分析。</small>
|
|
@@ -3084,6 +3169,10 @@ async function renderBookAiSettings() {
|
|
|
3084
3169
|
"afterend",
|
|
3085
3170
|
`<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
3171
|
);
|
|
3172
|
+
if (!canEditModule("ai-settings")) {
|
|
3173
|
+
host.querySelectorAll("textarea, input, select").forEach((control) => { control.disabled = true; });
|
|
3174
|
+
host.querySelectorAll(".primary-button").forEach((button) => button.classList.add("permission-hidden"));
|
|
3175
|
+
}
|
|
3087
3176
|
$("#save-work-system-prompt").addEventListener("click", async () => {
|
|
3088
3177
|
const button = $("#save-work-system-prompt");
|
|
3089
3178
|
button.disabled = true;
|
|
@@ -3295,8 +3384,8 @@ async function loadAiReferences() {
|
|
|
3295
3384
|
const workId = state.work?.id;
|
|
3296
3385
|
if (!workId) return;
|
|
3297
3386
|
const [characters, settings] = await Promise.all([
|
|
3298
|
-
apiAllPages(`/api/works/${workId}/characters`),
|
|
3299
|
-
apiAllPages(`/api/works/${workId}/settings`)
|
|
3387
|
+
canReadModule("characters") ? apiAllPages(`/api/works/${workId}/characters`) : Promise.resolve([]),
|
|
3388
|
+
canReadModule("settings") ? apiAllPages(`/api/works/${workId}/settings`) : Promise.resolve([])
|
|
3300
3389
|
]);
|
|
3301
3390
|
if (state.work?.id !== workId) return;
|
|
3302
3391
|
state.characters = characters;
|
|
@@ -3533,8 +3622,8 @@ function openWorkSettingsDialog(work) {
|
|
|
3533
3622
|
if (!work) return;
|
|
3534
3623
|
const canManageAccess = ["admin", "owner"].includes(String(work.accessRole));
|
|
3535
3624
|
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"
|
|
3625
|
+
<div><strong id="work-access-title">成员权限</strong><small>选择成员后,可为每个作品模块单独设置无权限、只读或可编辑。</small><div class="work-access-options" aria-label="成员权限配置方式"><span>按成员配置</span><span>按模块授权</span><span>读写分离</span></div></div>
|
|
3626
|
+
${canManageAccess ? '<button id="work-access-manage" class="ghost-button" type="button">配置成员权限</button>' : '<small>仅作品创建者或系统管理员可以调整访问权限。</small>'}
|
|
3538
3627
|
</section>`;
|
|
3539
3628
|
openDialog("作品信息",
|
|
3540
3629
|
workCoverFieldHtml(work) + field("title", "作品名称", "text", work.title) + field("author", "作者", "text", work.author) + field("description", "简介", "textarea", work.description) + accessField,
|
|
@@ -3610,13 +3699,13 @@ function openSettingEditor(item = null) {
|
|
|
3610
3699
|
$("#setting-change-note").value = "";
|
|
3611
3700
|
$("#setting-change-note-field").classList.toggle("hidden", !item);
|
|
3612
3701
|
$("#setting-editor-submit").textContent = item ? "保存新版本" : "创建设定";
|
|
3613
|
-
const viewOnly = !
|
|
3702
|
+
const viewOnly = !canEditModule("settings");
|
|
3614
3703
|
$("#setting-editor-form").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = viewOnly; });
|
|
3615
3704
|
$("#setting-editor-form").querySelectorAll("select, input[type='checkbox']").forEach((control) => { control.disabled = viewOnly; });
|
|
3616
3705
|
$("#setting-editor-submit").classList.toggle("hidden", viewOnly);
|
|
3617
3706
|
$("#setting-editor-form").onsubmit = async (event) => {
|
|
3618
3707
|
event.preventDefault();
|
|
3619
|
-
if (!
|
|
3708
|
+
if (!canEditModule("settings")) return;
|
|
3620
3709
|
const form = new FormData(event.currentTarget);
|
|
3621
3710
|
const submit = $("#setting-editor-submit");
|
|
3622
3711
|
submit.disabled = true;
|
|
@@ -3698,11 +3787,11 @@ function renderCharacterEditorRelationships() {
|
|
|
3698
3787
|
const relationLabel = [category, relationship.subtype].filter(Boolean).join(" · ") || "未细分";
|
|
3699
3788
|
const keywords = Array.isArray(relationship.keywords) ? relationship.keywords : [];
|
|
3700
3789
|
return `<article class="character-relationship-row">
|
|
3701
|
-
<div class="character-relationship-heading"><div><strong>${esc(nameOf(otherCharacterId))}</strong><span>${direction} ${esc(relationLabel)}</span></div>${
|
|
3790
|
+
<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
3791
|
<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
3792
|
</article>`;
|
|
3704
3793
|
}).join("");
|
|
3705
|
-
host.innerHTML = `<div class="character-relationship-toolbar"><p>与 ${esc(characterEditorItem.name)} 有关的其他人物及关系关键词。</p>${
|
|
3794
|
+
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
3795
|
host.querySelectorAll("[data-character-relationship-edit]").forEach((button) => button.addEventListener("click", () => {
|
|
3707
3796
|
const relationship = characterEditorRelationships.find((item) => item.id === button.dataset.characterRelationshipEdit);
|
|
3708
3797
|
if (relationship) void openRelationshipDialog(relationship, { characterId });
|
|
@@ -3932,9 +4021,9 @@ function renderCharacterMarkdownSections() {
|
|
|
3932
4021
|
host.innerHTML = '<div class="character-editor-empty-field"><b>Markdown 档案章节</b><span>创建人物档案后即可添加背景故事、能力、经历和研究记录。</span></div>';
|
|
3933
4022
|
return;
|
|
3934
4023
|
}
|
|
3935
|
-
const toolbar = `<div class="character-markdown-list-toolbar"><div><b>Markdown 档案章节</b><span>长篇内容独立保存、渲染、检索和版本管理。</span></div>${
|
|
4024
|
+
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
4025
|
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>${
|
|
4026
|
+
<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
4027
|
<div class="character-markdown-document message-body">${renderMarkdown(section.contentMarkdown) || '<p class="character-markdown-empty">本章节暂无正文。</p>'}</div>
|
|
3939
4028
|
<div data-character-section-versions-host="${esc(section.id)}"></div>
|
|
3940
4029
|
</article>`).join("");
|
|
@@ -3992,14 +4081,20 @@ function renderCharacterEditorFields(item) {
|
|
|
3992
4081
|
characterEditorSection("basic", "基础资料", "用于检索、去重和建立人物在作品中的基本归属。",
|
|
3993
4082
|
field("name", "标准名", "text", item?.name) +
|
|
3994
4083
|
field("aliases", "别名", "item-list", item?.aliases ?? []) +
|
|
3995
|
-
(
|
|
4084
|
+
(!canReadModule("races")
|
|
4085
|
+
? '<div class="character-editor-empty-field"><b>种族</b><span>当前账户没有种族模块读取权限,原有绑定不会被修改。</span></div>'
|
|
4086
|
+
: state.races.length
|
|
3996
4087
|
? field("raceId", "种族", "select", item?.raceId ?? "", raceOptions)
|
|
3997
4088
|
: '<div class="character-editor-empty-field"><b>种族</b><span>尚未创建种族,请先在“种族”模块建立档案。</span></div>') +
|
|
3998
|
-
(
|
|
4089
|
+
(!canReadModule("organizations")
|
|
4090
|
+
? '<div class="character-editor-empty-field"><b>所属组织</b><span>当前账户没有组织模块读取权限,原有绑定不会被修改。</span></div>'
|
|
4091
|
+
: organizationOptions.length
|
|
3999
4092
|
? field("organizationIds", "所属组织(可多选)", "chips", item?.organizationIds ?? [], organizationOptions)
|
|
4000
4093
|
: '<div class="character-editor-empty-field"><b>所属组织</b><span>尚未创建组织,可稍后在“组织”模块中补充。</span></div>') +
|
|
4001
4094
|
field("visibility", "可见范围", "select", item?.visibility ?? "author", [["author", "仅作者"], ["collaborators", "协作者"], ["public", "公开"]]) +
|
|
4002
|
-
|
|
4095
|
+
(canReadModule("editor")
|
|
4096
|
+
? field("firstChapterId", "首次登场章节", "select", item?.firstChapterId ?? "", chapterOptions)
|
|
4097
|
+
: '<div class="character-editor-empty-field"><b>首次登场章节</b><span>当前账户没有正文读取权限,原有绑定不会被修改。</span></div>')),
|
|
4003
4098
|
characterEditorSection("profile", "人物档案", "记录人物定位、行为动力和便于创作时快速理解的简介。",
|
|
4004
4099
|
field("identity", "身份与定位", "text", item?.attributes?.identity) +
|
|
4005
4100
|
field("motivation", "核心动机", "textarea", item?.profile?.motivation) +
|
|
@@ -4035,11 +4130,9 @@ function collectCharacterBody(form) {
|
|
|
4035
4130
|
const item = characterEditorItem;
|
|
4036
4131
|
const profile = { ...(item?.profile ?? {}) };
|
|
4037
4132
|
delete profile.sections;
|
|
4038
|
-
|
|
4133
|
+
const body = {
|
|
4039
4134
|
name: String(form.get("name") ?? "").trim(),
|
|
4040
4135
|
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
4136
|
attributes: {
|
|
4044
4137
|
...(item?.attributes ?? {}),
|
|
4045
4138
|
identity: String(form.get("identity") ?? "").trim(),
|
|
@@ -4053,9 +4146,12 @@ function collectCharacterBody(form) {
|
|
|
4053
4146
|
currentState: buildCharacterState(form.getAll("stateKey"), form.getAll("stateValue"), item?.currentState ?? {}),
|
|
4054
4147
|
lockedFields: form.getAll("lockedFields").map((value) => String(value).trim()).filter(Boolean),
|
|
4055
4148
|
visibility: String(form.get("visibility") ?? "author"),
|
|
4056
|
-
firstChapterId: form.get("firstChapterId") || null,
|
|
4057
4149
|
changeNote: String(form.get("changeNote") ?? "").trim()
|
|
4058
4150
|
};
|
|
4151
|
+
if (canReadModule("races")) body.raceId = form.get("raceId") || null;
|
|
4152
|
+
if (canReadModule("organizations")) body.organizationIds = form.getAll("organizationIds").map(String);
|
|
4153
|
+
if (canReadModule("editor")) body.firstChapterId = form.get("firstChapterId") || null;
|
|
4154
|
+
return body;
|
|
4059
4155
|
}
|
|
4060
4156
|
|
|
4061
4157
|
function renderCharacterHistory() {
|
|
@@ -4127,8 +4223,8 @@ async function showCharacterHistory() {
|
|
|
4127
4223
|
|
|
4128
4224
|
async function openCharacterEditor(item = null) {
|
|
4129
4225
|
[state.races, state.organizations] = await Promise.all([
|
|
4130
|
-
apiAllPages(`/api/works/${state.work.id}/races`),
|
|
4131
|
-
apiAllPages(`/api/works/${state.work.id}/organizations`)
|
|
4226
|
+
canReadModule("races") ? apiAllPages(`/api/works/${state.work.id}/races`) : Promise.resolve([]),
|
|
4227
|
+
canReadModule("organizations") ? apiAllPages(`/api/works/${state.work.id}/organizations`) : Promise.resolve([])
|
|
4132
4228
|
]);
|
|
4133
4229
|
characterEditorItem = item ?? null;
|
|
4134
4230
|
characterEditorVersions = [];
|
|
@@ -4144,7 +4240,7 @@ async function openCharacterEditor(item = null) {
|
|
|
4144
4240
|
$("#character-history-button").title = item ? "查看、比较和回滚历史版本" : "创建人物档案后即可查看版本历史";
|
|
4145
4241
|
setCharacterHistoryVisible(false);
|
|
4146
4242
|
renderCharacterEditorFields(item);
|
|
4147
|
-
const viewOnly = !
|
|
4243
|
+
const viewOnly = !canEditModule("characters");
|
|
4148
4244
|
if (viewOnly) {
|
|
4149
4245
|
$("#character-editor-eyebrow").textContent = "人物档案";
|
|
4150
4246
|
$("#character-editor-fields").querySelectorAll("input, textarea").forEach((control) => { control.readOnly = true; });
|
|
@@ -4156,12 +4252,12 @@ async function openCharacterEditor(item = null) {
|
|
|
4156
4252
|
button.onclick = () => activateCharacterEditorTab(button.dataset.characterEditorTab);
|
|
4157
4253
|
});
|
|
4158
4254
|
const relationshipTab = document.querySelector("[data-character-editor-tab='relationships']");
|
|
4159
|
-
relationshipTab.disabled = !item;
|
|
4160
|
-
relationshipTab.title = item ? "查看和编辑人物关系" : "创建人物档案后即可维护人物关系";
|
|
4255
|
+
relationshipTab.disabled = !item || !canReadModule("relationships");
|
|
4256
|
+
relationshipTab.title = !canReadModule("relationships") ? "当前账户没有关系模块读取权限" : item ? "查看和编辑人物关系" : "创建人物档案后即可维护人物关系";
|
|
4161
4257
|
const form = $("#character-editor-form");
|
|
4162
4258
|
form.onsubmit = async (event) => {
|
|
4163
4259
|
event.preventDefault();
|
|
4164
|
-
if (!
|
|
4260
|
+
if (!canEditModule("characters")) return;
|
|
4165
4261
|
const submit = $("#character-editor-submit");
|
|
4166
4262
|
submit.disabled = true;
|
|
4167
4263
|
try {
|
|
@@ -4183,13 +4279,13 @@ async function openCharacterEditor(item = null) {
|
|
|
4183
4279
|
};
|
|
4184
4280
|
showEntityEditorPage("character");
|
|
4185
4281
|
if (item) {
|
|
4186
|
-
void loadCharacterEditorRelationships(item.id);
|
|
4282
|
+
if (canReadModule("relationships")) void loadCharacterEditorRelationships(item.id);
|
|
4187
4283
|
void loadCharacterMarkdownSections(item.id);
|
|
4188
4284
|
}
|
|
4189
4285
|
}
|
|
4190
4286
|
|
|
4191
4287
|
async function openRaceDialog(item) {
|
|
4192
|
-
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
4288
|
+
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
4193
4289
|
const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
|
|
4194
4290
|
const parentOptions = [["", "无(根种族)"], ...eligibleRaceParents(state.races, item?.id)
|
|
4195
4291
|
.sort((left, right) => racePathLabel(left).localeCompare(racePathLabel(right), "zh-CN"))
|
|
@@ -4202,7 +4298,8 @@ async function openRaceDialog(item) {
|
|
|
4202
4298
|
(memberOptions.length ? field("memberIds", "属于该种族的角色(可多选)", "chips", item?.memberIds ?? [], memberOptions) : ""),
|
|
4203
4299
|
async (form) => {
|
|
4204
4300
|
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
|
|
4301
|
+
const body = { name: form.get("name"), parentRaceId: form.get("parentRaceId") || null, description: form.get("description"), settings };
|
|
4302
|
+
if (canReadModule("characters")) body.memberIds = form.getAll("memberIds").map(String);
|
|
4206
4303
|
await api(item ? `/api/races/${item.id}` : `/api/works/${state.work.id}/races`, { method: item ? "PATCH" : "POST", body });
|
|
4207
4304
|
await renderRaces();
|
|
4208
4305
|
await loadAiReferences();
|
|
@@ -4210,7 +4307,7 @@ async function openRaceDialog(item) {
|
|
|
4210
4307
|
}
|
|
4211
4308
|
|
|
4212
4309
|
async function openOrganizationDialog(item) {
|
|
4213
|
-
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
4310
|
+
state.characters = canReadModule("characters") ? await apiAllPages(`/api/works/${state.work.id}/characters`) : [];
|
|
4214
4311
|
const memberOptions = state.characters.map((character) => [character.id, `${character.name}${character.aliases.length ? `(${character.aliases.join("、")})` : ""}`]);
|
|
4215
4312
|
openDialog(item ? "编辑组织" : "新建组织",
|
|
4216
4313
|
field("name", "组织名称", "text", item?.name) +
|
|
@@ -4219,7 +4316,8 @@ async function openOrganizationDialog(item) {
|
|
|
4219
4316
|
(memberOptions.length ? field("memberIds", "组织成员(可多选)", "chips", item?.memberIds ?? [], memberOptions) : ""),
|
|
4220
4317
|
async (form) => {
|
|
4221
4318
|
const settings = form.getAll("settings").map((value) => String(value).trim()).filter(Boolean);
|
|
4222
|
-
const body = { name: form.get("name"), description: form.get("description"), settings
|
|
4319
|
+
const body = { name: form.get("name"), description: form.get("description"), settings };
|
|
4320
|
+
if (canReadModule("characters")) body.memberIds = form.getAll("memberIds").map(String);
|
|
4223
4321
|
await api(item ? `/api/organizations/${item.id}` : `/api/works/${state.work.id}/organizations`, { method: item ? "PATCH" : "POST", body });
|
|
4224
4322
|
await renderOrganizations();
|
|
4225
4323
|
await loadAiReferences();
|
|
@@ -4316,6 +4414,7 @@ function openTimelineSplitDialog(item) {
|
|
|
4316
4414
|
}
|
|
4317
4415
|
|
|
4318
4416
|
async function openRelationshipDialog(item, options = {}) {
|
|
4417
|
+
if (!canReadModule("characters")) return toast("配置人物关系前需要角色模块读取权限", "error");
|
|
4319
4418
|
state.characters = await apiAllPages(`/api/works/${state.work.id}/characters`);
|
|
4320
4419
|
if (state.characters.length < 2) return toast("至少需要两个角色才能创建关系", "error");
|
|
4321
4420
|
const characterOptions = state.characters.map((item) => [item.id, item.name]);
|
|
@@ -4918,18 +5017,34 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
|
|
|
4918
5017
|
}
|
|
4919
5018
|
});
|
|
4920
5019
|
$("#members-dialog-close").addEventListener("click", () => $("#members-dialog").close());
|
|
4921
|
-
$("#members-dialog").addEventListener("close", () => {
|
|
4922
|
-
|
|
5020
|
+
$("#members-dialog").addEventListener("close", () => {
|
|
5021
|
+
memberDialogWork = null;
|
|
5022
|
+
memberDialogMembers = [];
|
|
5023
|
+
memberDialogDirectory = [];
|
|
5024
|
+
});
|
|
5025
|
+
$("#member-user-select").addEventListener("change", () => selectMemberForConfiguration($("#member-user-select").value));
|
|
5026
|
+
$("#member-permission-form").querySelectorAll("[data-permission-preset]").forEach((button) => button.addEventListener("click", () => {
|
|
5027
|
+
$("#member-permission-grid").querySelectorAll("[data-member-permission]").forEach((select) => {
|
|
5028
|
+
select.value = button.dataset.permissionPreset;
|
|
5029
|
+
});
|
|
5030
|
+
}));
|
|
5031
|
+
$("#member-permission-form").addEventListener("submit", async (event) => {
|
|
4923
5032
|
event.preventDefault();
|
|
4924
5033
|
const userId = $("#member-user-select").value;
|
|
4925
5034
|
const work = memberDialogWork ?? state.work;
|
|
4926
5035
|
if (!work || !userId) return;
|
|
4927
5036
|
try {
|
|
4928
|
-
const
|
|
4929
|
-
const
|
|
5037
|
+
const existing = memberDialogMembers.some((member) => member.userId === userId && member.role !== "owner");
|
|
5038
|
+
const permissions = selectedMemberPermissions();
|
|
5039
|
+
const members = await api(existing
|
|
5040
|
+
? `/api/works/${encodeURIComponent(work.id)}/members/${encodeURIComponent(userId)}`
|
|
5041
|
+
: `/api/works/${encodeURIComponent(work.id)}/members`, {
|
|
5042
|
+
method: existing ? "PATCH" : "POST",
|
|
5043
|
+
body: existing ? { permissions } : { userId, permissions }
|
|
5044
|
+
});
|
|
4930
5045
|
renderMembers(members);
|
|
4931
|
-
|
|
4932
|
-
toast(
|
|
5046
|
+
renderMemberSelector(userId);
|
|
5047
|
+
toast(existing ? "成员模块权限已更新" : "成员已添加并保存模块权限");
|
|
4933
5048
|
} catch (error) { toast(error.message, "error"); }
|
|
4934
5049
|
});
|
|
4935
5050
|
$("#platform-new-provider").addEventListener("click", () => openProviderDialog());
|