@musnows/scriverse 0.7.11 → 0.7.12

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.
@@ -26,6 +26,7 @@ import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-ti
26
26
  import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260812-context-usage-remaining-v2";
27
27
  import { formatAiToolCallResult } from "/ai-tool-call.js?v=20260801-ai-tool-result-chars-v1";
28
28
  import { copyAiRawMarkdown } from "/ai-message-actions.js?v=20260713-copy-raw-markdown";
29
+ import { bindPlainTextPaste } from "/plain-text-paste.js?v=20260815-plain-text-paste-v1";
29
30
  import { THEME_STORAGE_KEY, nextTheme, normalizeTheme, themeToggleLabel } from "/theme.js?v=20260713-dark-mode";
30
31
  import { buildCharacterDetails, buildCharacterState, characterStateEntries, normalizeCharacterDetails, normalizeCharacterSections } from "/character-profile.js?v=20260713-character-editor";
31
32
  import { characterVersionSourceLabel, describeCharacterVersionChanges } from "/character-version.js?v=20260801-entity-lifecycle-v1";
@@ -11821,6 +11822,7 @@ function createVditorEditor(host, value, { onInput = () => {}, uploadAttachment
11821
11822
  const attachmentObserver = new MutationObserver(() => normalizeVditorAttachmentImages(editor));
11822
11823
  attachmentObserver.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ["src"] });
11823
11824
  editor.__attachmentObserver = attachmentObserver;
11825
+ editor.__plainTextPasteCleanup = bindPlainTextPaste(host);
11824
11826
  host.__vditor = editor;
11825
11827
  return editor;
11826
11828
  }
