@musnows/scriverse 0.5.1 → 0.5.3

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.
@@ -31,7 +31,7 @@ import {
31
31
  taskScopeLabel,
32
32
  timelineStatusLabel,
33
33
  characterStateFieldLabel
34
- } from "/display-labels.js?v=20260725-state-field-zh";
34
+ } from "/display-labels.js?v=20260726-character-name-variant";
35
35
  import { parsePageRoute, serializePageRoute } from "/page-route.js?v=20260723-knowledge-editor-page";
36
36
  import { splitRelationshipKeywordInput, splitRelationshipKeywords, uniqueRelationshipKeywords } from "/relationship-keywords.js?v=20260720-relationship-keyword-chips";
37
37
  import { tokenizeVisibleSpaces } from "/whitespace-visualization.js?v=20260718-visible-whitespace";
@@ -147,6 +147,14 @@ function analysisTaskTypeLabel(taskType) {
147
147
  return analysisTaskTypeLabels.get(String(taskType)) ?? "其他分析";
148
148
  }
149
149
 
150
+ function analysisTaskModelPurpose(taskType) {
151
+ if (taskType === "timeline-analysis") return "timeline-analysis";
152
+ if (taskType === "relationship-analysis") return "relationship-analysis";
153
+ if (taskType === "consistency-check") return "consistency-check";
154
+ if (taskType === "chapter-analysis") return "chapter-analysis";
155
+ return "book-analysis";
156
+ }
157
+
150
158
  function analysisTaskStatusLabel(status) {
151
159
  return ({
152
160
  pending: "待执行",
@@ -2646,6 +2654,7 @@ async function openMembersDialog(targetWork = state.work) {
2646
2654
  if (!targetWork) return;
2647
2655
  memberDialogWork = targetWork;
2648
2656
  const canManage = ["admin", "owner"].includes(String(targetWork.accessRole));
2657
+ $("#members-settings-return").classList.toggle("hidden", $("#settings-hub-view").classList.contains("hidden"));
2649
2658
  $("#members-dialog-eyebrow").textContent = `作品权限 · 《${targetWork.title}》`;
2650
2659
  $("#members-dialog-title").textContent = "成员模块权限";
2651
2660
  $("#members-list").innerHTML = '<p class="empty-state">正在读取成员……</p>';
@@ -2755,6 +2764,13 @@ async function showSettingsHub() {
2755
2764
  return true;
2756
2765
  }
2757
2766
 
2767
+ async function returnToSettingsHub(actionSelector, dialogSelector = null) {
2768
+ const dialog = dialogSelector ? $(dialogSelector) : null;
2769
+ if (dialog?.open) dialog.close();
2770
+ if (!(await showSettingsHub())) return;
2771
+ queueMicrotask(() => $(actionSelector)?.focus());
2772
+ }
2773
+
2758
2774
  async function returnFromSettings() {
2759
2775
  const context = settingsReturnContext ?? { view: "shelf" };
2760
2776
  settingsReturnContext = null;
@@ -3438,9 +3454,11 @@ function openReviewDetailDialog(item) {
3438
3454
  const evidenceHtml = evidence.length
3439
3455
  ? `<ul>${evidence.map((entry) => {
3440
3456
  if (!entry || typeof entry !== "object") return `<li>${esc(String(entry))}</li>`;
3441
- const source = entry.chapterTitle || entry.chapterId || "相关证据";
3457
+ const source = entry.sourceTitle || entry.chapterTitle || entry.chapterId || "相关证据";
3442
3458
  const quote = entry.quote ? `<blockquote>${esc(entry.quote)}</blockquote>` : "";
3443
- const supports = entry.supports ? `<small>${esc(entry.supports)}</small>` : "";
3459
+ const supports = entry.reason
3460
+ ? `<small>${esc(entry.reason)}${typeof entry.confidence === "number" ? ` · 置信度 ${Math.round(entry.confidence * 100)}%` : ""}</small>`
3461
+ : entry.supports ? `<small>${esc(entry.supports)}</small>` : "";
3444
3462
  return `<li><strong>${esc(source)}</strong>${quote}${supports}</li>`;
3445
3463
  }).join("")}</ul>`
3446
3464
  : "<p>暂无证据</p>";
@@ -4031,9 +4049,10 @@ async function renderTasks(page = taskListPage) {
4031
4049
  </div>
4032
4050
  </div>
4033
4051
  </section>
4034
- ${tasks.length ? `<table class="table-list task-table"><thead><tr><th>分析类型</th><th>范围</th><th>状态</th><th>进度</th><th>操作</th></tr></thead><tbody>${tasks.map((item) => `
4052
+ ${tasks.length ? `<table class="table-list task-table"><thead><tr><th>分析类型</th><th>任务模型</th><th>范围</th><th>状态</th><th>进度</th><th>操作</th></tr></thead><tbody>${tasks.map((item) => `
4035
4053
  <tr>
4036
4054
  <td>${esc(analysisTaskTypeLabel(item.taskType))}</td>
4055
+ <td>${esc(item.model?.displayName || "运行时使用默认模型")}</td>
4037
4056
  <td>${esc(item.scopeSummary || taskScopeLabel(item.scope?.type || "book"))}</td>
4038
4057
  <td class="task-status-cell">${renderAnalysisTaskStatus(item)}</td>
4039
4058
  <td class="task-progress-cell">${renderAnalysisTaskProgress(item)}</td>
@@ -4089,7 +4108,7 @@ async function renderTasks(page = taskListPage) {
4089
4108
  button.disabled = true;
4090
4109
  const taskId = encodeURIComponent(button.dataset.taskDetail);
4091
4110
  Promise.all([
4092
- api(`/api/tasks/${taskId}`),
4111
+ api(`/api/tasks/${taskId}/detail`),
4093
4112
  api(`/api/tasks/${taskId}/trace`).catch((error) => {
4094
4113
  if (error.code === "WORK_MODULE_READ_DENIED") return { restricted: true, captured: false, calls: [] };
4095
4114
  throw error;
@@ -4113,7 +4132,7 @@ async function renderTasks(page = taskListPage) {
4113
4132
  const cancel = button.parentElement.querySelector("[data-cancel-task]");
4114
4133
  if (cancel) cancel.textContent = "取消运行";
4115
4134
  scheduleTaskProgressRefresh(workId, 1);
4116
- const completed = await api(`/api/tasks/${button.dataset.runTask}/run`, { method: "POST", body: { modelId: $("#ai-model").value || undefined } });
4135
+ const completed = await api(`/api/tasks/${button.dataset.runTask}/run`, { method: "POST", body: {} });
4117
4136
  toast(completed.status === "cancelled" ? "分析任务已取消" : completed.status === "expired" ? "正文已变化,本次分析已过期" : "分析已完成");
4118
4137
  if (state.module === "tasks" && state.work?.id === workId) await renderTasks();
4119
4138
  } catch (error) {
@@ -4241,32 +4260,6 @@ function renderTaskTraceRound(round) {
4241
4260
  </section>`;
4242
4261
  }
4243
4262
 
4244
- function renderTaskTraceRoundSummary(round) {
4245
- return `<section class="task-trace-round task-trace-round-summary">
4246
- <header class="task-trace-round-header">
4247
- <span class="task-trace-round-index">${esc(String(round?.round ?? 1))}</span>
4248
- <div><strong>Agent 轮次 ${esc(String(round?.round ?? 1))}</strong><small>${Number(round?.messageCount ?? 0)} 条消息 · ${Number(round?.attemptCount ?? 0)} 次请求尝试 · ${Number(round?.toolExecutionCount ?? 0)} 次工具执行</small></div>
4249
- <time>${esc(formatDateTime(round?.requestedAt))}</time>
4250
- </header>
4251
- <p>本轮 Prompt 共 ${Number(round?.promptChars ?? 0).toLocaleString("zh-CN")} 字符,正文、模型响应和工具结果尚未传输。</p>
4252
- </section>`;
4253
- }
4254
-
4255
- function renderTaskTraceCallPreview(detail) {
4256
- const trace = detail?.trace && typeof detail.trace === "object" ? detail.trace : {};
4257
- const messages = Array.isArray(trace.initialMessages) ? trace.initialMessages : [];
4258
- const rounds = Array.isArray(trace.rounds) ? trace.rounds : [];
4259
- return `<div class="task-trace-load-state is-loaded">
4260
- <div><strong>Prompt 预览</strong><p>全部消息合计最多传输 ${Number(detail?.previewLimit ?? 3000).toLocaleString("zh-CN")} 个字符;模型响应和工具结果仍未传输。</p></div>
4261
- <details class="task-trace-initial" open>
4262
- <summary>初始上下文预览(${messages.length} 条消息 · 原文 ${Number(detail?.totalPromptChars ?? 0).toLocaleString("zh-CN")} 字符)</summary>
4263
- ${renderTaskTraceMessages(messages)}
4264
- </details>
4265
- <div class="task-trace-rounds">${rounds.map(renderTaskTraceRoundSummary).join("") || '<p class="task-trace-empty">尚未记录 Agent 轮次。</p>'}</div>
4266
- <div class="card-actions"><button class="ghost-button" type="button" data-load-task-trace-call="full">查看完整调用</button></div>
4267
- </div>`;
4268
- }
4269
-
4270
4263
  function renderTaskTraceCallFull(detail) {
4271
4264
  const trace = detail?.trace && typeof detail.trace === "object" ? detail.trace : {};
4272
4265
  const messages = Array.isArray(trace.initialMessages) ? trace.initialMessages : [];
@@ -4285,19 +4278,17 @@ function bindTaskTraceCallActions(container) {
4285
4278
  const call = button.closest("[data-task-trace-call]");
4286
4279
  const content = call?.querySelector("[data-task-trace-call-content]");
4287
4280
  if (!call || !content || button.disabled) return;
4288
- const mode = button.dataset.loadTaskTraceCall;
4289
4281
  button.disabled = true;
4290
- button.textContent = mode === "full" ? "正在加载完整内容" : "正在加载预览";
4282
+ button.textContent = "正在加载中";
4291
4283
  try {
4292
4284
  const taskId = encodeURIComponent(call.dataset.taskTraceTask);
4293
4285
  const callId = encodeURIComponent(call.dataset.taskTraceCall);
4294
- const detail = await api(`/api/tasks/${taskId}/trace/calls/${callId}${mode === "full" ? "?full=true" : ""}`);
4295
- content.innerHTML = mode === "full" ? renderTaskTraceCallFull(detail) : renderTaskTraceCallPreview(detail);
4296
- bindTaskTraceCallActions(content);
4286
+ const detail = await api(`/api/tasks/${taskId}/trace/calls/${callId}`);
4287
+ content.innerHTML = renderTaskTraceCallFull(detail);
4297
4288
  } catch (error) {
4298
4289
  toast(error.message, "error");
4299
4290
  button.disabled = false;
4300
- button.textContent = mode === "full" ? "查看完整调用" : "加载内容预览";
4291
+ button.textContent = "加载完整内容";
4301
4292
  }
4302
4293
  }));
4303
4294
  }
@@ -4323,7 +4314,7 @@ function renderTaskTraceVisualization(trace, taskId) {
4323
4314
  return `<section class="task-trace-section" aria-labelledby="task-trace-title">
4324
4315
  <header class="task-trace-heading">
4325
4316
  <div><span class="eyebrow">执行追踪</span><h3 id="task-trace-title">完整全流程上下文</h3></div>
4326
- <p>首次只加载调用摘要;Prompt 预览与完整调用内容均按需单独请求。</p>
4317
+ <p>首次只加载调用摘要;每次调用的完整 Prompt、响应和工具结果按需单独请求。</p>
4327
4318
  </header>
4328
4319
  <div class="task-trace-metrics" aria-label="执行追踪统计">
4329
4320
  <div><strong>${capturedCalls.length}</strong><span>模型调用</span></div>
@@ -4334,10 +4325,12 @@ function renderTaskTraceVisualization(trace, taskId) {
4334
4325
  <div class="task-trace-calls">${capturedCalls.map((call, index) => {
4335
4326
  const modelName = call.model?.displayName || call.model?.modelId || "未知模型";
4336
4327
  const providerName = call.provider?.name || "未知供应商";
4328
+ const sourceRefs = Array.isArray(call.sourceRefs) ? call.sourceRefs : [];
4329
+ const sourceSummary = sourceRefs.map((source) => String(source?.title || "")).filter(Boolean).join("、");
4337
4330
  return `<details class="task-trace-call is-${esc(call.status || "failed")}" data-task-trace-call="${esc(call.id)}" data-task-trace-task="${esc(taskId)}" ${index === 0 ? "open" : ""}>
4338
4331
  <summary>
4339
4332
  <span class="task-trace-call-index">${index + 1}</span>
4340
- <span><strong>${esc(modelName)}</strong><small>${esc(providerName)} · ${Number(call.trace?.roundCount || 0)} 轮 · ${Number(call.inputChars || 0).toLocaleString("zh-CN")} → ${Number(call.outputChars || 0).toLocaleString("zh-CN")} 字符</small></span>
4333
+ <span><strong>${esc(modelName)}</strong><small>${esc(providerName)} · ${Number(call.trace?.roundCount || 0)} 轮 · ${Number(call.inputChars || 0).toLocaleString("zh-CN")} → ${Number(call.outputChars || 0).toLocaleString("zh-CN")} 字符</small>${sourceSummary ? `<small class="task-trace-call-sources" title="${esc(sourceSummary)}">发送:${esc(sourceSummary)}</small>` : ""}</span>
4341
4334
  <span class="task-trace-status">${call.status === "completed" ? "已完成" : call.status === "running" ? "运行中" : "失败"}</span>
4342
4335
  </summary>
4343
4336
  <div class="task-trace-call-body">
@@ -4346,7 +4339,7 @@ function renderTaskTraceVisualization(trace, taskId) {
4346
4339
  <div data-task-trace-call-content>
4347
4340
  <div class="task-trace-load-state">
4348
4341
  <div><strong>调用内容尚未加载</strong><p>这次调用的 Prompt、模型响应和工具结果不会随任务详情传输。</p></div>
4349
- <button class="ghost-button" type="button" data-load-task-trace-call="preview">加载内容预览</button>
4342
+ <button class="ghost-button" type="button" data-load-task-trace-call="full">加载完整内容</button>
4350
4343
  </div>
4351
4344
  </div>
4352
4345
  </div>
@@ -4355,6 +4348,85 @@ function renderTaskTraceVisualization(trace, taskId) {
4355
4348
  </section>`;
4356
4349
  }
4357
4350
 
4351
+ function renderTaskResultEvidence(item) {
4352
+ const evidence = Array.isArray(item.evidence) ? item.evidence : [];
4353
+ if (!evidence.length) return '<p class="task-result-muted">没有保存可展示的证据摘录。</p>';
4354
+ return `<ul class="task-result-evidence">${evidence.map((item) => {
4355
+ const source = item.chapterTitle || item.chapterId || "未标明章节";
4356
+ return `<li><strong>${esc(source)}</strong>${item.quote ? `<q>${esc(item.quote)}</q>` : ""}${item.supports ? `<small>${esc(item.supports)}</small>` : ""}</li>`;
4357
+ }).join("")}</ul>`;
4358
+ }
4359
+
4360
+ function renderTaskResultItem(item) {
4361
+ const tags = Array.isArray(item.tags) ? item.tags : [];
4362
+ const details = Array.isArray(item.details) ? item.details : [];
4363
+ const evidence = Array.isArray(item.evidence) ? item.evidence : [];
4364
+ return `<article class="task-result-item">
4365
+ <header>
4366
+ <div><strong>${esc(item.title || "未命名结果")}</strong>${item.subtitle ? `<small>${esc(item.subtitle)}</small>` : ""}</div>
4367
+ </header>
4368
+ ${item.description ? `<p class="task-result-item-description">${esc(item.description)}</p>` : ""}
4369
+ ${details.length ? `<dl class="task-result-item-details">${details.map((detail) => `<div><dt>${esc(detail.label || "详情")}</dt><dd>${esc(detail.value ?? "")}</dd></div>`).join("")}</dl>` : ""}
4370
+ ${tags.length ? `<div class="task-result-tags">${tags.map((tag) => `<span>${esc(tag)}</span>`).join("")}</div>` : ""}
4371
+ ${evidence.length ? `<details><summary>查看原文证据(${evidence.length} 条)</summary>${renderTaskResultEvidence(item)}</details>` : ""}
4372
+ </article>`;
4373
+ }
4374
+
4375
+ function renderTaskResult(task) {
4376
+ const result = task.resultSummary && typeof task.resultSummary === "object" ? task.resultSummary : {};
4377
+ const metrics = Array.isArray(result.metrics) ? result.metrics : [];
4378
+ const storageTargets = Array.isArray(result.storageTargets) ? result.storageTargets : [];
4379
+ const sections = Array.isArray(result.sections) ? result.sections : [];
4380
+ return `<div class="task-result-readable">
4381
+ <section class="task-result-section">
4382
+ <h4>${esc(result.title || "分析结果")}</h4>
4383
+ <p><strong>分析内容</strong> ${esc(result.analysisContent || `${analysisTaskTypeLabel(task.taskType)};范围:${task.scopeSummary || "未指定"}`)}</p>
4384
+ <p class="task-result-summary">${esc(result.summary || "任务尚未产生分析结果。")}</p>
4385
+ ${result.restricted ? '<p class="task-result-warning">部分结果因当前账号权限受限而隐藏。</p>' : ""}
4386
+ </section>
4387
+ <section class="task-result-section">
4388
+ <h4>结果保存位置</h4>
4389
+ <p><strong>作品</strong> ${esc(state.work?.title || "当前作品")}</p>
4390
+ <div class="task-result-storage-list">${storageTargets.map((target) => `<article><strong>${esc(target.label || "分析结果")}</strong><p>${esc(target.location || "当前作品 · AI 分析记录")}</p><small>保存 ${Number(target.count || 0)} 条${target.note ? ` · ${esc(target.note)}` : ""}</small></article>`).join("") || "<p>没有可说明的结果保存位置。</p>"}</div>
4391
+ ${metrics.length ? `<div class="task-result-metrics" aria-label="分析结果统计">${metrics.map((item) => `<span><strong>${esc(item.value ?? 0)}</strong>${esc(item.label || "数量")}</span>`).join("")}</div>` : ""}
4392
+ </section>
4393
+ ${sections.map((section) => {
4394
+ const items = Array.isArray(section.items) ? section.items : [];
4395
+ const totalCount = Number(section.totalCount ?? items.length);
4396
+ return `<section class="task-result-section"><h4>${esc(section.title || "分析结论")}(${totalCount})</h4>${items.map(renderTaskResultItem).join("") || `<p class="task-result-empty">${esc(section.emptyMessage || "没有可展示的结果。")}</p>`}${totalCount > items.length ? `<p class="task-result-muted">可读摘要展示前 ${items.length} 项;完整 ${totalCount} 项可通过下方按钮查看完整 JSON。</p>` : ""}</section>`;
4397
+ }).join("")}
4398
+ <section class="task-result-json-loader">
4399
+ <div><strong>完整返回 JSON</strong><p>点击后按需从服务器拉取本任务的完整 JSON,不做字符截断。</p></div>
4400
+ <button class="ghost-button" type="button" data-load-task-result-json="${esc(task.id)}" ${task.hasResult ? "" : "disabled"}>${task.hasResult ? "查看完整 JSON" : "尚无 JSON 结果"}</button>
4401
+ <div class="task-result-json-content" data-task-result-json-content></div>
4402
+ </section>
4403
+ </div>`;
4404
+ }
4405
+
4406
+ function bindTaskResultActions(container) {
4407
+ container.querySelectorAll("[data-load-task-result-json]").forEach((button) => button.addEventListener("click", async () => {
4408
+ if (button.disabled) return;
4409
+ const content = button.closest(".task-result-json-loader")?.querySelector("[data-task-result-json-content]");
4410
+ if (!content) return;
4411
+ button.disabled = true;
4412
+ button.textContent = "正在拉取完整 JSON";
4413
+ try {
4414
+ const payload = await api(`/api/tasks/${encodeURIComponent(button.dataset.loadTaskResultJson)}/result`);
4415
+ const resultJson = document.createElement("textarea");
4416
+ resultJson.readOnly = true;
4417
+ resultJson.spellcheck = false;
4418
+ resultJson.setAttribute("aria-label", "完整返回 JSON");
4419
+ resultJson.value = JSON.stringify(payload.result, null, 2);
4420
+ content.replaceChildren(resultJson);
4421
+ button.textContent = "完整 JSON 已加载";
4422
+ } catch (error) {
4423
+ toast(error.message, "error");
4424
+ button.disabled = false;
4425
+ button.textContent = "重新加载完整 JSON";
4426
+ }
4427
+ }));
4428
+ }
4429
+
4358
4430
  function openTaskDetailDialog(task, trace) {
4359
4431
  if (!task) return;
4360
4432
  const details = Array.isArray(task.scopeDetails) ? task.scopeDetails : [];
@@ -4371,26 +4443,28 @@ function openTaskDetailDialog(task, trace) {
4371
4443
  </li>`;
4372
4444
  }
4373
4445
  if (item.type === "book") return "<li>全书</li>";
4446
+ if (item.type === "selection") return item.restricted
4447
+ ? "<li>选定内容(正文读取权限受限)</li>"
4448
+ : `<li>选定内容:${esc(item.selection || "未提供")}</li>`;
4449
+ if (item.type === "none") return "<li>无上下文</li>";
4374
4450
  return `<li>${esc(JSON.stringify(item))}</li>`;
4375
4451
  }).join("") || "<li>无范围详情</li>";
4376
4452
  const failures = Array.isArray(task.failures) ? task.failures : [];
4377
4453
  const failureHtml = failures.length
4378
4454
  ? `<ul>${failures.map((item) => `<li>${esc(item.message || JSON.stringify(item))}</li>`).join("")}</ul>`
4379
4455
  : "<p>无</p>";
4380
- const resultPreview = task.result && Object.keys(task.result).length
4381
- ? `<pre class="task-detail-result">${esc(JSON.stringify(task.result, null, 2).slice(0, 2000))}</pre>`
4382
- : "<p>尚无结果</p>";
4456
+ const resultPreview = renderTaskResult(task);
4383
4457
  openDialog("任务详情",
4384
4458
  `<div class="task-detail">
4385
4459
  <section class="task-detail-overview">
4386
- <p><strong>任务 ID</strong><br><code>${esc(task.id)}</code></p>
4460
+ <p><strong>任务 ID</strong><br><code>${esc(task.id)}</code><br><small>创建于 ${esc(formatDateTime(task.createdAt))} · 更新于 ${esc(formatDateTime(task.updatedAt))}</small></p>
4387
4461
  <p><strong>类型</strong> ${esc(analysisTaskTypeLabel(task.taskType))}</p>
4462
+ <p><strong>任务模型</strong> ${esc(task.model?.displayName || "运行时使用默认模型")}${task.model?.modelId ? ` · <code>${esc(task.model.modelId)}</code>` : ""}</p>
4388
4463
  <p><strong>状态</strong> ${esc(analysisTaskStatusLabel(task.status))} · 进度 ${Number(task.progress ?? 0)}%</p>
4389
4464
  <p><strong>范围摘要</strong> ${esc(task.scopeSummary || "未指定")}</p>
4390
4465
  <div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
4391
4466
  <div><strong>失败信息</strong>${failureHtml}</div>
4392
4467
  <div><strong>结果摘要</strong>${resultPreview}</div>
4393
- <p><small>创建于 ${esc(formatDateTime(task.createdAt))} · 更新于 ${esc(formatDateTime(task.updatedAt))}</small></p>
4394
4468
  </section>
4395
4469
  ${renderTaskTraceVisualization(trace, task.id)}
4396
4470
  </div>`,
@@ -4398,6 +4472,7 @@ function openTaskDetailDialog(task, trace) {
4398
4472
  "AI 分析详情",
4399
4473
  { submitLabel: "关闭", wide: true, trace: true });
4400
4474
  bindTaskTraceCallActions($("#dialog-fields"));
4475
+ bindTaskResultActions($("#dialog-fields"));
4401
4476
  }
4402
4477
 
4403
4478
  function renderProviderCards(providers, models) {
@@ -6346,14 +6421,26 @@ function openReviewDialog() {
6346
6421
  async function openTaskDialog() {
6347
6422
  const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => [chapter.id, `${volume.title} / ${chapter.title}`]));
6348
6423
  let relationshipCharacters = [];
6424
+ let taskModels = [];
6425
+ let taskDefaults = [];
6349
6426
  try {
6350
- relationshipCharacters = canReadModule("characters")
6351
- ? await apiAllPages(`/api/works/${state.work.id}/characters`)
6352
- : [];
6427
+ [relationshipCharacters, taskModels, taskDefaults] = await Promise.all([
6428
+ canReadModule("characters")
6429
+ ? apiAllPages(`/api/works/${state.work.id}/characters`)
6430
+ : Promise.resolve([]),
6431
+ api(`/api/works/${state.work.id}/models`),
6432
+ api(`/api/works/${state.work.id}/task-defaults`)
6433
+ ]);
6353
6434
  } catch (error) {
6354
- toast(`角色列表加载失败:${error.message}`, "error");
6435
+ toast(`分析任务配置加载失败:${error.message}`, "error");
6355
6436
  return;
6356
6437
  }
6438
+ const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
6439
+ const availableTaskModels = taskModels.filter((model) =>
6440
+ model.enabled
6441
+ && model.providerStatus === "enabled"
6442
+ && model.providerConnectionStatus === "success"
6443
+ );
6357
6444
  const characterOptions = relationshipCharacters.map((character) => [character.id, character.name]);
6358
6445
  const relationshipCharacterPicker = `<div class="form-field relationship-character-field">
6359
6446
  <span id="relationship-character-label">被分析角色(可多选)</span>
@@ -6374,27 +6461,36 @@ async function openTaskDialog() {
6374
6461
  </div>`;
6375
6462
  const defaultTaskType = ANALYSIS_TYPES[0].value;
6376
6463
  const taskTypeField = `<div class="form-field analysis-type-field"><label>分析类型<select name="taskType" aria-describedby="analysis-type-description">${ANALYSIS_TYPES.map(({ value, label }) => `<option value="${esc(value)}" ${value === defaultTaskType ? "selected" : ""}>${esc(label)}</option>`).join("")}</select></label><p id="analysis-type-description" class="analysis-type-description" aria-live="polite">${esc(analysisTypeDescription(defaultTaskType))}</p></div>`;
6464
+ const defaultModelId = defaultModelByTask.get(analysisTaskModelPurpose(defaultTaskType)) ?? "";
6465
+ const modelField = `<label>任务模型<select name="modelId" required aria-describedby="analysis-task-model-help">
6466
+ <option value="" ${availableTaskModels.some((model) => model.id === defaultModelId) ? "" : "selected"} disabled>${availableTaskModels.length ? "请选择模型" : "没有可用模型"}</option>
6467
+ ${availableTaskModels.map((model) => `<option value="${esc(model.id)}" ${model.id === defaultModelId ? "selected" : ""}>${esc(modelOptionLabel(model))}</option>`).join("")}
6468
+ </select><small id="analysis-task-model-help">默认值来自“本书 AI 设置”,只修改当前任务,不会改变全书默认模型。</small></label>`;
6377
6469
  const chapterField = `<label class="task-chapter-field">章节<select name="chapterId">${chapterOptions.map(([key, text], index) => `<option value="${esc(key)}" ${index === 0 ? "selected" : ""}>${esc(text)}</option>`).join("")}</select></label>`;
6378
6470
  const relationshipFields = `<div class="relationship-analysis-options hidden">
6379
6471
  ${relationshipCharacterPicker}
6380
- <p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。</span></p>
6472
+ <p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。默认仅追加不存在的关系,不修改或删除已有关系。</span></p>
6381
6473
  <div class="relationship-overwrite-card hidden">
6382
6474
  <label class="checkbox-field"><input name="replaceExistingRelationships" type="checkbox" disabled><span>用本次结果覆盖所选角色的已有关系</span></label>
6383
- <p>任务成功后,会先删除所有涉及所选角色的旧关系,再写入本次分析结果。</p>
6475
+ <p>勾选后,任务成功时会先删除所有涉及所选角色的旧关系,再写入本次分析结果;不勾选则只追加新关系。</p>
6384
6476
  </div>
6385
6477
  <label>额外分析提示<textarea name="additionalPrompt" maxlength="10000" placeholder="例如:重点识别权力继承、师承变化或隐秘亲缘关系"></textarea><small>将同时追加到证据收集和全局关系归纳提示词,仅影响本次任务。</small></label>
6386
6478
  </div>`;
6387
- openDialog("开始 AI 分析", taskTypeField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
6479
+ openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
6388
6480
  const taskType = String(form.get("taskType"));
6481
+ const modelId = String(form.get("modelId"));
6389
6482
  const scopeType = String(form.get("scopeType"));
6390
6483
  const includeAllSettings = taskType === "relationship-analysis" && scopeType === "book-with-settings";
6484
+ const settingsOnly = taskType === "relationship-analysis" && scopeType === "settings";
6391
6485
  const additionalPrompt = taskType === "relationship-analysis" ? String(form.get("additionalPrompt") ?? "").trim() : "";
6392
6486
  const characterIds = taskType === "relationship-analysis" ? form.getAll("characterIds").map(String).filter(Boolean) : [];
6393
6487
  const replaceExistingRelationships = characterIds.length > 0 && form.get("replaceExistingRelationships") === "on";
6394
- const scope = taskType === "character-identity-audit" || scopeType === "book" || includeAllSettings
6488
+ const scope = settingsOnly
6489
+ ? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
6490
+ : taskType === "character-identity-audit" || scopeType === "book" || includeAllSettings
6395
6491
  ? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
6396
6492
  : { type: "chapter", chapterId: form.get("chapterId"), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
6397
- await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType, scope } });
6493
+ await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType, scope, modelId } });
6398
6494
  taskListPage = 1;
6399
6495
  toast("分析任务已创建,已进入任务队列");
6400
6496
  void renderTasks(1).catch((error) => toast(`任务已创建,但列表刷新失败:${error.message}`, "error"));
@@ -6405,6 +6501,7 @@ async function openTaskDialog() {
6405
6501
  errorPrefix: "任务创建失败:"
6406
6502
  });
6407
6503
  const taskTypeSelect = $("#dialog-fields").querySelector('select[name="taskType"]');
6504
+ const taskModelSelect = $("#dialog-fields").querySelector('select[name="modelId"]');
6408
6505
  const scopeTypeSelect = $("#dialog-fields").querySelector('select[name="scopeType"]');
6409
6506
  const chapterSelect = $("#dialog-fields").querySelector('select[name="chapterId"]');
6410
6507
  const chapterFieldElement = chapterSelect.closest(".task-chapter-field");
@@ -6424,7 +6521,10 @@ async function openTaskDialog() {
6424
6521
  const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-overwrite-card");
6425
6522
  const allSettingsOption = document.createElement("option");
6426
6523
  allSettingsOption.value = "book-with-settings";
6427
- allSettingsOption.textContent = "全书 + 所有设定";
6524
+ allSettingsOption.textContent = "全书 + 设定集";
6525
+ const settingsOnlyOption = document.createElement("option");
6526
+ settingsOnlyOption.value = "settings";
6527
+ settingsOnlyOption.textContent = "仅设定集";
6428
6528
  const syncChapterField = () => {
6429
6529
  const disabled = scopeTypeSelect.value !== "chapter";
6430
6530
  chapterSelect.disabled = disabled;
@@ -6460,10 +6560,15 @@ async function openTaskDialog() {
6460
6560
  const syncRelationshipOptions = () => {
6461
6561
  const enabled = taskTypeSelect.value === "relationship-analysis";
6462
6562
  if (enabled && !allSettingsOption.isConnected) scopeTypeSelect.append(allSettingsOption);
6563
+ if (enabled && !settingsOnlyOption.isConnected) scopeTypeSelect.append(settingsOnlyOption);
6463
6564
  if (!enabled && allSettingsOption.isConnected) {
6464
6565
  if (scopeTypeSelect.value === allSettingsOption.value) scopeTypeSelect.value = "book";
6465
6566
  allSettingsOption.remove();
6466
6567
  }
6568
+ if (!enabled && settingsOnlyOption.isConnected) {
6569
+ if (scopeTypeSelect.value === settingsOnlyOption.value) scopeTypeSelect.value = "book";
6570
+ settingsOnlyOption.remove();
6571
+ }
6467
6572
  relationshipOptions.classList.toggle("hidden", !enabled);
6468
6573
  relationshipPrompt.disabled = !enabled;
6469
6574
  relationshipCharacterTrigger.disabled = !enabled;
@@ -6478,8 +6583,13 @@ async function openTaskDialog() {
6478
6583
  filterRelationshipCharacters();
6479
6584
  syncChapterField();
6480
6585
  };
6586
+ const syncTaskModelDefault = () => {
6587
+ const defaultId = defaultModelByTask.get(analysisTaskModelPurpose(taskTypeSelect.value)) ?? "";
6588
+ taskModelSelect.value = availableTaskModels.some((model) => model.id === defaultId) ? defaultId : "";
6589
+ };
6481
6590
  taskTypeSelect.addEventListener("change", () => {
6482
6591
  description.textContent = analysisTypeDescription(taskTypeSelect.value);
6592
+ syncTaskModelDefault();
6483
6593
  syncRelationshipOptions();
6484
6594
  });
6485
6595
  relationshipCharacterTrigger.addEventListener("click", () => {
@@ -7483,6 +7593,7 @@ $("#register-form").addEventListener("submit", async (event) => {
7483
7593
  });
7484
7594
  $("#settings-return").addEventListener("click", () => returnFromSettings().catch((error) => toast(error.message, "error")));
7485
7595
  $("#platform-ai-button").addEventListener("click", () => showPlatformAi().catch((error) => toast(error.message, "error")));
7596
+ $("#platform-ai-return").addEventListener("click", () => returnToSettingsHub("#platform-ai-button").catch((error) => toast(error.message, "error")));
7486
7597
  $("#user-management-button").addEventListener("click", openUsersDialog);
7487
7598
  $("#platform-ui-settings-button").addEventListener("click", openPlatformUiSettingsDialog);
7488
7599
  $("#collaboration-button").addEventListener("click", () => openMembersDialog());
@@ -7492,7 +7603,9 @@ $("#presence-button").addEventListener("click", () => {
7492
7603
  $("#presence-button").setAttribute("aria-expanded", String(open));
7493
7604
  });
7494
7605
  $("#users-dialog-close").addEventListener("click", () => $("#users-dialog").close());
7606
+ $("#users-settings-return").addEventListener("click", () => returnToSettingsHub("#user-management-button", "#users-dialog").catch((error) => toast(error.message, "error")));
7495
7607
  $("#platform-ui-settings-close").addEventListener("click", () => $("#platform-ui-settings-dialog").close());
7608
+ $("#platform-ui-settings-return").addEventListener("click", () => returnToSettingsHub("#platform-ui-settings-button", "#platform-ui-settings-dialog").catch((error) => toast(error.message, "error")));
7496
7609
  $("#platform-ui-settings-cancel").addEventListener("click", () => $("#platform-ui-settings-dialog").close());
7497
7610
  $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
7498
7611
  event.preventDefault();
@@ -7522,6 +7635,7 @@ $("#platform-ui-settings-form").addEventListener("submit", async (event) => {
7522
7635
  }
7523
7636
  });
7524
7637
  $("#members-dialog-close").addEventListener("click", () => $("#members-dialog").close());
7638
+ $("#members-settings-return").addEventListener("click", () => returnToSettingsHub("#collaboration-button", "#members-dialog").catch((error) => toast(error.message, "error")));
7525
7639
  $("#members-dialog").addEventListener("close", () => {
7526
7640
  memberDialogWork = null;
7527
7641
  memberDialogMembers = [];
@@ -7591,6 +7705,7 @@ function cleanupExpandedRelationshipMap() {
7591
7705
  $("#relationship-map-close").addEventListener("click", () => $("#relationship-map-dialog").close());
7592
7706
  $("#relationship-map-dialog").addEventListener("close", cleanupExpandedRelationshipMap);
7593
7707
  $("#appearance-button").addEventListener("click", openAppearanceDialog);
7708
+ $("#appearance-settings-return").addEventListener("click", () => returnToSettingsHub("#appearance-button", "#appearance-dialog").catch((error) => toast(error.message, "error")));
7594
7709
  $("#toggle-whitespace-appearance").addEventListener("click", toggleChapterWhitespaceVisibility);
7595
7710
  $("#theme-toggle").addEventListener("click", () => {
7596
7711
  const theme = nextTheme(currentColorTheme());
@@ -37,6 +37,7 @@ export function reviewItemTypeLabel(value) {
37
37
  return enumLabel({
38
38
  consistency: "一致性问题",
39
39
  "character-duplicate": "角色重复",
40
+ "character-name-variant": "疑似人物名错字",
40
41
  "timeline-conflict": "时间线冲突",
41
42
  "setting-conflict": "设定冲突",
42
43
  "relationship-conflict": "关系冲突",
@@ -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=20260726-task-progress-ring-trace-lazy-load">
13
+ <link rel="stylesheet" href="/styles.css?v=20260726-settings-select-spacing-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -159,7 +159,10 @@
159
159
  <section id="platform-ai-view" class="shelf-view hidden" aria-labelledby="platform-ai-title">
160
160
  <div class="shelf-header">
161
161
  <div><span class="eyebrow">平台资源</span><h1 id="platform-ai-title">AI 管理</h1><p>供应商、模型与全局系统提示词对所有作品生效;每本书可在“AI 设置”中追加自己的系统提示词。</p></div>
162
- <button id="platform-new-provider" class="primary-button" type="button">新建供应商</button>
162
+ <div class="settings-detail-actions">
163
+ <button id="platform-ai-return" class="ghost-button settings-parent-button" type="button">返回设置</button>
164
+ <button id="platform-new-provider" class="primary-button" type="button">新建供应商</button>
165
+ </div>
163
166
  </div>
164
167
  <div id="platform-ai-content" class="module-content"></div>
165
168
  </section>
@@ -497,7 +500,10 @@
497
500
  <form id="appearance-form" method="dialog">
498
501
  <div class="dialog-header">
499
502
  <div><span class="eyebrow">本机偏好</span><h2>显示设置</h2></div>
500
- <button class="dialog-close" value="cancel" aria-label="关闭" type="submit">×</button>
503
+ <div class="settings-dialog-header-actions">
504
+ <button id="appearance-settings-return" class="ghost-button settings-parent-button" type="button">返回设置</button>
505
+ <button class="dialog-close" value="cancel" aria-label="关闭" type="submit">×</button>
506
+ </div>
501
507
  </div>
502
508
  <div class="appearance-grid">
503
509
  <label>中文字体
@@ -572,14 +578,14 @@
572
578
  </dialog>
573
579
 
574
580
  <dialog id="users-dialog" class="dialog wide-dialog" aria-labelledby="users-dialog-title">
575
- <div class="dialog-header"><div><span class="eyebrow">系统管理员</span><h2 id="users-dialog-title">用户管理</h2></div><button id="users-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div>
581
+ <div class="dialog-header"><div><span class="eyebrow">系统管理员</span><h2 id="users-dialog-title">用户管理</h2></div><div class="settings-dialog-header-actions"><button id="users-settings-return" class="ghost-button settings-parent-button" type="button">返回设置</button><button id="users-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div></div>
576
582
  <div class="access-dialog-body">
577
583
  <div id="users-list" class="access-list"></div>
578
584
  </div>
579
585
  </dialog>
580
586
 
581
587
  <dialog id="platform-ui-settings-dialog" class="dialog" aria-labelledby="platform-ui-settings-title">
582
- <div class="dialog-header"><div><span class="eyebrow">系统管理员</span><h2 id="platform-ui-settings-title">界面与分页</h2></div><button id="platform-ui-settings-close" class="dialog-close" aria-label="关闭" type="button">×</button></div>
588
+ <div class="dialog-header"><div><span class="eyebrow">系统管理员</span><h2 id="platform-ui-settings-title">界面与分页</h2></div><div class="settings-dialog-header-actions"><button id="platform-ui-settings-return" class="ghost-button settings-parent-button" type="button">返回设置</button><button id="platform-ui-settings-close" class="dialog-close" aria-label="关闭" type="button">×</button></div></div>
583
589
  <form id="platform-ui-settings-form">
584
590
  <div class="dialog-fields">
585
591
  <label>Toast 提示位置<select id="toast-position" name="toastPosition" aria-label="Toast 提示位置"><option value="bottom-right">右下角(默认)</option><option value="top-right">右上角</option></select></label>
@@ -660,7 +666,7 @@
660
666
  </dialog>
661
667
 
662
668
  <dialog id="members-dialog" class="dialog wide-dialog" aria-labelledby="members-dialog-title">
663
- <div class="dialog-header"><div><span id="members-dialog-eyebrow" class="eyebrow">作品权限</span><h2 id="members-dialog-title">成员模块权限</h2></div><button id="members-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div>
669
+ <div class="dialog-header"><div><span id="members-dialog-eyebrow" class="eyebrow">作品权限</span><h2 id="members-dialog-title">成员模块权限</h2></div><div class="settings-dialog-header-actions"><button id="members-settings-return" class="ghost-button settings-parent-button hidden" type="button">返回设置</button><button id="members-dialog-close" class="dialog-close" aria-label="关闭" type="button">×</button></div></div>
664
670
  <div class="access-dialog-body">
665
671
  <form id="member-permission-form" class="member-permission-form">
666
672
  <label class="member-person-field">选择成员<select id="member-user-select" required></select><small>先选择已注册用户,再逐项配置模块权限。</small></label>
@@ -745,6 +751,6 @@
745
751
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
746
752
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
747
753
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
748
- <script type="module" src="/app.js?v=20260726-task-progress-ring-trace-lazy-load"></script>
754
+ <script type="module" src="/app.js?v=20260726-relationship-pinyin-v10"></script>
749
755
  </body>
750
756
  </html>