@musnows/scriverse 0.6.11 → 0.7.0
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.md +2 -0
- package/dist/ai.js +43 -10
- package/dist/ai.js.map +1 -1
- package/dist/app.js +16 -0
- package/dist/app.js.map +1 -1
- package/dist/database.js +48 -3
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +298 -15
- package/dist/public/display-labels.js +1 -1
- package/dist/public/entity-version.js +2 -1
- package/dist/public/index.html +29 -3
- package/dist/public/styles.css +56 -12
- package/dist/server-runtime.js +118 -4
- package/dist/server-runtime.js.map +1 -1
- package/dist/store.js +170 -16
- package/dist/store.js.map +1 -1
- package/dist/user-auth.js +8 -0
- package/dist/user-auth.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -17,7 +17,7 @@ import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-ma
|
|
|
17
17
|
import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
|
|
18
18
|
import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
|
|
19
19
|
import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260801-entity-lifecycle-v1";
|
|
20
|
-
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=
|
|
20
|
+
import { VERSIONED_ENTITY_LABELS, entityVersionSnapshotSummary, entityVersionSourceLabel } from "/entity-version.js?v=20260809-global-replace-v1";
|
|
21
21
|
import {
|
|
22
22
|
chapterVersionSourceLabel,
|
|
23
23
|
foreshadowStatusLabel,
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
taskScopeLabel,
|
|
37
37
|
timelineStatusLabel,
|
|
38
38
|
characterStateFieldLabel
|
|
39
|
-
} from "/display-labels.js?v=
|
|
39
|
+
} from "/display-labels.js?v=20260809-global-replace-v1";
|
|
40
40
|
import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260731-work-comments-v2";
|
|
41
41
|
import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
|
|
42
42
|
import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
|
|
@@ -331,6 +331,16 @@ function canReadAggregateContent(work = state.work) {
|
|
|
331
331
|
.every((module) => canReadModule(module, work));
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
function canGlobalReplaceScope(scope, work = state.work) {
|
|
335
|
+
if (scope === "settings") return canEditModule("settings", work);
|
|
336
|
+
if (scope === "prose-and-settings") return canEditProse(work) && canEditModule("settings", work);
|
|
337
|
+
return canEditProse(work);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function canGlobalReplaceAny(work = state.work) {
|
|
341
|
+
return Boolean(work) && (canGlobalReplaceScope("prose", work) || canGlobalReplaceScope("settings", work));
|
|
342
|
+
}
|
|
343
|
+
|
|
334
344
|
function applyWorkAccessMode() {
|
|
335
345
|
const viewOnly = Boolean(state.work) && !canEditWork();
|
|
336
346
|
const proseReadOnly = Boolean(state.work) && !canEditProse();
|
|
@@ -1648,7 +1658,7 @@ function resolveAiProcessDuration(metadata, steps, completedAt) {
|
|
|
1648
1658
|
return Math.max(0, completedTime - Math.min(...startedTimes));
|
|
1649
1659
|
}
|
|
1650
1660
|
|
|
1651
|
-
function renderAiProcessSteps(message, steps, completed, durationMs = null) {
|
|
1661
|
+
function renderAiProcessSteps(message, steps, completed, durationMs = null, visibleContents = null) {
|
|
1652
1662
|
message.querySelector(".ai-process-details")?.remove();
|
|
1653
1663
|
if (!Array.isArray(steps) || !steps.length) return;
|
|
1654
1664
|
const details = document.createElement("details");
|
|
@@ -1688,7 +1698,8 @@ function renderAiProcessSteps(message, steps, completed, durationMs = null) {
|
|
|
1688
1698
|
label.textContent = `第 ${Number(step.round) || 1} 轮 · ${step.type === "thinking" ? "Thinking" : "中间输出"}`;
|
|
1689
1699
|
const body = document.createElement("div");
|
|
1690
1700
|
body.className = "message-body ai-process-step-body";
|
|
1691
|
-
|
|
1701
|
+
const content = visibleContents?.has(step) ? visibleContents.get(step) : step.content;
|
|
1702
|
+
body.innerHTML = renderMarkdown(content);
|
|
1692
1703
|
section.append(label, body);
|
|
1693
1704
|
list.append(section);
|
|
1694
1705
|
}
|
|
@@ -1942,7 +1953,7 @@ function applyAiRoleplayCharacter(character) {
|
|
|
1942
1953
|
if (active) $("#ai-scope").value = "none";
|
|
1943
1954
|
$(".ai-panel").classList.toggle("is-roleplaying", active);
|
|
1944
1955
|
$("#ai-prompt").dataset.placeholder = active
|
|
1945
|
-
?
|
|
1956
|
+
? `与 ${String(state.aiRoleplayCharacter.name)} 角色开始对话……`
|
|
1946
1957
|
: "告诉 AI 你想讨论或修改什么……";
|
|
1947
1958
|
renderAiRoleplayCharacterSelect();
|
|
1948
1959
|
syncAiTaskOptions();
|
|
@@ -2673,6 +2684,10 @@ function invalidateModuleRequestsAfterMutation(path, method) {
|
|
|
2673
2684
|
if (path.includes("/relationships")) affected.add("relationships");
|
|
2674
2685
|
if (path.includes("/chapter-annotations/") || /\/chapters\/[^/]+\/annotations(?:$|\?)/u.test(path)) affected.add("comments");
|
|
2675
2686
|
if (path.includes("/reviews")) affected.add("reviews");
|
|
2687
|
+
if (/\/api\/works\/[^/]+\/replace(?:$|\?)/u.test(path)) {
|
|
2688
|
+
affected.add("settings");
|
|
2689
|
+
if (state.work?.id) moduleRequestCache.invalidate(state.work.id, "settings");
|
|
2690
|
+
}
|
|
2676
2691
|
if (path.includes("/entity-versions/")) {
|
|
2677
2692
|
if (path.includes("/draft/")) affected.add("drafts");
|
|
2678
2693
|
if (path.includes("/setting/")) affected.add("settings");
|
|
@@ -3510,6 +3525,7 @@ function renderSettingsHub() {
|
|
|
3510
3525
|
$("#collaboration-button").disabled = !canManageWork;
|
|
3511
3526
|
$("#writing-progress-button").disabled = !hasWork || !canReadModule("editor");
|
|
3512
3527
|
$("#work-audit-button").disabled = !canManageWork;
|
|
3528
|
+
$("#global-replace-button").classList.toggle("hidden", !canGlobalReplaceAny());
|
|
3513
3529
|
$("#top-search-button").disabled = !canReadAggregate;
|
|
3514
3530
|
$("#export-button").disabled = !canExportManuscript;
|
|
3515
3531
|
$("#export-button").setAttribute("aria-expanded", "false");
|
|
@@ -4246,11 +4262,156 @@ async function openSearchDialog() {
|
|
|
4246
4262
|
$("#search-dialog .eyebrow").textContent = `当前作品 · 《${state.work.title}》`;
|
|
4247
4263
|
$("#search-query").value = "";
|
|
4248
4264
|
$("#search-type").value = "";
|
|
4265
|
+
$("#search-to-replace").disabled = !canGlobalReplaceAny();
|
|
4249
4266
|
$("#search-results").innerHTML = '<p class="search-results-empty">输入关键词后开始检索。</p>';
|
|
4250
4267
|
$("#search-dialog").showModal();
|
|
4251
4268
|
queueMicrotask(() => $("#search-query").focus());
|
|
4252
4269
|
}
|
|
4253
4270
|
|
|
4271
|
+
const globalReplaceScopeLabels = Object.freeze({
|
|
4272
|
+
prose: "正文",
|
|
4273
|
+
settings: "设定库",
|
|
4274
|
+
"prose-and-settings": "正文+设定库"
|
|
4275
|
+
});
|
|
4276
|
+
|
|
4277
|
+
function syncGlobalReplaceScopeOptions() {
|
|
4278
|
+
const dialog = $("#replace-dialog");
|
|
4279
|
+
const options = [...dialog.querySelectorAll('input[name="replaceScope"]')];
|
|
4280
|
+
for (const option of options) {
|
|
4281
|
+
const allowed = canGlobalReplaceScope(option.value);
|
|
4282
|
+
option.disabled = !allowed;
|
|
4283
|
+
option.closest(".replace-scope-option")?.classList.toggle("is-disabled", !allowed);
|
|
4284
|
+
}
|
|
4285
|
+
const selected = options.find((option) => option.checked && !option.disabled) ?? options.find((option) => !option.disabled);
|
|
4286
|
+
options.forEach((option) => { option.checked = option === selected; });
|
|
4287
|
+
const scope = selected?.value ?? "";
|
|
4288
|
+
$("#replace-submit").disabled = !scope || !canGlobalReplaceScope(scope);
|
|
4289
|
+
$("#replace-permission-note").textContent = scope
|
|
4290
|
+
? "替换完成后,命中的章节和设定会分别生成新的版本历史。"
|
|
4291
|
+
: "当前账户没有可写入的正文或设定库权限。";
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
function openGlobalReplaceDialog() {
|
|
4295
|
+
if (!state.work) {
|
|
4296
|
+
toast("请先打开一部作品", "error");
|
|
4297
|
+
return;
|
|
4298
|
+
}
|
|
4299
|
+
if (!canGlobalReplaceAny()) {
|
|
4300
|
+
toast("当前账户没有正文或设定库的编辑权限", "error");
|
|
4301
|
+
return;
|
|
4302
|
+
}
|
|
4303
|
+
if ($("#search-dialog").open) $("#search-dialog").close();
|
|
4304
|
+
$("#replace-form").reset();
|
|
4305
|
+
syncGlobalReplaceScopeOptions();
|
|
4306
|
+
$("#replace-dialog").showModal();
|
|
4307
|
+
queueMicrotask(() => $("#replace-find").focus());
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4310
|
+
async function refreshWorkAfterGlobalReplace(route, result) {
|
|
4311
|
+
const workId = state.work?.id;
|
|
4312
|
+
if (!workId) return;
|
|
4313
|
+
const nextWork = result?.work ?? await api(`/api/works/${encodeURIComponent(workId)}?directory=volumes`);
|
|
4314
|
+
if (!nextWork || nextWork.id !== workId) return;
|
|
4315
|
+
state.work = nextWork;
|
|
4316
|
+
state.work.volumes = state.work.volumes.map((volume) => ({ ...volume, chapters: Array.isArray(volume.chapters) ? volume.chapters : [] }));
|
|
4317
|
+
state.works = state.works.map((work) => work.id === workId ? { ...work, ...nextWork } : work);
|
|
4318
|
+
state.settings = [];
|
|
4319
|
+
loadedVolumeChapterIds.clear();
|
|
4320
|
+
volumeChapterLoadingIds.clear();
|
|
4321
|
+
volumeChapterRequests.clear();
|
|
4322
|
+
for (const volume of state.work.volumes) loadedVolumeChapterIds.add(volume.id);
|
|
4323
|
+
state.collapsedVolumeIds = new Set(state.work.volumes.map((volume) => volume.id));
|
|
4324
|
+
if (String(result?.scope) === "prose" || String(result?.scope) === "prose-and-settings") {
|
|
4325
|
+
state.chapter = null;
|
|
4326
|
+
lastSavedChapterSnapshot = null;
|
|
4327
|
+
}
|
|
4328
|
+
applyWorkAccessMode();
|
|
4329
|
+
showSystemStatus();
|
|
4330
|
+
updateDocumentTitle(state.work);
|
|
4331
|
+
$("#work-meta").textContent = `${state.work.title}${state.work.author ? ` · ${state.work.author}` : ""} · ${Number(state.work.wordCount ?? 0).toLocaleString("zh-CN")} 字`;
|
|
4332
|
+
$("#top-search-button").disabled = !canReadAggregateContent();
|
|
4333
|
+
renderTree();
|
|
4334
|
+
if (route.view === "editor" && route.chapterId && canReadModule("editor")) {
|
|
4335
|
+
await selectChapter(route.chapterId);
|
|
4336
|
+
} else if (route.view === "module") {
|
|
4337
|
+
await showModule(route.module);
|
|
4338
|
+
} else if (route.view === "settings") {
|
|
4339
|
+
renderSettingsHub();
|
|
4340
|
+
replacePageRoute({ view: "settings", workId: state.work.id, ...settingsRouteContext() });
|
|
4341
|
+
} else if (route.view === "welcome") {
|
|
4342
|
+
showWelcome(true);
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
|
|
4346
|
+
async function submitGlobalReplace(event) {
|
|
4347
|
+
event.preventDefault();
|
|
4348
|
+
if (!state.work) return;
|
|
4349
|
+
const find = $("#replace-find").value;
|
|
4350
|
+
const replacement = $("#replace-with").value;
|
|
4351
|
+
const scope = $("#replace-form").querySelector('input[name="replaceScope"]:checked')?.value ?? "prose";
|
|
4352
|
+
if (!find.trim()) {
|
|
4353
|
+
toast("请输入要查找的内容", "error");
|
|
4354
|
+
$("#replace-find").focus();
|
|
4355
|
+
return;
|
|
4356
|
+
}
|
|
4357
|
+
if (!canGlobalReplaceScope(scope)) {
|
|
4358
|
+
toast("当前账户没有所选范围的编辑权限", "error");
|
|
4359
|
+
syncGlobalReplaceScopeOptions();
|
|
4360
|
+
return;
|
|
4361
|
+
}
|
|
4362
|
+
const dialog = $("#replace-dialog");
|
|
4363
|
+
const reopenDialog = () => {
|
|
4364
|
+
dialog.showModal();
|
|
4365
|
+
syncGlobalReplaceScopeOptions();
|
|
4366
|
+
queueMicrotask(() => $("#replace-find").focus());
|
|
4367
|
+
};
|
|
4368
|
+
dialog.close();
|
|
4369
|
+
if (state.dirty && !(await confirmDiscardChanges("当前章节有未保存修改,执行全局替换会放弃这些修改。是否继续?"))) {
|
|
4370
|
+
reopenDialog();
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
const scopeLabel = globalReplaceScopeLabels[scope] ?? "正文";
|
|
4374
|
+
const replacementLabel = replacement ? `“${replacement}”` : "空内容";
|
|
4375
|
+
const confirmed = await confirmToast(`将把《${state.work.title}》的${scopeLabel}中所有“${find}”替换为${replacementLabel}。每个命中对象都会生成新版本,确认继续吗?`, {
|
|
4376
|
+
title: "确认全局替换",
|
|
4377
|
+
confirmLabel: "确认替换"
|
|
4378
|
+
});
|
|
4379
|
+
if (!confirmed) {
|
|
4380
|
+
reopenDialog();
|
|
4381
|
+
return;
|
|
4382
|
+
}
|
|
4383
|
+
const button = $("#replace-submit");
|
|
4384
|
+
const route = currentPageRoute();
|
|
4385
|
+
const workId = state.work.id;
|
|
4386
|
+
button.disabled = true;
|
|
4387
|
+
button.textContent = "替换中…";
|
|
4388
|
+
cancelChapterAutoSave();
|
|
4389
|
+
state.dirty = false;
|
|
4390
|
+
try {
|
|
4391
|
+
const result = await api(`/api/works/${encodeURIComponent(workId)}/replace`, {
|
|
4392
|
+
method: "POST",
|
|
4393
|
+
body: { find, replacement, scope },
|
|
4394
|
+
skipOptimisticVersion: true
|
|
4395
|
+
});
|
|
4396
|
+
$("#replace-dialog").close();
|
|
4397
|
+
if (Number(result.totalMatches) > 0) await refreshWorkAfterGlobalReplace(route, result);
|
|
4398
|
+
if (Number(result.totalMatches) > 0) {
|
|
4399
|
+
const changedTargets = [];
|
|
4400
|
+
if (Number(result.chapterCount) > 0) changedTargets.push(`${result.chapterCount} 章`);
|
|
4401
|
+
if (Number(result.settingCount) > 0) changedTargets.push(`${result.settingCount} 条设定`);
|
|
4402
|
+
toast(`全局替换完成:${result.totalMatches} 处,已更新 ${changedTargets.join("、")}`);
|
|
4403
|
+
} else {
|
|
4404
|
+
toast("没有找到需要替换的内容");
|
|
4405
|
+
}
|
|
4406
|
+
} catch (error) {
|
|
4407
|
+
toast(error.message, "error");
|
|
4408
|
+
} finally {
|
|
4409
|
+
button.disabled = false;
|
|
4410
|
+
button.textContent = "开始替换";
|
|
4411
|
+
syncGlobalReplaceScopeOptions();
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
|
|
4254
4415
|
function highlightedSearchText(value, query) {
|
|
4255
4416
|
return splitGlobalSearchHighlight(value, query)
|
|
4256
4417
|
.map((segment) => segment.match ? `<mark>${esc(segment.text)}</mark>` : esc(segment.text))
|
|
@@ -6443,7 +6604,7 @@ async function renderTasks(page = taskListPage, { refresh = false } = {}) {
|
|
|
6443
6604
|
<small>开启后会持续执行直到队列清空;临时错误自动退避重试,连续失败达到阈值后暂停。</small>
|
|
6444
6605
|
</div>
|
|
6445
6606
|
<div class="task-auto-run-actions">
|
|
6446
|
-
${autoRunEditing ? "" :
|
|
6607
|
+
${autoRunEditing ? "" : `<button id="task-auto-run-edit" class="record-card-edit" type="button" aria-label="编辑自动执行设置" title="编辑自动执行设置">${pencilIconMarkup()}</button>`}
|
|
6447
6608
|
</div>
|
|
6448
6609
|
</div>
|
|
6449
6610
|
<div class="task-auto-run-controls">
|
|
@@ -6476,7 +6637,7 @@ async function renderTasks(page = taskListPage, { refresh = false } = {}) {
|
|
|
6476
6637
|
<tr>
|
|
6477
6638
|
<td>${esc(analysisTaskTypeLabel(item.taskType))}</td>
|
|
6478
6639
|
<td>${esc(item.model?.displayName || "运行时使用默认模型")}</td>
|
|
6479
|
-
<td>${
|
|
6640
|
+
<td>${renderTaskScopeSummary(item)}</td>
|
|
6480
6641
|
<td class="task-status-cell">${renderAnalysisTaskStatus(item)}</td>
|
|
6481
6642
|
<td class="task-progress-cell">${renderAnalysisTaskProgress(item)}</td>
|
|
6482
6643
|
<td class="task-row-actions">
|
|
@@ -6491,6 +6652,7 @@ async function renderTasks(page = taskListPage, { refresh = false } = {}) {
|
|
|
6491
6652
|
$("#module-content").querySelectorAll("[data-task-page]").forEach((control) => { control.disabled = true; });
|
|
6492
6653
|
await renderTasks(Number(button.dataset.taskPage));
|
|
6493
6654
|
}));
|
|
6655
|
+
bindTaskScopeTargets($("#module-content"));
|
|
6494
6656
|
|
|
6495
6657
|
$("#task-auto-run-edit")?.addEventListener("click", () => {
|
|
6496
6658
|
taskAutoRunEditing = true;
|
|
@@ -6596,6 +6758,47 @@ async function renderTasks(page = taskListPage, { refresh = false } = {}) {
|
|
|
6596
6758
|
scheduleTaskProgressRefresh(state.work.id, runningCount > 0 || (pendingCount > 0 && autoRunActive) ? 1 : 0);
|
|
6597
6759
|
}
|
|
6598
6760
|
|
|
6761
|
+
function renderTaskScopeSummary(task) {
|
|
6762
|
+
const summary = String(task.scopeSummary || taskScopeLabel(task.scope?.type || "book"));
|
|
6763
|
+
const target = task.scopeTarget && typeof task.scopeTarget === "object" ? task.scopeTarget : null;
|
|
6764
|
+
if (!target || !target.id || !["character", "chapter"].includes(target.type)) return esc(summary);
|
|
6765
|
+
const targetLabel = String(target.label || "").trim();
|
|
6766
|
+
if (!targetLabel) return esc(summary);
|
|
6767
|
+
const targetButton = `<button class="task-scope-link" type="button" data-task-scope-target data-task-scope-type="${esc(target.type)}" data-task-scope-id="${esc(target.id)}" aria-label="打开${target.type === "character" ? "人物档案" : "章节"}:${esc(targetLabel)}">${esc(targetLabel)}</button>`;
|
|
6768
|
+
if (target.type === "chapter") return targetButton;
|
|
6769
|
+
const targetIndex = summary.lastIndexOf(targetLabel);
|
|
6770
|
+
if (targetIndex < 0) return esc(summary);
|
|
6771
|
+
return `${esc(summary.slice(0, targetIndex))}${targetButton}${esc(summary.slice(targetIndex + targetLabel.length))}`;
|
|
6772
|
+
}
|
|
6773
|
+
|
|
6774
|
+
function bindTaskScopeTargets(container) {
|
|
6775
|
+
container.querySelectorAll("[data-task-scope-target]").forEach((button) => button.addEventListener("click", async () => {
|
|
6776
|
+
if (button.disabled) return;
|
|
6777
|
+
const targetType = button.dataset.taskScopeType;
|
|
6778
|
+
const targetId = button.dataset.taskScopeId;
|
|
6779
|
+
if (!targetId || !["character", "chapter"].includes(targetType)) return;
|
|
6780
|
+
button.disabled = true;
|
|
6781
|
+
try {
|
|
6782
|
+
if (targetType === "chapter") {
|
|
6783
|
+
await selectChapter(targetId);
|
|
6784
|
+
if (isMobileViewport()) {
|
|
6785
|
+
panelLayout.leftCollapsed = true;
|
|
6786
|
+
applyPanelLayout(true);
|
|
6787
|
+
}
|
|
6788
|
+
} else {
|
|
6789
|
+
await showModule("characters");
|
|
6790
|
+
if (state.module !== "characters") return;
|
|
6791
|
+
const character = await api(`/api/characters/${encodeURIComponent(targetId)}`);
|
|
6792
|
+
await openCharacterEditor(character, { readOnly: true });
|
|
6793
|
+
}
|
|
6794
|
+
} catch (error) {
|
|
6795
|
+
toast(`打开分析目标失败:${error.message}`, "error");
|
|
6796
|
+
} finally {
|
|
6797
|
+
button.disabled = false;
|
|
6798
|
+
}
|
|
6799
|
+
}));
|
|
6800
|
+
}
|
|
6801
|
+
|
|
6599
6802
|
async function rerunAnalysisTask(taskId, button, { closeDetail = false } = {}) {
|
|
6600
6803
|
const workId = state.work?.id;
|
|
6601
6804
|
button.disabled = true;
|
|
@@ -6851,11 +7054,56 @@ function renderTaskResultEvidence(item) {
|
|
|
6851
7054
|
const evidence = Array.isArray(item.evidence) ? item.evidence : [];
|
|
6852
7055
|
if (!evidence.length) return '<p class="task-result-muted">没有保存可展示的证据摘录。</p>';
|
|
6853
7056
|
return `<ul class="task-result-evidence">${evidence.map((item) => {
|
|
6854
|
-
const
|
|
6855
|
-
|
|
7057
|
+
const sourceType = item.sourceType === "chapter" || item.sourceType === "setting"
|
|
7058
|
+
? item.sourceType
|
|
7059
|
+
: item.chapterId || item.chapterTitle
|
|
7060
|
+
? "chapter"
|
|
7061
|
+
: item.settingId || item.settingTitle
|
|
7062
|
+
? "setting"
|
|
7063
|
+
: "";
|
|
7064
|
+
const sourceId = String(item.sourceId || (sourceType === "chapter" ? item.chapterId : sourceType === "setting" ? item.settingId : ""));
|
|
7065
|
+
const sourceTitle = String(item.sourceTitle || (sourceType === "chapter" ? item.chapterTitle : sourceType === "setting" ? item.settingTitle : ""));
|
|
7066
|
+
const sourceLabel = sourceType === "chapter"
|
|
7067
|
+
? `正文:${sourceTitle || sourceId || "未标明章节"}`
|
|
7068
|
+
: sourceType === "setting"
|
|
7069
|
+
? `设定集:${sourceTitle || sourceId || "未标明条目"}`
|
|
7070
|
+
: `引用:${sourceTitle || sourceId || "未标明来源"}`;
|
|
7071
|
+
const sourceMarkup = sourceId && (sourceType === "chapter" || sourceType === "setting")
|
|
7072
|
+
? `<button class="task-reference-link" type="button" data-task-result-reference data-task-reference-type="${esc(sourceType)}" data-task-reference-id="${esc(sourceId)}" aria-label="打开${esc(sourceLabel)}">${esc(sourceLabel)}</button>`
|
|
7073
|
+
: `<strong>${esc(sourceLabel)}</strong>`;
|
|
7074
|
+
return `<li>${sourceMarkup}${item.quote ? `<q>${esc(item.quote)}</q>` : ""}${item.supports ? `<small>${esc(item.supports)}</small>` : ""}</li>`;
|
|
6856
7075
|
}).join("")}</ul>`;
|
|
6857
7076
|
}
|
|
6858
7077
|
|
|
7078
|
+
function bindTaskResultReferenceActions(container) {
|
|
7079
|
+
container.querySelectorAll("[data-task-result-reference]").forEach((button) => button.addEventListener("click", async () => {
|
|
7080
|
+
if (button.disabled) return;
|
|
7081
|
+
const referenceType = button.dataset.taskReferenceType;
|
|
7082
|
+
const referenceId = button.dataset.taskReferenceId;
|
|
7083
|
+
if (!referenceId || !["chapter", "setting"].includes(referenceType)) return;
|
|
7084
|
+
button.disabled = true;
|
|
7085
|
+
try {
|
|
7086
|
+
$("#form-dialog").close();
|
|
7087
|
+
if (referenceType === "chapter") {
|
|
7088
|
+
await selectChapter(referenceId);
|
|
7089
|
+
if (isMobileViewport()) {
|
|
7090
|
+
panelLayout.leftCollapsed = true;
|
|
7091
|
+
applyPanelLayout(true);
|
|
7092
|
+
}
|
|
7093
|
+
} else {
|
|
7094
|
+
await showModule("settings");
|
|
7095
|
+
if (state.module !== "settings") return;
|
|
7096
|
+
const setting = await api(`/api/settings/${encodeURIComponent(referenceId)}`);
|
|
7097
|
+
openSettingEditor(setting, { readOnly: true });
|
|
7098
|
+
}
|
|
7099
|
+
} catch (error) {
|
|
7100
|
+
toast(`打开分析引用失败:${error.message}`, "error");
|
|
7101
|
+
} finally {
|
|
7102
|
+
button.disabled = false;
|
|
7103
|
+
}
|
|
7104
|
+
}));
|
|
7105
|
+
}
|
|
7106
|
+
|
|
6859
7107
|
function renderTaskResultItem(item) {
|
|
6860
7108
|
const tags = Array.isArray(item.tags) ? item.tags : [];
|
|
6861
7109
|
const details = Array.isArray(item.details) ? item.details : [];
|
|
@@ -7113,6 +7361,7 @@ function openTaskDetailDialog(task, trace) {
|
|
|
7113
7361
|
{ submitLabel: "关闭", wide: true, trace: true });
|
|
7114
7362
|
bindTaskTraceCallActions($("#dialog-fields"));
|
|
7115
7363
|
bindTaskResultActions($("#dialog-fields"));
|
|
7364
|
+
bindTaskResultReferenceActions($("#dialog-fields"));
|
|
7116
7365
|
$("#dialog-fields").querySelector("[data-rerun-task-detail]")?.addEventListener("click", async (event) => {
|
|
7117
7366
|
await rerunAnalysisTask(event.currentTarget.dataset.rerunTaskDetail, event.currentTarget, { closeDetail: true });
|
|
7118
7367
|
});
|
|
@@ -10739,6 +10988,29 @@ async function streamChat(body) {
|
|
|
10739
10988
|
let finalAnswerStarted = false;
|
|
10740
10989
|
const processStartedAt = Date.now();
|
|
10741
10990
|
const elapsedProcessTime = () => Math.max(0, Date.now() - processStartedAt);
|
|
10991
|
+
const processStepTypewriters = new Map();
|
|
10992
|
+
const processStepVisibleContents = new Map();
|
|
10993
|
+
const renderStreamingProcessSteps = (completed, durationMs = elapsedProcessTime()) => {
|
|
10994
|
+
renderAiProcessSteps(message, processSteps, completed, durationMs, processStepVisibleContents);
|
|
10995
|
+
};
|
|
10996
|
+
const processStepTypewriter = (step) => {
|
|
10997
|
+
const existing = processStepTypewriters.get(step);
|
|
10998
|
+
if (existing) return existing;
|
|
10999
|
+
processStepVisibleContents.set(step, "");
|
|
11000
|
+
const typewriter = createStreamTypewriter({
|
|
11001
|
+
onRender: (text) => {
|
|
11002
|
+
processStepVisibleContents.set(step, text);
|
|
11003
|
+
renderStreamingProcessSteps(finalAnswerStarted);
|
|
11004
|
+
scrollAiFeedToBottom();
|
|
11005
|
+
}
|
|
11006
|
+
});
|
|
11007
|
+
processStepTypewriters.set(step, typewriter);
|
|
11008
|
+
return typewriter;
|
|
11009
|
+
};
|
|
11010
|
+
const finishProcessStepTypewriters = () => Promise.all([...processStepTypewriters.values()].map((typewriter) => typewriter.finish()));
|
|
11011
|
+
const revealProcessStepTypewriters = () => {
|
|
11012
|
+
for (const typewriter of processStepTypewriters.values()) typewriter.reveal();
|
|
11013
|
+
};
|
|
10742
11014
|
try {
|
|
10743
11015
|
const response = await fetch(`/api/works/${state.work.id}/chat/stream`, {
|
|
10744
11016
|
method: "POST",
|
|
@@ -10798,7 +11070,7 @@ async function streamChat(body) {
|
|
|
10798
11070
|
streamedText += delta;
|
|
10799
11071
|
if (streamedText.length > 0) finalAnswerStarted = true;
|
|
10800
11072
|
typewriter.append(delta);
|
|
10801
|
-
if (firstFinalDelta && processSteps.length)
|
|
11073
|
+
if (firstFinalDelta && processSteps.length) renderStreamingProcessSteps(true, elapsedProcessTime());
|
|
10802
11074
|
meta.textContent = "正在生成回复……";
|
|
10803
11075
|
} else if (eventName === "process_step") {
|
|
10804
11076
|
mountAssistantMessage();
|
|
@@ -10808,7 +11080,11 @@ async function streamChat(body) {
|
|
|
10808
11080
|
const existing = append ? processSteps.find((item) => item.id === step.id && item.type === step.type) : null;
|
|
10809
11081
|
if (existing && typeof step.content === "string") existing.content += step.content;
|
|
10810
11082
|
else processSteps.push(step);
|
|
10811
|
-
|
|
11083
|
+
const targetStep = existing ?? step;
|
|
11084
|
+
if (typeof step.content === "string" && step.content.length > 0 && step.type === "thinking") {
|
|
11085
|
+
processStepTypewriter(targetStep).append(step.content);
|
|
11086
|
+
}
|
|
11087
|
+
renderStreamingProcessSteps(finalAnswerStarted, elapsedProcessTime());
|
|
10812
11088
|
meta.textContent = step.type === "thinking"
|
|
10813
11089
|
? `正在思考 · 第 ${Number(step.round) || 1} 轮`
|
|
10814
11090
|
: step.type === "context_compaction"
|
|
@@ -10823,7 +11099,7 @@ async function streamChat(body) {
|
|
|
10823
11099
|
if (toolCall.status === "failed") setAiAssistantStatus("error");
|
|
10824
11100
|
toolCalls.push(toolCall);
|
|
10825
11101
|
processSteps.push(aiToolProcessStep(toolCall, round));
|
|
10826
|
-
|
|
11102
|
+
renderStreamingProcessSteps(finalAnswerStarted, elapsedProcessTime());
|
|
10827
11103
|
meta.textContent = `已调用 ${toolCalls.length} 个工具,正在等待模型处理结果`;
|
|
10828
11104
|
scrollAiFeedToBottom();
|
|
10829
11105
|
} else if (eventName === "context_compacted") {
|
|
@@ -10835,7 +11111,7 @@ async function streamChat(body) {
|
|
|
10835
11111
|
persistedMessageCreatedAt = typeof payload.messageCreatedAt === "string" ? payload.messageCreatedAt : null;
|
|
10836
11112
|
conversationTitle = typeof payload.conversationTitle === "string" ? payload.conversationTitle : null;
|
|
10837
11113
|
setAiContextMeter(payload.contextUsage);
|
|
10838
|
-
await typewriter.finish();
|
|
11114
|
+
await Promise.all([typewriter.finish(), finishProcessStepTypewriters()]);
|
|
10839
11115
|
message.classList.remove("is-streaming");
|
|
10840
11116
|
content.setAttribute("aria-busy", "false");
|
|
10841
11117
|
message.querySelector(".message-heading > span").textContent = "助手";
|
|
@@ -10861,16 +11137,17 @@ async function streamChat(body) {
|
|
|
10861
11137
|
if (chunk.done) break;
|
|
10862
11138
|
}
|
|
10863
11139
|
if (buffer.trim()) await consume(buffer);
|
|
10864
|
-
await typewriter.finish();
|
|
11140
|
+
await Promise.all([typewriter.finish(), finishProcessStepTypewriters()]);
|
|
10865
11141
|
if (streamError) throw streamError;
|
|
10866
11142
|
return { action: contextAction, content: streamedText, message, metadata: generatedMetadata, messageId: persistedMessageId, createdAt: persistedMessageCreatedAt, conversationTitle, userMessage: persistedUserMessage };
|
|
10867
11143
|
} catch (error) {
|
|
10868
11144
|
mountAssistantMessage();
|
|
10869
11145
|
typewriter.reveal();
|
|
11146
|
+
revealProcessStepTypewriters();
|
|
10870
11147
|
message.classList.remove("is-streaming");
|
|
10871
11148
|
content.setAttribute("aria-busy", "false");
|
|
10872
11149
|
message.querySelector(".message-heading > span").textContent = aiAssistantLabel("生成中断");
|
|
10873
|
-
|
|
11150
|
+
renderStreamingProcessSteps(true, elapsedProcessTime());
|
|
10874
11151
|
meta.textContent = "生成中断";
|
|
10875
11152
|
scrollAiFeedToBottom();
|
|
10876
11153
|
throw error;
|
|
@@ -11277,6 +11554,7 @@ $("#home-button").addEventListener("click", async () => {
|
|
|
11277
11554
|
$("#settings-button").addEventListener("click", () => {
|
|
11278
11555
|
void showSettingsHub();
|
|
11279
11556
|
});
|
|
11557
|
+
$("#global-replace-button").addEventListener("click", openGlobalReplaceDialog);
|
|
11280
11558
|
$("#account-button").addEventListener("click", () => {
|
|
11281
11559
|
const expanded = $("#account-menu").classList.toggle("hidden") === false;
|
|
11282
11560
|
$("#account-button").setAttribute("aria-expanded", String(expanded));
|
|
@@ -12532,12 +12810,17 @@ $("#background-task-open-analysis").addEventListener("click", () => {
|
|
|
12532
12810
|
showModule("tasks").catch((error) => toast(error.message, "error"));
|
|
12533
12811
|
});
|
|
12534
12812
|
$("#search-dialog-close").addEventListener("click", () => $("#search-dialog").close());
|
|
12813
|
+
$("#search-to-replace").addEventListener("click", openGlobalReplaceDialog);
|
|
12535
12814
|
$("#search-form").addEventListener("submit", async (event) => {
|
|
12536
12815
|
event.preventDefault();
|
|
12537
12816
|
await runWorkSearch().catch((error) => {
|
|
12538
12817
|
$("#search-results").innerHTML = `<p class="search-results-status">${esc(error.message)}</p>`;
|
|
12539
12818
|
});
|
|
12540
12819
|
});
|
|
12820
|
+
$("#replace-dialog-close").addEventListener("click", () => $("#replace-dialog").close());
|
|
12821
|
+
$("#replace-cancel").addEventListener("click", () => $("#replace-dialog").close());
|
|
12822
|
+
$("#replace-form").querySelectorAll('input[name="replaceScope"]').forEach((input) => input.addEventListener("change", syncGlobalReplaceScopeOptions));
|
|
12823
|
+
$("#replace-form").addEventListener("submit", submitGlobalReplace);
|
|
12541
12824
|
$("#export-button").addEventListener("click", (event) => {
|
|
12542
12825
|
event.preventDefault();
|
|
12543
12826
|
event.stopPropagation();
|
|
@@ -74,7 +74,7 @@ export function providerProtocolLabel(value) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function chapterVersionSourceLabel(value) {
|
|
77
|
-
return enumLabel({ manual: "人工保存", auto: "自动保存", "ai-suggestion": "AI 建议", restore: "历史恢复", import: "文件导入", create: "初始版本" }, value, "其他来源");
|
|
77
|
+
return enumLabel({ manual: "人工保存", auto: "自动保存", "ai-suggestion": "AI 建议", restore: "历史恢复", import: "文件导入", create: "初始版本", "global-replace": "全局替换" }, value, "其他来源");
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function occurrenceRoleLabel(value) {
|
package/dist/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest">
|
|
12
12
|
<link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
|
|
13
|
-
<link rel="stylesheet" href="/styles.css?v=
|
|
13
|
+
<link rel="stylesheet" href="/styles.css?v=20260809-global-replace-v6">
|
|
14
14
|
</head>
|
|
15
15
|
<body class="auth-pending">
|
|
16
16
|
<section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
|
|
@@ -214,6 +214,7 @@
|
|
|
214
214
|
<button id="collaboration-button" class="settings-hub-card" type="button"><span class="settings-card-mark">协</span><strong>作品协作</strong><small>邀请注册用户共同编辑当前作品</small></button>
|
|
215
215
|
<button id="writing-progress-button" class="settings-hub-card" type="button"><span class="settings-card-mark">字</span><strong>写作目标</strong><small>每日目标、总字数目标与近 30 天趋势</small></button>
|
|
216
216
|
<button id="work-audit-button" class="settings-hub-card" type="button"><span class="settings-card-mark">录</span><strong>操作记录</strong><small>查看作品修改、操作者、对象与时间</small></button>
|
|
217
|
+
<button id="global-replace-button" class="settings-hub-card" type="button"><span class="settings-card-mark">替</span><strong>全局替换</strong><small>替换已保存正文、设定库或两者内容</small></button>
|
|
217
218
|
<button id="appearance-button" class="settings-hub-card" type="button" data-settings-action="appearance"><span class="settings-card-mark">Aa</span><strong>显示设置</strong><small>中文字体、等宽英文字体、字号与行距</small></button>
|
|
218
219
|
<button id="export-button" class="settings-hub-card" type="button" data-settings-action="export" aria-haspopup="menu" aria-controls="manuscript-export-menu" aria-expanded="false"><span class="settings-card-mark">出</span><strong>导出正文</strong><small>选择导出 Markdown ZIP 或 DOCX;不包含角色和设定资料</small></button>
|
|
219
220
|
</div>
|
|
@@ -610,7 +611,7 @@
|
|
|
610
611
|
<dialog id="search-dialog" class="dialog wide-dialog" aria-labelledby="search-dialog-title">
|
|
611
612
|
<div class="dialog-header">
|
|
612
613
|
<div><span class="eyebrow">当前作品</span><h2 id="search-dialog-title">全文检索</h2></div>
|
|
613
|
-
<button id="search-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button>
|
|
614
|
+
<div class="settings-dialog-header-actions"><button id="search-to-replace" class="ghost-button" type="button">全局替换</button><button id="search-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div>
|
|
614
615
|
</div>
|
|
615
616
|
<div class="access-dialog-body">
|
|
616
617
|
<form id="search-form" class="search-form">
|
|
@@ -638,6 +639,31 @@
|
|
|
638
639
|
</div>
|
|
639
640
|
</dialog>
|
|
640
641
|
|
|
642
|
+
<dialog id="replace-dialog" class="dialog replace-dialog" aria-labelledby="replace-dialog-title" aria-describedby="replace-dialog-description">
|
|
643
|
+
<form id="replace-form">
|
|
644
|
+
<div class="dialog-header">
|
|
645
|
+
<div><span class="eyebrow">内容工具</span><h2 id="replace-dialog-title">全局替换</h2><p id="replace-dialog-description" class="dialog-header-meta">只处理已保存内容;每个被修改的章节或设定都会保留一个可恢复版本。</p></div>
|
|
646
|
+
<button id="replace-dialog-close" class="dialog-close" aria-label="关闭全局替换" type="button">×</button>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="replace-dialog-body">
|
|
649
|
+
<label class="replace-field">查找内容<input id="replace-find" name="find" type="text" maxlength="500" autocomplete="off" required placeholder="输入要查找的文字"></label>
|
|
650
|
+
<label class="replace-field">替换为<textarea id="replace-with" name="replacement" maxlength="200000" rows="4" placeholder="输入替换后的文字;留空表示删除"></textarea></label>
|
|
651
|
+
<fieldset class="replace-scope-fieldset" aria-describedby="replace-scope-description">
|
|
652
|
+
<legend>替换范围</legend>
|
|
653
|
+
<p id="replace-scope-description">默认只替换章节正文内容。</p>
|
|
654
|
+
<label class="replace-scope-option"><input type="radio" name="replaceScope" value="prose" checked><span><strong>正文</strong><small>仅替换章节正文内容</small></span></label>
|
|
655
|
+
<label class="replace-scope-option"><input type="radio" name="replaceScope" value="settings"><span><strong>设定库</strong><small>仅替换世界观设定内容</small></span></label>
|
|
656
|
+
<label class="replace-scope-option"><input type="radio" name="replaceScope" value="prose-and-settings"><span><strong>正文+设定库</strong><small>同时替换章节正文和世界观设定内容</small></span></label>
|
|
657
|
+
</fieldset>
|
|
658
|
+
<p id="replace-permission-note" class="replace-permission-note" role="status"></p>
|
|
659
|
+
</div>
|
|
660
|
+
<div class="dialog-actions">
|
|
661
|
+
<button id="replace-cancel" class="ghost-button" type="button">取消</button>
|
|
662
|
+
<button id="replace-submit" class="primary-button" type="submit">开始替换</button>
|
|
663
|
+
</div>
|
|
664
|
+
</form>
|
|
665
|
+
</dialog>
|
|
666
|
+
|
|
641
667
|
<dialog id="appearance-dialog" class="dialog">
|
|
642
668
|
<form id="appearance-form" method="dialog">
|
|
643
669
|
<div class="dialog-header">
|
|
@@ -998,6 +1024,6 @@
|
|
|
998
1024
|
<div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
|
|
999
1025
|
<script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
|
|
1000
1026
|
<script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
|
|
1001
|
-
<script type="module" src="/app.js?v=
|
|
1027
|
+
<script type="module" src="/app.js?v=20260809-global-replace-v2"></script>
|
|
1002
1028
|
</body>
|
|
1003
1029
|
</html>
|