@@ -12012,6 +12014,7 @@ function ensureVditorIconScript() {
12012
12014
  function destroyVditorEditor(editor) {
12013
12015
  if (!editor) return;
12014
12016
  editor.__attachmentObserver?.disconnect();
12017
+ editor.__plainTextPasteCleanup?.();
12015
12018
  editor.__vditorLineNumberObserver?.disconnect();
12016
12019
  editor.__vditorLineNumberResizeObserver?.disconnect();
12017
12020
  if (editor.__vditorLineNumberSurface && editor.__vditorLineNumberScrollHandler) {
@@ -13084,7 +13087,12 @@ function openReviewDialog() {
13084
13087
  }
13085
13088
 
13086
13089
  async function openTaskDialog() {
13087
- const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => [chapter.id, `${volume.title} / ${chapter.title}`]));
13090
+ const volumeOptions = state.work.volumes.map((volume) => ({ id: String(volume.id), title: String(volume.title) }));
13091
+ const chapterOptions = state.work.volumes.flatMap((volume) => volume.chapters.map((chapter) => ({
13092
+ id: String(chapter.id),
13093
+ title: String(chapter.title),
13094
+ volumeTitle: String(volume.title)
13095
+ })));
13088
13096
  let relationshipCharacters = [];
13089
13097
  let taskModels = [];
13090
13098
  let taskDefaults = [];
@@ -13100,6 +13108,23 @@ async function openTaskDialog() {
13100
13108
  toast(`分析任务配置加载失败:${error.message}`, "error");
13101
13109
  return;
13102
13110
  }
13111
+ const taskScopePicker = (kind, label, options, emptyLabel) => `<div class="form-field task-scope-field task-scope-${kind}-field is-disabled" data-task-scope-field="${kind}" aria-disabled="true">
13112
+ <span id="task-${kind}-label">${esc(label)}(可多选)</span>
13113
+ <div class="task-scope-picker">
13114
+ <button class="task-scope-trigger" type="button" data-task-scope-trigger="${kind}" aria-expanded="false" aria-controls="task-${kind}-bubble" aria-labelledby="task-${kind}-label task-${kind}-summary" disabled>
13115
+ <span id="task-${kind}-summary">${esc(emptyLabel)}</span>
13116
+ <span class="task-scope-trigger-meta"><span data-task-scope-count="${kind}">未选择</span><span class="task-scope-chevron" aria-hidden="true">⌄</span></span>
13117
+ </button>
13118
+ <div id="task-${kind}-bubble" class="task-scope-bubble hidden" data-task-scope-bubble="${kind}">
13119
+ <label class="task-scope-search">筛选${esc(label)}<input type="search" data-task-scope-search="${kind}" placeholder="输入名称" autocomplete="off"></label>
13120
+ <div class="task-scope-bubble-meta"><span>共 ${options.length} ${kind === "chapter" ? "章" : "卷"}</span><button type="button" data-task-scope-clear="${kind}" disabled>清空选择</button></div>
13121
+ <div class="task-scope-options" role="group" aria-labelledby="task-${kind}-label">
13122
+ ${options.map((item) => `<label class="task-scope-option" data-task-scope-search-name="${esc(`${item.title} ${item.volumeTitle ?? ""}`.toLocaleLowerCase())}"><input type="checkbox" name="${kind === "chapter" ? "chapterIds" : "volumeIds"}" value="${esc(item.id)}" data-task-scope-input="${kind}"><span><strong>${esc(item.title)}</strong>${item.volumeTitle ? `<small>${esc(item.volumeTitle)}</small>` : ""}</span></label>`).join("")}
13123
+ </div>
13124
+ <p class="task-scope-empty hidden" data-task-scope-empty="${kind}">没有匹配的${esc(label)}</p>
13125
+ </div>
13126
+ </div>
13127
+ </div>`;
13103
13128
  const defaultModelByTask = new Map(taskDefaults.map((item) => [item.taskType, item.model.id]));
13104
13129
  const availableTaskModels = taskModels.filter((model) => isSelectableModel(model));
13105
13130
  const characterOptions = relationshipCharacters.map((character) => [character.id, character.name]);
@@ -13126,8 +13151,9 @@ async function openTaskDialog() {
13126
13151
  const modelField = `<label>任务模型<select name="modelId" required aria-describedby="analysis-task-model-help">
13127
13152
  <option value="" ${availableTaskModels.some((model) => model.id === defaultModelId) ? "" : "selected"} disabled>${availableTaskModels.length ? "请选择模型" : "没有可用模型"}</option>
13128
13153
  ${availableTaskModels.map((model) => `<option value="${esc(model.id)}" ${model.id === defaultModelId ? "selected" : ""}>${esc(modelOptionLabel(model))}</option>`).join("")}
13129
- </select><small id="analysis-task-model-help">默认值来自“本书 AI 设置”,只修改当前任务,不会改变全书默认模型。</small></label>`;
13130
- 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>`;
13154
+ </select><small id="analysis-task-model-help">默认值来自“本书 AI 设置”,只修改当前任务,不会改变全书默认模型。</small><p class="analysis-context-warning hidden" data-analysis-context-warning role="alert"></p></label>`;
13155
+ const chapterField = taskScopePicker("chapter", "章节", chapterOptions, "选择需要分析的章节");
13156
+ const volumeField = taskScopePicker("volume", "分卷", volumeOptions, "选择需要分析的分卷");
13131
13157
  const relationshipFields = `<div class="relationship-analysis-options hidden">
13132
13158
  ${relationshipCharacterPicker}
13133
13159
  <p class="relationship-analysis-helper"><span aria-hidden="true">i</span><span>留空时使用基础关系抽取;选中角色后,将汇总其跨章节证据再进行全局关系归纳。默认仅追加不存在的关系,不修改或删除已有关系。</span></p>
@@ -13164,25 +13190,43 @@ async function openTaskDialog() {
13164
13190
  const preFilterRelationshipSources = characterIds.length > 0 && form.get("preFilterRelationshipSources") === "on";
13165
13191
  const previewRelationshipChanges = taskType === "relationship-analysis" && form.get("previewRelationshipChanges") === "on";
13166
13192
  const replaceExistingRelationships = characterIds.length > 0 && form.get("replaceExistingRelationships") === "on";
13193
+ const chapterIds = form.getAll("chapterIds").map(String).filter(Boolean);
13194
+ const volumeIds = form.getAll("volumeIds").map(String).filter(Boolean);
13195
+ const chapterScope = {
13196
+ type: "chapter",
13197
+ ...(chapterIds[0] ? { chapterId: chapterIds[0] } : {}),
13198
+ ...(chapterIds.length ? { chapterIds } : {})
13199
+ };
13200
+ const volumeScope = {
13201
+ type: "volume",
13202
+ ...(volumeIds[0] ? { volumeId: volumeIds[0] } : {}),
13203
+ ...(volumeIds.length ? { volumeIds } : {})
13204
+ };
13167
13205
  const scope = settingsOnly
13168
13206
  ? { type: "settings", ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
13169
- : taskType === "character-identity-audit" || scopeType === "book" || includeAllSettings
13207
+ : scopeType === "book" || includeAllSettings
13170
13208
  ? { type: "book", ...(includeAllSettings ? { includeAllSettings: true } : {}), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) }
13171
- : { type: "chapter", chapterId: form.get("chapterId"), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
13209
+ : { ...(scopeType === "volume" ? volumeScope : chapterScope), ...(additionalPrompt ? { additionalPrompt } : {}), ...(characterIds.length ? { characterIds, preFilterRelationshipSources } : {}), ...(previewRelationshipChanges ? { previewRelationshipChanges: true } : {}), ...(replaceExistingRelationships ? { replaceExistingRelationships: true } : {}) };
13172
13210
  return scope;
13173
13211
  };
13174
13212
  const relationshipPreviewKey = (scope, modelId) => JSON.stringify({
13175
13213
  type: scope.type,
13176
13214
  chapterId: scope.chapterId ?? null,
13215
+ chapterIds: scope.chapterIds ?? [],
13216
+ volumeId: scope.volumeId ?? null,
13217
+ volumeIds: scope.volumeIds ?? [],
13177
13218
  includeAllSettings: scope.includeAllSettings === true,
13178
13219
  characterIds: scope.characterIds ?? [],
13179
13220
  preFilterRelationshipSources: scope.preFilterRelationshipSources !== false,
13180
13221
  modelId
13181
13222
  });
13182
- openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["book", "全书"]]) + chapterField + relationshipFields, async (form) => {
13223
+ openDialog("开始 AI 分析", taskTypeField + modelField + field("scopeType", "分析范围", "select", "chapter", [["chapter", "指定章节"], ["volume", "指定分卷"], ["book", "全书"]]) + chapterField + volumeField + relationshipFields, async (form) => {
13224
+ const workId = state.work.id;
13183
13225
  const taskType = String(form.get("taskType"));
13184
13226
  const modelId = String(form.get("modelId"));
13185
13227
  const scope = buildRelationshipScope(form);
13228
+ if (scope.type === "chapter" && !scope.chapterIds?.length) return toast("请先选择至少一个章节", "error");
13229
+ if (scope.type === "volume" && !scope.volumeIds?.length) return toast("请先选择至少一个分卷", "error");
13186
13230
  if (taskType === "relationship-analysis" && relationshipSourcePreview
13187
13231
  && relationshipSourcePreviewConfigKey === relationshipPreviewKey(scope, modelId)) {
13188
13232
  scope.relationshipSourceRefs = [...$("#dialog-fields").querySelectorAll("[data-relationship-source-selected]:checked")]
@@ -13192,15 +13236,19 @@ async function openTaskDialog() {
13192
13236
  sourceVersion: input.dataset.sourceVersion
13193
13237
  }));
13194
13238
  }
13195
- await api(`/api/works/${state.work.id}/tasks`, { method: "POST", body: { taskType, scope, modelId } });
13196
- await refreshBackgroundTaskCenter({ announce: false });
13197
- taskListPage = 1;
13239
+ clearContextWarning();
13198
13240
  try {
13199
- await renderTasks(1);
13200
- toast("分析任务已创建,已进入任务队列");
13241
+ await api(`/api/works/${workId}/tasks`, { method: "POST", body: { taskType, scope, modelId } });
13201
13242
  } catch (error) {
13202
- toast(`任务已创建,但列表刷新失败:${error.message}`, "error");
13243
+ if (error.code === "AI_CONTEXT_TOO_LARGE") {
13244
+ contextWarning.textContent = error.message || "当前分析范围超过模型上下文阈值,请切换到上下文更长的模型后重试。";
13245
+ contextWarning.classList.remove("hidden");
13246
+ }
13247
+ throw error;
13203
13248
  }
13249
+ taskListPage = 1;
13250
+ toast("分析任务已创建,已进入任务队列");
13251
+ void refreshAnalysisTaskViewsAfterCreate(workId);
13204
13252
  }, "AI 分析", {
13205
13253
  submitLabel: "创建任务",
13206
13254
  pendingLabel: "创建中…",
@@ -13210,8 +13258,42 @@ async function openTaskDialog() {
13210
13258
  const taskTypeSelect = $("#dialog-fields").querySelector('select[name="taskType"]');
13211
13259
  const taskModelSelect = $("#dialog-fields").querySelector('select[name="modelId"]');
13212
13260
  const scopeTypeSelect = $("#dialog-fields").querySelector('select[name="scopeType"]');
13213
- const chapterSelect = $("#dialog-fields").querySelector('select[name="chapterId"]');
13214
- const chapterFieldElement = chapterSelect.closest(".task-chapter-field");
13261
+ const scopeFields = {
13262
+ chapter: $("#dialog-fields").querySelector('[data-task-scope-field="chapter"]'),
13263
+ volume: $("#dialog-fields").querySelector('[data-task-scope-field="volume"]')
13264
+ };
13265
+ const scopeTriggers = {
13266
+ chapter: $("#dialog-fields").querySelector('[data-task-scope-trigger="chapter"]'),
13267
+ volume: $("#dialog-fields").querySelector('[data-task-scope-trigger="volume"]')
13268
+ };
13269
+ const scopeBubbles = {
13270
+ chapter: $("#dialog-fields").querySelector('[data-task-scope-bubble="chapter"]'),
13271
+ volume: $("#dialog-fields").querySelector('[data-task-scope-bubble="volume"]')
13272
+ };
13273
+ const scopeSearches = {
13274
+ chapter: $("#dialog-fields").querySelector('[data-task-scope-search="chapter"]'),
13275
+ volume: $("#dialog-fields").querySelector('[data-task-scope-search="volume"]')
13276
+ };
13277
+ const scopeInputs = {
13278
+ chapter: [...$("#dialog-fields").querySelectorAll('[data-task-scope-input="chapter"]')],
13279
+ volume: [...$("#dialog-fields").querySelectorAll('[data-task-scope-input="volume"]')]
13280
+ };
13281
+ const scopeSummaries = {
13282
+ chapter: $("#dialog-fields").querySelector("#task-chapter-summary"),
13283
+ volume: $("#dialog-fields").querySelector("#task-volume-summary")
13284
+ };
13285
+ const scopeCounts = {
13286
+ chapter: $("#dialog-fields").querySelector('[data-task-scope-count="chapter"]'),
13287
+ volume: $("#dialog-fields").querySelector('[data-task-scope-count="volume"]')
13288
+ };
13289
+ const scopeClearButtons = {
13290
+ chapter: $("#dialog-fields").querySelector('[data-task-scope-clear="chapter"]'),
13291
+ volume: $("#dialog-fields").querySelector('[data-task-scope-clear="volume"]')
13292
+ };
13293
+ const scopeEmptyMessages = {
13294
+ chapter: $("#dialog-fields").querySelector('[data-task-scope-empty="chapter"]'),
13295
+ volume: $("#dialog-fields").querySelector('[data-task-scope-empty="volume"]')
13296
+ };
13215
13297
  const description = $("#analysis-type-description");
13216
13298
  const relationshipOptions = $("#dialog-fields").querySelector(".relationship-analysis-options");
13217
13299
  const relationshipPrompt = relationshipOptions.querySelector('textarea[name="additionalPrompt"]');
@@ -13229,17 +13311,57 @@ async function openTaskDialog() {
13229
13311
  const relationshipSourcePreviewContent = relationshipOptions.querySelector("[data-relationship-source-preview-content]");
13230
13312
  const replaceRelationships = relationshipOptions.querySelector('input[name="replaceExistingRelationships"]');
13231
13313
  const relationshipOverwriteCard = relationshipOptions.querySelector(".relationship-existing-overwrite-card");
13314
+ const contextWarning = $("#dialog-fields").querySelector("[data-analysis-context-warning]");
13315
+ const clearContextWarning = () => {
13316
+ contextWarning.textContent = "";
13317
+ contextWarning.classList.add("hidden");
13318
+ };
13232
13319
  const allSettingsOption = document.createElement("option");
13233
13320
  allSettingsOption.value = "book-with-settings";
13234
13321
  allSettingsOption.textContent = "全书 + 设定集";
13235
13322
  const settingsOnlyOption = document.createElement("option");
13236
13323
  settingsOnlyOption.value = "settings";
13237
13324
  settingsOnlyOption.textContent = "仅设定集";
13325
+ const setTaskScopeBubbleOpen = (kind, open) => {
13326
+ scopeBubbles[kind].classList.toggle("hidden", !open);
13327
+ scopeTriggers[kind].setAttribute("aria-expanded", String(open));
13328
+ if (open) scopeSearches[kind].focus();
13329
+ };
13330
+ const syncTaskScopePicker = (kind) => {
13331
+ const selected = scopeInputs[kind].filter((input) => input.checked);
13332
+ const selectedNames = selected.map((input) => input.closest(".task-scope-option")?.querySelector("strong")?.textContent ?? "");
13333
+ const label = kind === "chapter" ? "选择需要分析的章节" : "选择需要分析的分卷";
13334
+ scopeSummaries[kind].textContent = selectedNames.length
13335
+ ? `${selectedNames.slice(0, 2).join("、")}${selectedNames.length > 2 ? ` 等 ${selectedNames.length} 个` : ""}`
13336
+ : label;
13337
+ scopeCounts[kind].textContent = selected.length ? `已选 ${selected.length}` : "未选择";
13338
+ scopeClearButtons[kind].disabled = selected.length === 0;
13339
+ scopeTriggers[kind].setAttribute("aria-label", `筛选${kind === "chapter" ? "章节" : "分卷"},已选择 ${selected.length} 个`);
13340
+ };
13341
+ const filterTaskScopeOptions = (kind) => {
13342
+ const query = scopeSearches[kind].value.trim().toLocaleLowerCase();
13343
+ let visibleCount = 0;
13344
+ for (const input of scopeInputs[kind]) {
13345
+ const option = input.closest(".task-scope-option");
13346
+ const visible = !query || option.dataset.taskScopeSearchName.includes(query);
13347
+ option.classList.toggle("hidden", !visible);
13348
+ if (visible) visibleCount += 1;
13349
+ }
13350
+ scopeEmptyMessages[kind].classList.toggle("hidden", visibleCount > 0);
13351
+ };
13238
13352
  const syncChapterField = () => {
13239
- const disabled = scopeTypeSelect.value !== "chapter";
13240
- chapterSelect.disabled = disabled;
13241
- chapterFieldElement.classList.toggle("is-disabled", disabled);
13242
- chapterFieldElement.setAttribute("aria-disabled", String(disabled));
13353
+ const activeKind = ["chapter", "volume"].includes(scopeTypeSelect.value) ? scopeTypeSelect.value : null;
13354
+ for (const kind of ["chapter", "volume"]) {
13355
+ const enabled = kind === activeKind;
13356
+ scopeFields[kind].classList.toggle("is-disabled", !enabled);
13357
+ scopeFields[kind].classList.toggle("hidden", !enabled);
13358
+ scopeFields[kind].setAttribute("aria-disabled", String(!enabled));
13359
+ scopeTriggers[kind].disabled = !enabled;
13360
+ for (const input of scopeInputs[kind]) input.disabled = !enabled;
13361
+ if (!enabled) setTaskScopeBubbleOpen(kind, false);
13362
+ syncTaskScopePicker(kind);
13363
+ filterTaskScopeOptions(kind);
13364
+ }
13243
13365
  };
13244
13366
  const setRelationshipCharacterBubbleOpen = (open) => {
13245
13367
  relationshipCharacterBubble.classList.toggle("hidden", !open);
@@ -13337,10 +13459,12 @@ async function openTaskDialog() {
13337
13459
  taskTypeSelect.addEventListener("change", () => {
13338
13460
  description.textContent = analysisTypeDescription(taskTypeSelect.value);
13339
13461
  syncTaskModelDefault();
13462
+ clearContextWarning();
13340
13463
  invalidateRelationshipSourcePreview();
13341
13464
  syncRelationshipOptions();
13342
13465
  });
13343
13466
  taskModelSelect.addEventListener("change", () => {
13467
+ clearContextWarning();
13344
13468
  invalidateRelationshipSourcePreview();
13345
13469
  syncRelationshipOptions();
13346
13470
  });
@@ -13387,11 +13511,39 @@ async function openTaskDialog() {
13387
13511
  syncRelationshipOptions();
13388
13512
  }
13389
13513
  });
13514
+ for (const kind of ["chapter", "volume"]) {
13515
+ scopeTriggers[kind].addEventListener("click", () => {
13516
+ setTaskScopeBubbleOpen(kind, scopeTriggers[kind].getAttribute("aria-expanded") !== "true");
13517
+ });
13518
+ scopeSearches[kind].addEventListener("input", () => filterTaskScopeOptions(kind));
13519
+ for (const input of scopeInputs[kind]) input.addEventListener("change", () => {
13520
+ syncTaskScopePicker(kind);
13521
+ clearContextWarning();
13522
+ invalidateRelationshipSourcePreview();
13523
+ });
13524
+ scopeClearButtons[kind].addEventListener("click", () => {
13525
+ for (const input of scopeInputs[kind]) input.checked = false;
13526
+ syncTaskScopePicker(kind);
13527
+ clearContextWarning();
13528
+ invalidateRelationshipSourcePreview();
13529
+ });
13530
+ }
13390
13531
  $("#dynamic-form").onclick = (event) => {
13391
13532
  if (!relationshipCharacterPickerElement.contains(event.target)) setRelationshipCharacterBubbleOpen(false);
13533
+ for (const kind of ["chapter", "volume"]) {
13534
+ if (!scopeFields[kind].contains(event.target)) setTaskScopeBubbleOpen(kind, false);
13535
+ }
13392
13536
  };
13393
13537
  $("#dynamic-form").onkeydown = (event) => {
13394
- if (event.key !== "Escape" || relationshipCharacterBubble.classList.contains("hidden")) return;
13538
+ if (event.key !== "Escape") return;
13539
+ const openScope = ["chapter", "volume"].find((kind) => !scopeBubbles[kind].classList.contains("hidden"));
13540
+ if (openScope) {
13541
+ event.preventDefault();
13542
+ setTaskScopeBubbleOpen(openScope, false);
13543
+ scopeTriggers[openScope].focus();
13544
+ return;
13545
+ }
13546
+ if (relationshipCharacterBubble.classList.contains("hidden")) return;
13395
13547
  event.preventDefault();
13396
13548
  setRelationshipCharacterBubbleOpen(false);
13397
13549
  relationshipCharacterTrigger.focus();
@@ -13400,10 +13552,23 @@ async function openTaskDialog() {
13400
13552
  invalidateRelationshipSourcePreview();
13401
13553
  syncChapterField();
13402
13554
  });
13403
- chapterSelect.addEventListener("change", invalidateRelationshipSourcePreview);
13404
13555
  syncRelationshipOptions();
13405
13556
  }
13406
13557
 
13558
+ async function refreshAnalysisTaskViewsAfterCreate(workId) {
13559
+ try {
13560
+ await Promise.all([
13561
+ refreshBackgroundTaskCenter({ announce: false }),
13562
+ state.module === "tasks" && state.work?.id === workId
13563
+ ? renderTasks(1, { refresh: true })
13564
+ : Promise.resolve()
13565
+ ]);
13566
+ } catch (error) {
13567
+ console.error("Failed to refresh analysis task views after creation", error);
13568
+ toast(`任务已创建,但列表刷新失败:${error.message}`, "error");
13569
+ }
13570
+ }
13571
+
13407
13572
  function openProviderDialog(item) {
13408
13573
  const protocol = item?.protocol ?? "openai-chat-completions";
13409
13574
  const providerProtocolOptions = [
@@ -15605,6 +15770,7 @@ $("#module-nav").addEventListener("click", (event) => {
15605
15770
  });
15606
15771
  $("#module-more-button").addEventListener("click", () => setModuleNavExpanded(!moduleNavExpanded));
15607
15772
  $("#module-create-button").addEventListener("click", () => ({ drafts: openDraftDialog, settings: openSettingEditor, characters: openCharacterEditor, races: openRaceDialog, organizations: openOrganizationDialog, timeline: openTimelineDialog, outlines: openForeshadowDialog, relationships: openRelationshipDialog, reviews: openReviewDialog, tasks: openTaskDialog })[state.module]?.());
15773
+ bindPlainTextPaste($("#ai-prompt"));
15608
15774
  $("#ai-prompt").addEventListener("input", async () => {
15609
15775
  updateAiMentionMenu();
15610
15776
  setAiContextMeter(null);
@@ -1164,6 +1164,6 @@
1164
1164
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1165
1165
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1166
1166
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1167
- <script type="module" src="/app.js?v=20260815-ai-history-favorite-v1"></script>
1167
+ <script type="module" src="/app.js?v=20260815-analysis-task-create-v1"></script>
1168
1168
  </body>
1169
1169
  </html>
@@ -0,0 +1,89 @@
1
+ function normalizePlainText(value) {
2
+ return String(value ?? "").replace(/\r\n?/gu, "\n");
3
+ }
4
+
5
+ export function readClipboardPlainText(clipboardData) {
6
+ if (!clipboardData || typeof clipboardData.getData !== "function") return "";
7
+ try {
8
+ return normalizePlainText(clipboardData.getData("text/plain"));
9
+ } catch {
10
+ return "";
11
+ }
12
+ }
13
+
14
+ function isInside(root, node) {
15
+ return Boolean(node) && (node === root || root.contains(node));
16
+ }
17
+
18
+ function insertIntoContentEditable(target, text, documentRef, windowRef) {
19
+ const selection = windowRef?.getSelection?.();
20
+ const currentRange = selection?.rangeCount ? selection.getRangeAt(0) : null;
21
+ const range = currentRange && isInside(target, currentRange.commonAncestorContainer)
22
+ ? currentRange.cloneRange()
23
+ : documentRef.createRange();
24
+ if (!currentRange || !isInside(target, currentRange.commonAncestorContainer)) {
25
+ target.focus?.();
26
+ range.selectNodeContents(target);
27
+ range.collapse(false);
28
+ }
29
+ range.deleteContents();
30
+ const fragment = documentRef.createDocumentFragment();
31
+ normalizePlainText(text).split("\n").forEach((line, index) => {
32
+ if (index > 0) fragment.append(documentRef.createElement("br"));
33
+ if (line) fragment.append(documentRef.createTextNode(line));
34
+ });
35
+ range.insertNode(fragment);
36
+ range.collapse(false);
37
+ if (selection) {
38
+ selection.removeAllRanges();
39
+ selection.addRange(range);
40
+ }
41
+ return true;
42
+ }
43
+
44
+ function insertIntoTextArea(target, text) {
45
+ const value = String(target.value ?? "");
46
+ const start = Number.isInteger(target.selectionStart) ? target.selectionStart : value.length;
47
+ const end = Number.isInteger(target.selectionEnd) ? target.selectionEnd : start;
48
+ target.value = `${value.slice(0, start)}${normalizePlainText(text)}${value.slice(end)}`;
49
+ target.setSelectionRange?.(start + normalizePlainText(text).length, start + normalizePlainText(text).length);
50
+ return true;
51
+ }
52
+
53
+ function dispatchInput(target, text, windowRef) {
54
+ const InputEventConstructor = windowRef?.InputEvent;
55
+ const EventConstructor = windowRef?.Event;
56
+ if (typeof InputEventConstructor === "function") {
57
+ target.dispatchEvent(new InputEventConstructor("input", { bubbles: true, inputType: "insertFromPaste", data: text }));
58
+ } else if (typeof EventConstructor === "function") {
59
+ target.dispatchEvent(new EventConstructor("input", { bubbles: true }));
60
+ }
61
+ }
62
+
63
+ export function insertClipboardPlainText(target, text, documentRef = globalThis.document, windowRef = globalThis.window) {
64
+ if (!target) return false;
65
+ if (target.tagName === "TEXTAREA") return insertIntoTextArea(target, text);
66
+ if (target.getAttribute?.("contenteditable") === "true") return insertIntoContentEditable(target, text, documentRef, windowRef);
67
+ return false;
68
+ }
69
+
70
+ function pasteTarget(event, root) {
71
+ let target = event.target;
72
+ if (target?.nodeType !== 1) target = target?.parentElement;
73
+ const editable = target?.closest?.("textarea, [contenteditable=\"true\"]");
74
+ return editable && isInside(root, editable) ? editable : null;
75
+ }
76
+
77
+ export function bindPlainTextPaste(root, documentRef = globalThis.document, windowRef = globalThis.window) {
78
+ if (!root) return () => {};
79
+ const handler = (event) => {
80
+ const target = pasteTarget(event, root);
81
+ if (!target) return;
82
+ const text = readClipboardPlainText(event.clipboardData);
83
+ event.preventDefault();
84
+ event.stopImmediatePropagation();
85
+ if (insertClipboardPlainText(target, text, documentRef, windowRef)) dispatchInput(target, text, windowRef);
86
+ };
87
+ root.addEventListener("paste", handler, true);
88
+ return () => root.removeEventListener("paste", handler, true);
89
+ }
@@ -2878,6 +2878,33 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2878
2878
  .task-chapter-field { transition: opacity .15s ease; }
2879
2879
  .task-chapter-field.is-disabled { opacity: .48; }
2880
2880
  .task-chapter-field select:disabled { cursor: not-allowed; }
2881
+ .task-scope-field { min-width: 0; transition: opacity .15s ease; }
2882
+ .task-scope-field.is-disabled { opacity: .48; }
2883
+ .task-scope-picker { display: grid; gap: 8px; min-width: 0; }
2884
+ .dialog-fields .task-scope-trigger { display: flex; width: 100%; min-height: 42px; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 11px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 12px; text-align: left; }
2885
+ .task-scope-trigger:hover, .task-scope-trigger:focus-visible, .task-scope-trigger[aria-expanded="true"] { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 12%, transparent); outline: none; }
2886
+ .task-scope-trigger:disabled { cursor: not-allowed; opacity: .48; }
2887
+ .task-scope-trigger > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2888
+ .task-scope-trigger-meta { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 8px; color: var(--muted); font-family: var(--font-latin), monospace; font-size: 10px; }
2889
+ .task-scope-chevron { font-size: 15px; line-height: 1; transition: transform .15s ease; }
2890
+ .task-scope-trigger[aria-expanded="true"] .task-scope-chevron { transform: rotate(180deg); }
2891
+ .task-scope-bubble { position: relative; display: grid; gap: 9px; padding: 12px; border: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line)); border-radius: 8px; background: var(--surface-soft); box-shadow: 0 10px 30px rgba(48,39,31,.12); }
2892
+ .task-scope-bubble::before { position: absolute; top: -6px; right: 20px; width: 10px; height: 10px; border-top: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line)); border-left: 1px solid color-mix(in srgb, var(--accent) 36%, var(--line)); background: var(--surface-soft); content: ""; transform: rotate(45deg); }
2893
+ .dialog-fields .task-scope-search { gap: 5px; }
2894
+ .dialog-fields .task-scope-search input { min-height: 36px; padding: 7px 9px; background: var(--surface); }
2895
+ .task-scope-bubble-meta { display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--muted); font-size: 9px; }
2896
+ .task-scope-bubble-meta button { padding: 2px 0; border: 0; background: transparent; color: var(--accent-dark); font-size: 9px; }
2897
+ .task-scope-bubble-meta button:disabled { color: var(--muted); cursor: default; opacity: .45; }
2898
+ .task-scope-options { display: grid; gap: 6px; max-height: 220px; overflow-y: auto; padding-right: 3px; }
2899
+ .dialog-fields .task-scope-option { display: grid; grid-template-columns: 16px minmax(0, 1fr); align-items: center; gap: 8px; min-width: 0; padding: 8px 9px; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--muted); cursor: pointer; }
2900
+ .dialog-fields .task-scope-option.hidden { display: none; }
2901
+ .dialog-fields .task-scope-option input { width: 15px; height: 15px; margin: 0; }
2902
+ .task-scope-option > span { display: grid; gap: 2px; min-width: 0; }
2903
+ .task-scope-option strong { overflow: hidden; color: var(--ink); font-size: 10px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
2904
+ .task-scope-option small { overflow: hidden; color: var(--muted); font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
2905
+ .task-scope-option:has(input:checked) { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); background: color-mix(in srgb, var(--accent) 7%, var(--surface)); }
2906
+ .task-scope-option input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
2907
+ .task-scope-empty { margin: 0; padding: 18px 8px; color: var(--muted); font-size: 10px; text-align: center; }
2881
2908
  .relationship-analysis-options { display: grid; gap: 14px; }
2882
2909
  .relationship-character-field { min-width: 0; }
2883
2910
  .relationship-character-picker { display: grid; gap: 8px; min-width: 0; }
@@ -2927,6 +2954,7 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
2927
2954
  .relationship-source-preview-match { padding: 2px 6px; border-radius: 10px; background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--accent-dark); font-size: 8px; white-space: nowrap; }
2928
2955
  .relationship-source-preview-match.is-fuzzy { background: color-mix(in srgb, var(--warning, #a56f19) 12%, transparent); color: var(--warning, #8b5c10); }
2929
2956
  .relationship-source-preview-empty { margin: 0; padding: 9px; border: 1px dashed var(--line); border-radius: 4px; }
2957
+ .analysis-context-warning { margin: 7px 0 0; padding: 8px 10px; border: 1px solid color-mix(in srgb, var(--warning, #a56f19) 45%, var(--line)); border-radius: 4px; background: color-mix(in srgb, var(--warning, #a56f19) 9%, transparent); color: var(--warning, #8b5c10); font-size: 10px; line-height: 1.55; }
2930
2958
  .model-temperature-hint, .model-context-window-hint { margin: 0; color: var(--accent-dark); font-size: 10px; line-height: 1.55; }
2931
2959
  .item-list-rows { display: grid; gap: 7px; }
2932
2960
  .item-list-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 7px; }