@musnows/scriverse 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +12 -4
- package/README.md +12 -4
- package/dist/app.js +18 -5
- package/dist/app.js.map +1 -1
- package/dist/public/app.js +191 -17
- package/dist/public/index.html +13 -9
- package/dist/public/page-route.js +5 -1
- package/dist/public/relationship-keywords.js +34 -0
- package/dist/public/styles.css +48 -1
- package/dist/public/theme-init.js +15 -0
- package/dist/security.js +1 -1
- package/dist/security.js.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/store.js +3 -4
- package/dist/store.js.map +1 -1
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -15,6 +15,7 @@ import { buildCharacterDetails, buildCharacterSections, buildCharacterState, cha
|
|
|
15
15
|
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260713-character-history";
|
|
16
16
|
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260714-all-knowledge-history";
|
|
17
17
|
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260714-refresh-restore";
|
|
18
|
+
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
18
19
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
19
20
|
|
|
20
21
|
const state = {
|
|
@@ -459,6 +460,8 @@ let aiMentionRange = null;
|
|
|
459
460
|
let settingsReturnContext = null;
|
|
460
461
|
let characterEditorItem = null;
|
|
461
462
|
let characterEditorVersions = [];
|
|
463
|
+
let characterEditorRelationships = [];
|
|
464
|
+
let characterEditorRelationshipsLoading = false;
|
|
462
465
|
let entityHistoryContext = null;
|
|
463
466
|
|
|
464
467
|
function setModuleNavExpanded(expanded) {
|
|
@@ -1393,7 +1396,19 @@ function selectAuthMode(mode) {
|
|
|
1393
1396
|
$("#login-form").classList.toggle("hidden", !login);
|
|
1394
1397
|
$("#register-form").classList.toggle("hidden", login);
|
|
1395
1398
|
$("#auth-error").textContent = "";
|
|
1396
|
-
|
|
1399
|
+
// 验证码默认不加载,等用户点击“点击显示验证码”按钮后再请求,避免首屏空白等待
|
|
1400
|
+
resetAuthCaptcha(login ? "login" : "register");
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function resetAuthCaptcha(target = "login") {
|
|
1404
|
+
const prefix = target === "register" ? "register" : "login";
|
|
1405
|
+
const image = $(`#${prefix}-captcha-image`);
|
|
1406
|
+
image.hidden = true;
|
|
1407
|
+
image.removeAttribute("src");
|
|
1408
|
+
$(`#${prefix}-captcha-placeholder`).hidden = false;
|
|
1409
|
+
$(`#${prefix}-captcha-id`).value = "";
|
|
1410
|
+
const answerInput = $(`#${prefix}-form`).querySelector('input[name="captchaAnswer"]');
|
|
1411
|
+
if (answerInput) answerInput.value = "";
|
|
1397
1412
|
}
|
|
1398
1413
|
|
|
1399
1414
|
async function refreshAuthCaptcha(target = "login") {
|
|
@@ -1402,24 +1417,31 @@ async function refreshAuthCaptcha(target = "login") {
|
|
|
1402
1417
|
const challenge = (await response.json()).data;
|
|
1403
1418
|
const prefix = target === "register" ? "register" : "login";
|
|
1404
1419
|
$(`#${prefix}-captcha-id`).value = challenge.captchaId;
|
|
1405
|
-
$(`#${prefix}-captcha-image`)
|
|
1420
|
+
const image = $(`#${prefix}-captcha-image`);
|
|
1421
|
+
image.src = challenge.imageDataUrl;
|
|
1422
|
+
image.hidden = false;
|
|
1423
|
+
$(`#${prefix}-captcha-placeholder`).hidden = true;
|
|
1406
1424
|
const answerInput = $(`#${prefix}-form`).querySelector('input[name="captchaAnswer"]');
|
|
1407
1425
|
if (answerInput) answerInput.value = "";
|
|
1408
1426
|
}
|
|
1409
1427
|
|
|
1410
|
-
function showAuth(setupRequired, registrationOpen =
|
|
1428
|
+
function showAuth(setupRequired, registrationOpen = false) {
|
|
1411
1429
|
document.body.classList.add("auth-pending");
|
|
1412
1430
|
$("#auth-view").classList.remove("hidden");
|
|
1413
|
-
|
|
1431
|
+
const canRegister = registrationOpen === true;
|
|
1432
|
+
$("#auth-title").textContent = setupRequired
|
|
1433
|
+
? canRegister ? "创建首个管理员账户" : "注册已禁用"
|
|
1434
|
+
: "登录后继续创作";
|
|
1414
1435
|
$("#auth-description").textContent = setupRequired
|
|
1415
|
-
?
|
|
1436
|
+
? canRegister
|
|
1437
|
+
? "这是首次启动。首个注册用户会成为系统管理员,并接管现有作品。"
|
|
1438
|
+
: "请将 APP_ALLOW_REGISTRATION 设置为 true 后创建首个管理员账户。"
|
|
1416
1439
|
: "你的作品、协作权限和每一次修改都会绑定到账户。";
|
|
1417
|
-
const canRegister = setupRequired || registrationOpen;
|
|
1418
1440
|
const registerTab = $("#auth-register-tab");
|
|
1419
1441
|
registerTab.disabled = !canRegister;
|
|
1420
1442
|
registerTab.setAttribute("aria-disabled", String(!canRegister));
|
|
1421
1443
|
registerTab.textContent = canRegister ? "注册" : "注册已禁用";
|
|
1422
|
-
selectAuthMode(setupRequired ? "register" : "login");
|
|
1444
|
+
selectAuthMode(setupRequired && canRegister ? "register" : "login");
|
|
1423
1445
|
}
|
|
1424
1446
|
|
|
1425
1447
|
function applyAuthenticatedUser(session) {
|
|
@@ -1430,7 +1452,9 @@ function applyAuthenticatedUser(session) {
|
|
|
1430
1452
|
$("#account-menu-name").textContent = `${session.user.displayName} · @${session.user.username}`;
|
|
1431
1453
|
$("#account-menu-role").textContent = session.user.role === "admin" ? "系统管理员" : "普通用户";
|
|
1432
1454
|
$("#auth-view").classList.add("hidden");
|
|
1433
|
-
document.
|
|
1455
|
+
document.documentElement.classList.remove("login-route");
|
|
1456
|
+
// 注意:auth-pending 由 initializePage 路由完成后才移除,
|
|
1457
|
+
// 避免会话确认后、目标视图渲染前露出无内容的编辑器外壳
|
|
1434
1458
|
}
|
|
1435
1459
|
|
|
1436
1460
|
function applyPlatformUiSettings(settings) {
|
|
@@ -1447,13 +1471,18 @@ async function loadPlatformUiSettings() {
|
|
|
1447
1471
|
}
|
|
1448
1472
|
|
|
1449
1473
|
async function initializeAuthentication() {
|
|
1474
|
+
const route = parsePageRoute(window.location.hash);
|
|
1450
1475
|
const response = await fetch("/api/auth/session", { headers: { Accept: "application/json" } });
|
|
1451
1476
|
if (!response.ok) throw new Error("无法读取登录状态");
|
|
1452
1477
|
const session = (await response.json()).data;
|
|
1453
1478
|
if (!session.authenticated) {
|
|
1454
|
-
|
|
1479
|
+
// 未登录时一律转到登录页路由;登录页本身则保持原样
|
|
1480
|
+
if (route.view !== "login") window.history.replaceState(null, "", serializePageRoute({ view: "login" }));
|
|
1481
|
+
showAuth(session.setupRequired, session.registrationOpen === true);
|
|
1455
1482
|
return false;
|
|
1456
1483
|
}
|
|
1484
|
+
// 已登录却停在登录页路由时,回到书架首页
|
|
1485
|
+
if (route.view === "login") window.history.replaceState(null, "", serializePageRoute({ view: "shelf" }));
|
|
1457
1486
|
applyAuthenticatedUser(session);
|
|
1458
1487
|
await loadPlatformUiSettings();
|
|
1459
1488
|
return true;
|
|
@@ -1660,6 +1689,7 @@ async function initializePage() {
|
|
|
1660
1689
|
settingsReturnContext = restoredSettingsReturnContext(route);
|
|
1661
1690
|
}
|
|
1662
1691
|
} finally {
|
|
1692
|
+
document.body.classList.remove("auth-pending");
|
|
1663
1693
|
restoringPageRoute = false;
|
|
1664
1694
|
replacePageRoute(currentPageRoute());
|
|
1665
1695
|
scheduleFirstUseOnboarding();
|
|
@@ -2934,6 +2964,11 @@ function field(name, label, type = "text", value = "", options = []) {
|
|
|
2934
2964
|
const values = Array.isArray(value) && value.length ? value : [""];
|
|
2935
2965
|
return `<div class="form-field item-list-field"><span>${esc(label)}</span><div class="item-list-rows" data-item-list-rows data-name="${esc(name)}" data-label="${esc(label)}">${values.map((item) => `<div class="item-list-row"><input name="${esc(name)}" value="${esc(item)}" aria-label="${esc(label)}"><button type="button" data-item-list-remove aria-label="删除此条">删除</button></div>`).join("")}</div><button class="item-list-add" type="button" data-item-list-add>添加一条</button></div>`;
|
|
2936
2966
|
}
|
|
2967
|
+
if (type === "keyword-chips") {
|
|
2968
|
+
const values = uniqueRelationshipKeywords(Array.isArray(value) ? value : []);
|
|
2969
|
+
const chips = values.map((keyword) => `<span class="keyword-chip" data-keyword-chip><span>${esc(keyword)}</span><input type="hidden" name="${esc(name)}" value="${esc(keyword)}" data-keyword-value><button type="button" data-keyword-chip-remove aria-label="删除关键词:${esc(keyword)}">×</button></span>`).join("");
|
|
2970
|
+
return `<div class="form-field keyword-chip-field" data-keyword-chips data-name="${esc(name)}"><span>${esc(label)}</span><div class="keyword-chip-editor" role="group" aria-label="${esc(label)}">${chips}<input type="text" data-keyword-input aria-label="${esc(label)}" placeholder="输入后按回车添加,逗号可批量添加" autocomplete="off"></div><small>输入关键词后按回车添加;也可用逗号一次添加多个。</small></div>`;
|
|
2971
|
+
}
|
|
2937
2972
|
if (type === "key-value-list") {
|
|
2938
2973
|
const config = Array.isArray(options) ? {} : options;
|
|
2939
2974
|
const keyName = config.keyName ?? "detailLabel";
|
|
@@ -3000,6 +3035,53 @@ function bindDynamicListControls(container) {
|
|
|
3000
3035
|
};
|
|
3001
3036
|
}
|
|
3002
3037
|
|
|
3038
|
+
function appendRelationshipKeywordChips(editor, values) {
|
|
3039
|
+
const input = editor.querySelector("[data-keyword-input]");
|
|
3040
|
+
if (!input) return;
|
|
3041
|
+
const existing = new Set([...editor.querySelectorAll("[data-keyword-value]")].map((control) => String(control.value).toLocaleLowerCase("zh-CN")));
|
|
3042
|
+
const name = editor.dataset.name || "keywords";
|
|
3043
|
+
for (const keyword of uniqueRelationshipKeywords(values)) {
|
|
3044
|
+
const key = keyword.toLocaleLowerCase("zh-CN");
|
|
3045
|
+
if (existing.has(key)) continue;
|
|
3046
|
+
existing.add(key);
|
|
3047
|
+
input.insertAdjacentHTML("beforebegin", `<span class="keyword-chip" data-keyword-chip><span>${esc(keyword)}</span><input type="hidden" name="${esc(name)}" value="${esc(keyword)}" data-keyword-value><button type="button" data-keyword-chip-remove aria-label="删除关键词:${esc(keyword)}">×</button></span>`);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
function commitRelationshipKeywordInput(editor) {
|
|
3052
|
+
const input = editor.querySelector("[data-keyword-input]");
|
|
3053
|
+
if (!input) return;
|
|
3054
|
+
appendRelationshipKeywordChips(editor, splitRelationshipKeywords(input.value));
|
|
3055
|
+
input.value = "";
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
function bindRelationshipKeywordControls(container) {
|
|
3059
|
+
container.querySelectorAll("[data-keyword-chips]").forEach((editor) => {
|
|
3060
|
+
const input = editor.querySelector("[data-keyword-input]");
|
|
3061
|
+
if (!input) return;
|
|
3062
|
+
input.addEventListener("keydown", (event) => {
|
|
3063
|
+
if (event.isComposing || event.key !== "Enter") return;
|
|
3064
|
+
event.preventDefault();
|
|
3065
|
+
commitRelationshipKeywordInput(editor);
|
|
3066
|
+
});
|
|
3067
|
+
input.addEventListener("input", () => {
|
|
3068
|
+
const { completed, remainder } = splitRelationshipKeywordInput(input.value);
|
|
3069
|
+
if (!completed.length) return;
|
|
3070
|
+
appendRelationshipKeywordChips(editor, completed);
|
|
3071
|
+
input.value = remainder;
|
|
3072
|
+
});
|
|
3073
|
+
editor.addEventListener("click", (event) => {
|
|
3074
|
+
const remove = event.target.closest("[data-keyword-chip-remove]");
|
|
3075
|
+
if (!remove) return;
|
|
3076
|
+
remove.closest("[data-keyword-chip]")?.remove();
|
|
3077
|
+
});
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
function commitRelationshipKeywordInputs(container) {
|
|
3082
|
+
container.querySelectorAll("[data-keyword-chips]").forEach(commitRelationshipKeywordInput);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3003
3085
|
function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
3004
3086
|
$("#dialog-title").textContent = title;
|
|
3005
3087
|
$("#dialog-eyebrow").textContent = eyebrow;
|
|
@@ -3007,6 +3089,7 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
|
3007
3089
|
$("#dialog-submit").textContent = options.submitLabel ?? "保存";
|
|
3008
3090
|
$("#form-dialog").classList.toggle("wide-dialog", Boolean(options.wide));
|
|
3009
3091
|
bindDynamicListControls($("#dialog-fields"));
|
|
3092
|
+
bindRelationshipKeywordControls($("#dialog-fields"));
|
|
3010
3093
|
const form = $("#dynamic-form");
|
|
3011
3094
|
form.onsubmit = async (event) => {
|
|
3012
3095
|
if (event.submitter?.value === "cancel") return;
|
|
@@ -3014,6 +3097,7 @@ function openDialog(title, fields, onSubmit, eyebrow = "新增", options = {}) {
|
|
|
3014
3097
|
const submit = $("#dialog-submit");
|
|
3015
3098
|
submit.disabled = true;
|
|
3016
3099
|
try {
|
|
3100
|
+
commitRelationshipKeywordInputs(form);
|
|
3017
3101
|
await onSubmit(new FormData(form));
|
|
3018
3102
|
$("#form-dialog").close();
|
|
3019
3103
|
} catch (error) {
|
|
@@ -3188,6 +3272,83 @@ function setCharacterHistoryVisible(visible) {
|
|
|
3188
3272
|
$("#character-history-button").setAttribute("aria-expanded", String(visible));
|
|
3189
3273
|
}
|
|
3190
3274
|
|
|
3275
|
+
const relationshipCategoryLabels = {
|
|
3276
|
+
family: "亲属",
|
|
3277
|
+
social: "社交",
|
|
3278
|
+
emotional: "情感",
|
|
3279
|
+
conflict: "冲突",
|
|
3280
|
+
uncertain: "未确定"
|
|
3281
|
+
};
|
|
3282
|
+
|
|
3283
|
+
function renderCharacterEditorRelationships() {
|
|
3284
|
+
const host = $("#character-editor-relationships");
|
|
3285
|
+
if (!host) return;
|
|
3286
|
+
if (!characterEditorItem?.id) {
|
|
3287
|
+
host.innerHTML = '<div class="character-editor-empty-field"><b>人物关系</b><span>保存人物档案后即可添加与其他人物的关系。</span></div>';
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
if (characterEditorRelationshipsLoading) {
|
|
3291
|
+
host.innerHTML = '<p class="character-relationship-status">正在读取人物关系……</p>';
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
const characterId = String(characterEditorItem.id);
|
|
3295
|
+
const nameOf = (id) => state.characters.find((character) => character.id === id)?.name ?? "未知角色";
|
|
3296
|
+
const rows = characterEditorRelationships.map((relationship) => {
|
|
3297
|
+
const isSource = relationship.fromCharacterId === characterId;
|
|
3298
|
+
const otherCharacterId = isSource ? relationship.toCharacterId : relationship.fromCharacterId;
|
|
3299
|
+
const direction = relationship.directed ? (isSource ? "→" : "←") : "↔";
|
|
3300
|
+
const category = relationshipCategoryLabels[relationship.category] ?? relationship.category;
|
|
3301
|
+
const relationLabel = [category, relationship.subtype].filter(Boolean).join(" · ") || "未细分";
|
|
3302
|
+
const keywords = Array.isArray(relationship.keywords) ? relationship.keywords : [];
|
|
3303
|
+
return `<article class="character-relationship-row">
|
|
3304
|
+
<div class="character-relationship-heading"><div><strong>${esc(nameOf(otherCharacterId))}</strong><span>${direction} ${esc(relationLabel)}</span></div>${canEditWork() ? `<button type="button" data-character-relationship-edit="${esc(relationship.id)}">编辑关系</button>` : ""}</div>
|
|
3305
|
+
<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>
|
|
3306
|
+
</article>`;
|
|
3307
|
+
}).join("");
|
|
3308
|
+
host.innerHTML = `<div class="character-relationship-toolbar"><p>与 ${esc(characterEditorItem.name)} 有关的其他人物及关系关键词。</p>${canEditWork() ? '<button type="button" class="ghost-button" data-character-relationship-create>新建关系</button>' : ""}</div>${rows || '<p class="character-relationship-status">暂未记录与其他人物的关系。</p>'}`;
|
|
3309
|
+
host.querySelectorAll("[data-character-relationship-edit]").forEach((button) => button.addEventListener("click", () => {
|
|
3310
|
+
const relationship = characterEditorRelationships.find((item) => item.id === button.dataset.characterRelationshipEdit);
|
|
3311
|
+
if (relationship) void openRelationshipDialog(relationship, { characterId });
|
|
3312
|
+
}));
|
|
3313
|
+
host.querySelector("[data-character-relationship-create]")?.addEventListener("click", () => void openRelationshipDialog(null, { characterId }));
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
async function loadCharacterEditorRelationships(characterId) {
|
|
3317
|
+
const workId = state.work?.id;
|
|
3318
|
+
if (!workId || characterEditorItem?.id !== characterId) return;
|
|
3319
|
+
characterEditorRelationshipsLoading = true;
|
|
3320
|
+
renderCharacterEditorRelationships();
|
|
3321
|
+
let loaded = false;
|
|
3322
|
+
try {
|
|
3323
|
+
const [characters, relationships] = await Promise.all([
|
|
3324
|
+
api(`/api/works/${workId}/characters`),
|
|
3325
|
+
api(`/api/works/${workId}/relationships`)
|
|
3326
|
+
]);
|
|
3327
|
+
if (state.work?.id !== workId || characterEditorItem?.id !== characterId) return;
|
|
3328
|
+
state.characters = characters;
|
|
3329
|
+
characterEditorRelationships = relationships.filter((relationship) => relationship.fromCharacterId === characterId || relationship.toCharacterId === characterId);
|
|
3330
|
+
loaded = true;
|
|
3331
|
+
} catch (error) {
|
|
3332
|
+
if (state.work?.id === workId && characterEditorItem?.id === characterId) {
|
|
3333
|
+
$("#character-editor-relationships").innerHTML = `<p class="character-relationship-status">关系载入失败:${esc(error.message)}</p>`;
|
|
3334
|
+
}
|
|
3335
|
+
} finally {
|
|
3336
|
+
if (state.work?.id === workId && characterEditorItem?.id === characterId) {
|
|
3337
|
+
characterEditorRelationshipsLoading = false;
|
|
3338
|
+
if (loaded) renderCharacterEditorRelationships();
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
async function refreshRelationshipSurfaces(characterId = null) {
|
|
3344
|
+
const tasks = [];
|
|
3345
|
+
if (state.module === "relationships") tasks.push(renderRelationships());
|
|
3346
|
+
if (characterId && $("#character-editor-dialog").open && characterEditorItem?.id === characterId) {
|
|
3347
|
+
tasks.push(loadCharacterEditorRelationships(characterId));
|
|
3348
|
+
}
|
|
3349
|
+
await Promise.all(tasks);
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3191
3352
|
function renderCharacterEditorFields(item) {
|
|
3192
3353
|
const raceOptions = [["", "未指定"], ...state.races.map((race) => [race.id, race.name])];
|
|
3193
3354
|
const organizationOptions = state.organizations.map((organization) => [organization.id, organization.name]);
|
|
@@ -3224,11 +3385,14 @@ function renderCharacterEditorFields(item) {
|
|
|
3224
3385
|
addLabel: "添加状态"
|
|
3225
3386
|
}) +
|
|
3226
3387
|
'<p class="character-editor-field-help">未修改的数字、布尔值、数组和对象会保留原有数据类型;被修改的值会按文本保存。</p>' +
|
|
3227
|
-
field("lockedFields", "锁定字段", "item-list", item?.lockedFields ?? []))
|
|
3388
|
+
field("lockedFields", "锁定字段", "item-list", item?.lockedFields ?? [])),
|
|
3389
|
+
characterEditorSection("relationships", "人物关系", "查看与其他人物的关系及关键词;编辑入口与“关系”面板共用同一份关系数据。",
|
|
3390
|
+
'<div id="character-editor-relationships" class="character-editor-relationships-field"></div>')
|
|
3228
3391
|
].join("");
|
|
3229
3392
|
const name = $("#character-editor-fields [name='name']");
|
|
3230
3393
|
if (name) name.required = true;
|
|
3231
3394
|
bindDynamicListControls($("#character-editor-fields"));
|
|
3395
|
+
renderCharacterEditorRelationships();
|
|
3232
3396
|
activateCharacterEditorTab("basic");
|
|
3233
3397
|
}
|
|
3234
3398
|
|
|
@@ -3332,6 +3496,8 @@ async function openCharacterDialog(item) {
|
|
|
3332
3496
|
]);
|
|
3333
3497
|
characterEditorItem = item ?? null;
|
|
3334
3498
|
characterEditorVersions = [];
|
|
3499
|
+
characterEditorRelationships = [];
|
|
3500
|
+
characterEditorRelationshipsLoading = Boolean(item);
|
|
3335
3501
|
$("#character-editor-eyebrow").textContent = item ? "人物主档案" : "建立人物档案";
|
|
3336
3502
|
$("#character-editor-title").textContent = item?.name || "新建角色";
|
|
3337
3503
|
$("#character-editor-version").textContent = item ? `v${item.versionNo}` : "新档案";
|
|
@@ -3350,6 +3516,9 @@ async function openCharacterDialog(item) {
|
|
|
3350
3516
|
document.querySelectorAll("[data-character-editor-tab]").forEach((button) => {
|
|
3351
3517
|
button.onclick = () => activateCharacterEditorTab(button.dataset.characterEditorTab);
|
|
3352
3518
|
});
|
|
3519
|
+
const relationshipTab = document.querySelector("[data-character-editor-tab='relationships']");
|
|
3520
|
+
relationshipTab.disabled = !item;
|
|
3521
|
+
relationshipTab.title = item ? "查看和编辑人物关系" : "创建人物档案后即可维护人物关系";
|
|
3353
3522
|
const dialog = $("#character-editor-dialog");
|
|
3354
3523
|
const form = $("#character-editor-form");
|
|
3355
3524
|
form.onsubmit = async (event) => {
|
|
@@ -3374,6 +3543,7 @@ async function openCharacterDialog(item) {
|
|
|
3374
3543
|
}
|
|
3375
3544
|
};
|
|
3376
3545
|
dialog.showModal();
|
|
3546
|
+
if (item) void loadCharacterEditorRelationships(item.id);
|
|
3377
3547
|
}
|
|
3378
3548
|
|
|
3379
3549
|
async function openRaceDialog(item) {
|
|
@@ -3498,14 +3668,16 @@ function openTimelineSplitDialog(item) {
|
|
|
3498
3668
|
}, "原证据同步保留");
|
|
3499
3669
|
}
|
|
3500
3670
|
|
|
3501
|
-
async function openRelationshipDialog(item) {
|
|
3671
|
+
async function openRelationshipDialog(item, options = {}) {
|
|
3502
3672
|
state.characters = await api(`/api/works/${state.work.id}/characters`);
|
|
3503
3673
|
if (state.characters.length < 2) return toast("至少需要两个角色才能创建关系", "error");
|
|
3504
|
-
const
|
|
3505
|
-
|
|
3506
|
-
|
|
3674
|
+
const characterOptions = state.characters.map((item) => [item.id, item.name]);
|
|
3675
|
+
const defaultFrom = options.characterId && state.characters.some((character) => character.id === options.characterId) ? options.characterId : characterOptions[0][0];
|
|
3676
|
+
const defaultTo = characterOptions.find(([id]) => id !== defaultFrom)?.[0] ?? characterOptions[1][0];
|
|
3677
|
+
openDialog(item ? "编辑人物关系" : "新建人物关系", field("from", "起点人物", "select", item?.fromCharacterId ?? defaultFrom, characterOptions) + field("to", "终点人物", "select", item?.toCharacterId ?? defaultTo, characterOptions) + field("category", "关系大类", "select", item?.category ?? "social", [["family", "亲属"], ["social", "社交"], ["emotional", "情感"], ["conflict", "冲突"], ["uncertain", "未确定"]]) + field("subtype", "关系子类", "text", item?.subtype) + field("keywords", "关系关键词", "keyword-chips", item?.keywords ?? []) + field("confidence", "置信度(0-1)", "number", item?.confidence ?? "1") + field("directed", "有方向性", "checkbox", item?.directed ?? false), async (form) => {
|
|
3678
|
+
const keywords = uniqueRelationshipKeywords(form.getAll("keywords").map(String));
|
|
3507
3679
|
await api(item ? `/api/relationships/${item.id}` : `/api/works/${state.work.id}/relationships`, { method: item ? "PATCH" : "POST", body: { fromCharacterId: form.get("from"), toCharacterId: form.get("to"), category: form.get("category"), subtype: form.get("subtype"), keywords, confidence: Number(form.get("confidence")), directed: form.get("directed") === "on", confirmationStatus: item?.confirmationStatus ?? "confirmed" } });
|
|
3508
|
-
await
|
|
3680
|
+
await refreshRelationshipSurfaces(options.characterId ?? null);
|
|
3509
3681
|
}, item ? "关系档案" : "人工确认关系");
|
|
3510
3682
|
}
|
|
3511
3683
|
|
|
@@ -4034,7 +4206,8 @@ $("#login-form").addEventListener("submit", async (event) => {
|
|
|
4034
4206
|
window.location.reload();
|
|
4035
4207
|
} catch (error) {
|
|
4036
4208
|
$("#auth-error").textContent = error.message;
|
|
4037
|
-
|
|
4209
|
+
// 仅在验证码已显示时自动换一张,未加载过则保持默认隐藏状态
|
|
4210
|
+
if (!$("#login-captcha-image").hidden) refreshAuthCaptcha("login").catch(() => {});
|
|
4038
4211
|
}
|
|
4039
4212
|
});
|
|
4040
4213
|
$("#register-form").addEventListener("submit", async (event) => {
|
|
@@ -4060,7 +4233,8 @@ $("#register-form").addEventListener("submit", async (event) => {
|
|
|
4060
4233
|
window.location.reload();
|
|
4061
4234
|
} catch (error) {
|
|
4062
4235
|
$("#auth-error").textContent = error.message;
|
|
4063
|
-
|
|
4236
|
+
// 仅在验证码已显示时自动换一张,未加载过则保持默认隐藏状态
|
|
4237
|
+
if (!$("#register-captcha-image").hidden) refreshAuthCaptcha("register").catch(() => {});
|
|
4064
4238
|
}
|
|
4065
4239
|
});
|
|
4066
4240
|
$("#settings-return").addEventListener("click", () => returnFromSettings().catch((error) => toast(error.message, "error")));
|
package/dist/public/index.html
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
<meta name="description" content="面向长篇小说创作的 AI 协作工作台">
|
|
7
7
|
<meta name="theme-color" content="#8b3d2c">
|
|
8
8
|
<title>叙界 · 小说 AI 创作工作台</title>
|
|
9
|
-
<script src="/theme-init.js?v=
|
|
9
|
+
<script src="/theme-init.js?v=20260720-route-skeleton"></script>
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
|
-
<link rel="stylesheet" href="/styles.css?v=
|
|
12
|
+
<link rel="stylesheet" href="/styles.css?v=20260720-captcha-lazy-load">
|
|
13
13
|
</head>
|
|
14
14
|
<body class="auth-pending">
|
|
15
|
-
<section id="auth-view" class="auth-view" aria-labelledby="auth-title">
|
|
15
|
+
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
16
16
|
<div class="auth-card">
|
|
17
17
|
<div class="auth-brand"><span class="brand-mark">叙</span><div><strong>叙界</strong><small>小说 AI 创作工作台</small></div></div>
|
|
18
18
|
<span class="eyebrow">账户与作品空间</span>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<p id="auth-description">你的作品、协作权限和每一次修改都会绑定到账户。</p>
|
|
21
21
|
<div class="auth-tabs" role="tablist" aria-label="登录或注册">
|
|
22
22
|
<button id="auth-login-tab" type="button" role="tab" aria-selected="true">登录</button>
|
|
23
|
-
<button id="auth-register-tab" type="button" role="tab" aria-selected="false"
|
|
23
|
+
<button id="auth-register-tab" type="button" role="tab" aria-selected="false" aria-disabled="true" disabled>注册已禁用</button>
|
|
24
24
|
</div>
|
|
25
25
|
<form id="login-form" class="auth-form">
|
|
26
26
|
<label>用户名<input name="username" autocomplete="username" maxlength="100" required></label>
|
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
<div class="auth-captcha">
|
|
29
29
|
<label>验证码<input name="captchaAnswer" autocomplete="off" inputmode="text" maxlength="8" spellcheck="false" required aria-describedby="login-captcha-hint"></label>
|
|
30
30
|
<button id="login-captcha-refresh" class="auth-captcha-image" type="button" aria-label="刷新验证码" title="刷新验证码">
|
|
31
|
-
<img id="login-captcha-image" alt="验证码图片">
|
|
31
|
+
<img id="login-captcha-image" alt="验证码图片" hidden>
|
|
32
|
+
<span id="login-captcha-placeholder" class="auth-captcha-placeholder">点击显示验证码</span>
|
|
32
33
|
</button>
|
|
33
34
|
<input id="login-captcha-id" name="captchaId" type="hidden" required>
|
|
34
35
|
</div>
|
|
35
|
-
<p id="login-captcha-hint" class="auth-captcha-hint"
|
|
36
|
+
<p id="login-captcha-hint" class="auth-captcha-hint">点击按钮显示验证码,显示后点击图片可刷新</p>
|
|
36
37
|
<button class="primary-button" type="submit">登录</button>
|
|
37
38
|
</form>
|
|
38
39
|
<form id="register-form" class="auth-form hidden">
|
|
@@ -42,11 +43,12 @@
|
|
|
42
43
|
<div class="auth-captcha">
|
|
43
44
|
<label>验证码<input name="captchaAnswer" autocomplete="off" inputmode="text" maxlength="8" spellcheck="false" required aria-describedby="register-captcha-hint"></label>
|
|
44
45
|
<button id="register-captcha-refresh" class="auth-captcha-image" type="button" aria-label="刷新验证码" title="刷新验证码">
|
|
45
|
-
<img id="register-captcha-image" alt="验证码图片">
|
|
46
|
+
<img id="register-captcha-image" alt="验证码图片" hidden>
|
|
47
|
+
<span id="register-captcha-placeholder" class="auth-captcha-placeholder">点击显示验证码</span>
|
|
46
48
|
</button>
|
|
47
49
|
<input id="register-captcha-id" name="captchaId" type="hidden" required>
|
|
48
50
|
</div>
|
|
49
|
-
<p id="register-captcha-hint" class="auth-captcha-hint"
|
|
51
|
+
<p id="register-captcha-hint" class="auth-captcha-hint">点击按钮显示验证码,显示后点击图片可刷新</p>
|
|
50
52
|
<small>至少 10 个字符。系统中的第一个注册用户会自动成为管理员。</small>
|
|
51
53
|
<button class="primary-button" type="submit">创建账户</button>
|
|
52
54
|
</form>
|
|
@@ -332,6 +334,7 @@
|
|
|
332
334
|
<button type="button" role="tab" data-character-editor-tab="profile" aria-selected="false">人物档案<small>简介、身份与动机</small></button>
|
|
333
335
|
<button type="button" role="tab" data-character-editor-tab="settings" aria-selected="false">扩展设定<small>属性与长篇章节</small></button>
|
|
334
336
|
<button type="button" role="tab" data-character-editor-tab="state" aria-selected="false">状态与约束<small>当前状态与锁定项</small></button>
|
|
337
|
+
<button type="button" role="tab" data-character-editor-tab="relationships" aria-selected="false">人物关系<small>关联人物与关系关键词</small></button>
|
|
335
338
|
</nav>
|
|
336
339
|
<main id="character-editor-fields" class="character-editor-fields"></main>
|
|
337
340
|
<aside id="character-history-panel" class="character-history-panel hidden" aria-label="人物版本历史">
|
|
@@ -542,6 +545,7 @@
|
|
|
542
545
|
</section>
|
|
543
546
|
</dialog>
|
|
544
547
|
|
|
545
|
-
<
|
|
548
|
+
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
549
|
+
<script type="module" src="/app.js?v=20260720-captcha-lazy-load"></script>
|
|
546
550
|
</body>
|
|
547
551
|
</html>
|
|
@@ -31,7 +31,9 @@ export function serializePageRoute(route = {}) {
|
|
|
31
31
|
const view = String(route.view ?? "shelf");
|
|
32
32
|
const workId = String(route.workId ?? "").trim();
|
|
33
33
|
|
|
34
|
-
if (view === "
|
|
34
|
+
if (view === "login") {
|
|
35
|
+
params.set("view", "login");
|
|
36
|
+
} else if (view === "editor" && workId) {
|
|
35
37
|
params.set("view", "editor");
|
|
36
38
|
params.set("work", workId);
|
|
37
39
|
if (route.chapterId) params.set("chapter", String(route.chapterId));
|
|
@@ -58,6 +60,8 @@ export function parsePageRoute(hash = "") {
|
|
|
58
60
|
const view = value(params, "view");
|
|
59
61
|
const workId = value(params, "work");
|
|
60
62
|
|
|
63
|
+
if (view === "login") return { view: "login" };
|
|
64
|
+
|
|
61
65
|
if (view === "editor" && workId) {
|
|
62
66
|
const chapterId = value(params, "chapter");
|
|
63
67
|
return { view, workId, chapterId: chapterId || null };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const relationshipKeywordSeparator = /[,,、;;\n]/u;
|
|
2
|
+
|
|
3
|
+
function normalizeRelationshipKeyword(value) {
|
|
4
|
+
return String(value ?? "").normalize("NFKC").trim().replace(/\s+/gu, " ");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function splitRelationshipKeywords(value) {
|
|
8
|
+
return String(value ?? "")
|
|
9
|
+
.split(relationshipKeywordSeparator)
|
|
10
|
+
.map(normalizeRelationshipKeyword)
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function uniqueRelationshipKeywords(values) {
|
|
15
|
+
const keywords = [];
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
for (const value of values) {
|
|
18
|
+
for (const keyword of splitRelationshipKeywords(value)) {
|
|
19
|
+
const key = keyword.toLocaleLowerCase("zh-CN");
|
|
20
|
+
if (seen.has(key)) continue;
|
|
21
|
+
seen.add(key);
|
|
22
|
+
keywords.push(keyword);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return keywords;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function splitRelationshipKeywordInput(value) {
|
|
29
|
+
const parts = String(value ?? "").split(relationshipKeywordSeparator);
|
|
30
|
+
return {
|
|
31
|
+
completed: uniqueRelationshipKeywords(parts.slice(0, -1)),
|
|
32
|
+
remainder: parts.at(-1) ?? ""
|
|
33
|
+
};
|
|
34
|
+
}
|
package/dist/public/styles.css
CHANGED
|
@@ -31,7 +31,26 @@
|
|
|
31
31
|
background: var(--paper);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
.auth-pending .app-shell {
|
|
34
|
+
.auth-pending .app-shell { pointer-events: none; user-select: none; }
|
|
35
|
+
.auth-loading { display: none; }
|
|
36
|
+
.auth-pending .auth-loading { position: fixed; inset: 0; z-index: 900; display: grid; place-items: center; pointer-events: none; }
|
|
37
|
+
.auth-loading::before { content: ""; width: 32px; height: 32px; border: 3px solid color-mix(in srgb, var(--accent) 25%, transparent); border-top-color: var(--accent); border-radius: 50%; animation: auth-loading-spin .8s linear infinite; }
|
|
38
|
+
@keyframes auth-loading-spin { to { transform: rotate(360deg); } }
|
|
39
|
+
@media (prefers-reduced-motion: reduce) { .auth-loading::before { animation-duration: 2s; } }
|
|
40
|
+
.login-route .auth-view.hidden { display: grid !important; }
|
|
41
|
+
.login-route .auth-loading { display: none; }
|
|
42
|
+
/* 会话恢复期间按 data-pending-view 预显示目标视图的骨架(自然 display 与各视图定义一致) */
|
|
43
|
+
[data-pending-view="shelf"] .auth-pending #shelf-view.hidden,
|
|
44
|
+
[data-pending-view="settings"] .auth-pending #settings-hub-view.hidden,
|
|
45
|
+
[data-pending-view="platform-ai"] .auth-pending #platform-ai-view.hidden,
|
|
46
|
+
[data-pending-view="module"] .auth-pending #module-view.hidden { display: block !important; }
|
|
47
|
+
[data-pending-view="welcome"] .auth-pending #welcome-view.hidden,
|
|
48
|
+
[data-pending-view="editor"] .auth-pending #editor-view.hidden { display: grid !important; }
|
|
49
|
+
/* 书架/设置/平台 AI 路由的骨架使用整宽布局,与 shelf-mode 一致 */
|
|
50
|
+
.pending-shelf-mode .auth-pending .app-shell { grid-template-columns: 1fr; }
|
|
51
|
+
.pending-shelf-mode .auth-pending .topbar { grid-template-columns: 280px 1fr auto; }
|
|
52
|
+
.pending-shelf-mode .auth-pending .left-panel, .pending-shelf-mode .auth-pending .ai-panel { display: none; }
|
|
53
|
+
.pending-shelf-mode .auth-pending .main-panel { grid-column: 1 / -1; }
|
|
35
54
|
.auth-view { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; align-content: safe center; padding: 24px; overflow-y: auto; overscroll-behavior: contain; background: radial-gradient(circle at 18% 16%, color-mix(in srgb, var(--accent) 14%, transparent), transparent 34%), var(--paper); }
|
|
36
55
|
.auth-view.hidden { display: none; }
|
|
37
56
|
.auth-card { box-sizing: border-box; width: min(440px, 100%); padding: 34px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface); box-shadow: var(--shadow); }
|
|
@@ -163,6 +182,8 @@
|
|
|
163
182
|
cursor: pointer;
|
|
164
183
|
}
|
|
165
184
|
.auth-captcha-image img { display: block; width: 148px; height: 48px; border-radius: 6px; }
|
|
185
|
+
.auth-captcha-image img[hidden], .auth-captcha-placeholder[hidden] { display: none; }
|
|
186
|
+
.auth-captcha-placeholder { color: var(--muted); font-size: 13px; padding: 0 14px; white-space: nowrap; }
|
|
166
187
|
.auth-captcha-hint { margin: -8px 0 0; color: var(--muted); font-size: 12px; line-height: 1.4; }
|
|
167
188
|
.auth-error { min-height: 20px; margin: 14px 0 0 !important; color: var(--accent-dark) !important; }
|
|
168
189
|
|
|
@@ -471,6 +492,8 @@ body.work-viewer-mode #merge-events,
|
|
|
471
492
|
body.work-viewer-mode [data-edit-foreshadow],
|
|
472
493
|
body.work-viewer-mode [data-edit-outline],
|
|
473
494
|
body.work-viewer-mode [data-edit-relationship],
|
|
495
|
+
body.work-viewer-mode [data-character-relationship-edit],
|
|
496
|
+
body.work-viewer-mode [data-character-relationship-create],
|
|
474
497
|
body.work-viewer-mode [data-review-id],
|
|
475
498
|
body.work-viewer-mode .task-auto-run-panel,
|
|
476
499
|
body.work-viewer-mode [data-run-task],
|
|
@@ -1282,6 +1305,14 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1282
1305
|
.item-list-row button, .item-list-add { border: 1px solid var(--line); border-radius: 4px; background: var(--panel); color: var(--muted); font-size: 10px; }
|
|
1283
1306
|
.item-list-row button { padding: 0 10px; }
|
|
1284
1307
|
.item-list-add { justify-self: start; padding: 7px 11px; }
|
|
1308
|
+
.keyword-chip-field > small { color: var(--muted); font-size: 10px; line-height: 1.5; }
|
|
1309
|
+
.keyword-chip-editor { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-height: 42px; padding: 6px 8px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); }
|
|
1310
|
+
.keyword-chip-editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 14%, transparent); }
|
|
1311
|
+
.keyword-chip-editor > input[data-keyword-input] { flex: 1 1 160px; width: auto; min-width: 140px; padding: 6px 3px; border: 0; background: transparent; color: var(--ink); outline: none; }
|
|
1312
|
+
.keyword-chip-editor > input[data-keyword-input]:focus { border: 0; box-shadow: none; }
|
|
1313
|
+
.keyword-chip { display: inline-flex; align-items: center; gap: 5px; min-height: 28px; padding: 3px 5px 3px 10px; border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--line)); border-radius: 16px; background: var(--paper-deep); color: var(--ink); font-size: 11px; line-height: 1.3; }
|
|
1314
|
+
.keyword-chip button { display: inline-grid; place-items: center; width: 20px; height: 20px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: var(--muted); font-size: 15px; line-height: 1; }
|
|
1315
|
+
.keyword-chip button:hover, .keyword-chip button:focus-visible { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent-dark); outline: none; }
|
|
1285
1316
|
.structured-list-field > small { color: var(--muted); font-size: 9px; line-height: 1.5; }
|
|
1286
1317
|
.structured-list-rows { display: grid; gap: 9px; }
|
|
1287
1318
|
.structured-list-row { display: grid; gap: 7px; padding: 10px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface-soft); }
|
|
@@ -1338,6 +1369,22 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
|
|
|
1338
1369
|
.character-editor-section-fields .section-list-row textarea { min-height: 180px; }
|
|
1339
1370
|
.character-editor-section-fields .member-chip { position: relative; display: inline-flex; width: auto; cursor: pointer; }
|
|
1340
1371
|
.character-editor-section-fields .item-list-row button, .character-editor-section-fields .structured-list-row button { min-width: 54px; }
|
|
1372
|
+
.character-editor-relationships-field { display: grid; grid-column: 1 / -1; gap: 12px; }
|
|
1373
|
+
.character-relationship-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
1374
|
+
.character-relationship-toolbar p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
|
|
1375
|
+
.character-relationship-toolbar .ghost-button { flex: 0 0 auto; min-height: 32px; }
|
|
1376
|
+
.character-relationship-row { display: grid; gap: 10px; padding: 12px; border: 1px solid var(--line); border-radius: 5px; background: var(--surface-soft); }
|
|
1377
|
+
.character-relationship-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
|
1378
|
+
.character-relationship-heading > div { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; min-width: 0; }
|
|
1379
|
+
.character-relationship-heading strong { font-size: 14px; }
|
|
1380
|
+
.character-relationship-heading span { color: var(--muted); font-size: 10px; }
|
|
1381
|
+
.character-relationship-heading button { flex: 0 0 auto; padding: 6px 9px; border: 1px solid var(--line); border-radius: 4px; background: transparent; color: var(--muted); font-size: 10px; }
|
|
1382
|
+
.character-relationship-heading button:hover { border-color: var(--accent); color: var(--accent-dark); }
|
|
1383
|
+
.character-relationship-keywords { display: grid; gap: 6px; }
|
|
1384
|
+
.character-relationship-keywords > small { color: var(--muted); font-size: 9px; }
|
|
1385
|
+
.character-relationship-keywords > div { display: flex; flex-wrap: wrap; gap: 5px; }
|
|
1386
|
+
.character-relationship-empty-keywords, .character-relationship-status { color: var(--muted); font-size: 10px; }
|
|
1387
|
+
.character-relationship-status { margin: 0; padding: 18px 12px; border: 1px dashed var(--line); border-radius: 4px; text-align: center; }
|
|
1341
1388
|
.character-editor-empty-field { display: grid; align-content: start; gap: 7px; color: var(--muted); font-size: 11px; }
|
|
1342
1389
|
.character-editor-empty-field span { padding: 11px; border: 1px dashed var(--line); border-radius: 4px; font-size: 10px; }
|
|
1343
1390
|
.character-editor-field-help { margin: -8px 0 0; color: var(--muted); font-size: 9px; }
|
|
@@ -5,4 +5,19 @@
|
|
|
5
5
|
const theme = storedTheme === "dark" || storedTheme === "light" ? storedTheme : prefersDark ? "dark" : "light";
|
|
6
6
|
document.documentElement.dataset.theme = theme;
|
|
7
7
|
document.documentElement.style.colorScheme = theme;
|
|
8
|
+
// 登录页路由首帧直接显示登录卡片,避免出现"骨架屏 → 登录页"的跳变
|
|
9
|
+
const routeParams = new URLSearchParams(window.location.hash.replace(/^#/, ""));
|
|
10
|
+
const routeView = routeParams.get("view") ?? "";
|
|
11
|
+
if (routeView === "login") {
|
|
12
|
+
document.documentElement.classList.add("login-route");
|
|
13
|
+
} else {
|
|
14
|
+
// 会话恢复期间按目标路由预显示对应视图的骨架屏
|
|
15
|
+
const pendingView = ["editor", "module", "welcome"].includes(routeView) && routeParams.get("work")
|
|
16
|
+
? routeView
|
|
17
|
+
: ["settings", "platform-ai"].includes(routeView) ? routeView : "shelf";
|
|
18
|
+
document.documentElement.dataset.pendingView = pendingView;
|
|
19
|
+
if (["shelf", "settings", "platform-ai"].includes(pendingView)) {
|
|
20
|
+
document.documentElement.classList.add("pending-shelf-mode");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
8
23
|
})();
|
package/dist/security.js
CHANGED
|
@@ -241,7 +241,7 @@ export function resolveRuntimeSecurity(environment, requireAuthentication = fals
|
|
|
241
241
|
trustProxy,
|
|
242
242
|
enforceSameOrigin: true,
|
|
243
243
|
allowPrivateAiEndpoints: environment.APP_ALLOW_PRIVATE_AI_ENDPOINTS === "true" || !production,
|
|
244
|
-
allowRegistration: environment.APP_ALLOW_REGISTRATION
|
|
244
|
+
allowRegistration: environment.APP_ALLOW_REGISTRATION === "true"
|
|
245
245
|
};
|
|
246
246
|
}
|
|
247
247
|
//# sourceMappingURL=security.js.map
|