@musnows/scriverse 0.9.6 → 0.9.8

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.
@@ -0,0 +1,42 @@
1
+ export const AI_SKILL_COMMAND_OPTIONS = Object.freeze([
2
+ Object.freeze({
3
+ name: "continue-writing",
4
+ label: "续写正文",
5
+ description: "延续当前章节,并保持情节、人物与设定一致。"
6
+ }),
7
+ Object.freeze({
8
+ name: "polish-writing",
9
+ label: "润色选中文本",
10
+ description: "润色当前章节的精确选区,并生成可确认的替换建议。"
11
+ })
12
+ ]);
13
+
14
+ export function findAiSkillCommand(value, cursor = String(value ?? "").length) {
15
+ const text = String(value ?? "");
16
+ const safeCursor = Math.max(0, Math.min(Number(cursor) || 0, text.length));
17
+ const match = text.slice(0, safeCursor).match(/(?:^|\s)(\/([^/\s]*))$/u);
18
+ if (!match) return null;
19
+ return {
20
+ start: safeCursor - match[1].length,
21
+ end: safeCursor,
22
+ query: match[2]
23
+ };
24
+ }
25
+
26
+ export function listAiSkillOptions(query = "") {
27
+ const keyword = String(query).trim().toLocaleLowerCase("zh-CN");
28
+ if (!keyword) return [...AI_SKILL_COMMAND_OPTIONS];
29
+ return AI_SKILL_COMMAND_OPTIONS.filter((skill) => [skill.name, skill.label]
30
+ .some((value) => value.toLocaleLowerCase("zh-CN").includes(keyword)));
31
+ }
32
+
33
+ export function applyAiSkillCommand(value, match, skillName) {
34
+ const text = String(value ?? "");
35
+ const command = `/${String(skillName).trim()}`;
36
+ const separator = /^\s/u.test(text.slice(match.end)) ? "" : " ";
37
+ return {
38
+ text: `${text.slice(0, match.start)}${command}${separator}${text.slice(match.end)}`,
39
+ command,
40
+ cursor: match.start + command.length + separator.length
41
+ };
42
+ }
@@ -1,6 +1,7 @@
1
1
  export type AiUsageCalendarCell = {
2
2
  date: string;
3
3
  totalTokens: number;
4
+ outsideYear: boolean;
4
5
  future: boolean;
5
6
  week: number;
6
7
  weekday: number;
@@ -10,12 +11,14 @@ export type AiUsageCalendarCell = {
10
11
  export function formatTokenCount(value: unknown): string;
11
12
  export function formatCacheHitRate(value: unknown): string;
12
13
  export function formatEstimatedCost(value: unknown): string;
14
+ export function usageCalendarYears(daily: Array<{ date: string; totalTokens: number }> | unknown): number[];
13
15
  export function buildUsageCalendar(
14
16
  daily: Array<{ date: string; totalTokens: number }> | unknown,
15
- today?: Date,
16
- weekCount?: number
17
+ year: number,
18
+ todayDateKey?: string
17
19
  ): {
18
20
  cells: AiUsageCalendarCell[];
19
21
  months: Array<{ week: number; label: string }>;
20
22
  weekCount: number;
23
+ year: number | null;
21
24
  };
@@ -1,19 +1,23 @@
1
- function localDateKey(date) {
2
- const year = date.getFullYear();
3
- const month = String(date.getMonth() + 1).padStart(2, "0");
4
- const day = String(date.getDate()).padStart(2, "0");
5
- return `${year}-${month}-${day}`;
1
+ const usageDateKeyPattern = /^(\d{4})-(\d{2})-(\d{2})$/u;
2
+
3
+ function usageDateFromKey(value) {
4
+ const match = usageDateKeyPattern.exec(String(value));
5
+ if (!match) return null;
6
+ const year = Number(match[1]);
7
+ const month = Number(match[2]);
8
+ const day = Number(match[3]);
9
+ const date = new Date(Date.UTC(year, month - 1, day));
10
+ return year >= 1_000 && date.toISOString().slice(0, 10) === String(value) ? date : null;
6
11
  }
7
12
 
8
- function startOfLocalDay(value) {
9
- const date = value instanceof Date ? new Date(value) : new Date(String(value));
10
- date.setHours(0, 0, 0, 0);
11
- return date;
13
+ function usageDateKey(date) {
14
+ return date.toISOString().slice(0, 10);
12
15
  }
13
16
 
14
17
  export function formatTokenCount(value) {
15
18
  const count = Math.max(0, Math.round(Number(value) || 0));
16
19
  if (count < 10_000) return count.toLocaleString("zh-CN");
20
+ if (count >= 100_000_000) return `${(count / 100_000_000).toFixed(2)}亿`;
17
21
  return new Intl.NumberFormat("zh-CN", {
18
22
  notation: "compact",
19
23
  compactDisplay: "short",
@@ -38,26 +42,43 @@ export function formatEstimatedCost(value) {
38
42
  })}`;
39
43
  }
40
44
 
41
- export function buildUsageCalendar(daily, today = new Date(), weekCount = 53) {
42
- const normalizedToday = startOfLocalDay(today);
43
- const start = new Date(normalizedToday);
44
- start.setDate(start.getDate() - start.getDay() - (Math.max(1, weekCount) - 1) * 7);
45
- const usageByDate = new Map((Array.isArray(daily) ? daily : []).map((item) => [
46
- String(item.date),
47
- Math.max(0, Number(item.totalTokens) || 0)
48
- ]));
45
+ export function usageCalendarYears(daily) {
46
+ return [...new Set((Array.isArray(daily) ? daily : [])
47
+ .filter((item) => usageDateFromKey(item?.date) && Math.max(0, Number(item?.totalTokens) || 0) > 0)
48
+ .map((item) => Number(String(item.date).slice(0, 4))))]
49
+ .sort((left, right) => right - left);
50
+ }
51
+
52
+ export function buildUsageCalendar(daily, year, todayDateKey) {
53
+ const selectedYear = Number.isInteger(Number(year)) ? Number(year) : usageCalendarYears(daily)[0];
54
+ if (!selectedYear || selectedYear < 1_000 || selectedYear > 9_999) {
55
+ return { cells: [], months: [], weekCount: 0, year: null };
56
+ }
57
+ const firstDay = new Date(Date.UTC(selectedYear, 0, 1));
58
+ const lastDay = new Date(Date.UTC(selectedYear, 11, 31));
59
+ const start = new Date(firstDay);
60
+ start.setUTCDate(start.getUTCDate() - start.getUTCDay());
61
+ const end = new Date(lastDay);
62
+ end.setUTCDate(end.getUTCDate() + (6 - end.getUTCDay()));
63
+ const weekCount = Math.floor((end.getTime() - start.getTime()) / (7 * 24 * 60 * 60_000)) + 1;
64
+ const normalizedTodayKey = usageDateFromKey(todayDateKey) ? String(todayDateKey) : null;
65
+ const usageByDate = new Map((Array.isArray(daily) ? daily : [])
66
+ .filter((item) => usageDateFromKey(item?.date)?.getUTCFullYear() === selectedYear)
67
+ .map((item) => [String(item.date), Math.max(0, Number(item.totalTokens) || 0)]));
49
68
  const visibleValues = [];
50
69
  const cells = [];
51
- for (let index = 0; index < Math.max(1, weekCount) * 7; index += 1) {
70
+ for (let index = 0; index < weekCount * 7; index += 1) {
52
71
  const date = new Date(start);
53
- date.setDate(start.getDate() + index);
54
- const dateKey = localDateKey(date);
55
- const totalTokens = usageByDate.get(dateKey) ?? 0;
56
- const future = date > normalizedToday;
57
- if (!future && totalTokens > 0) visibleValues.push(totalTokens);
72
+ date.setUTCDate(start.getUTCDate() + index);
73
+ const dateKey = usageDateKey(date);
74
+ const outsideYear = date.getUTCFullYear() !== selectedYear;
75
+ const totalTokens = outsideYear ? 0 : usageByDate.get(dateKey) ?? 0;
76
+ const future = !outsideYear && normalizedTodayKey !== null && dateKey > normalizedTodayKey;
77
+ if (!outsideYear && !future && totalTokens > 0) visibleValues.push(totalTokens);
58
78
  cells.push({
59
79
  date: dateKey,
60
80
  totalTokens,
81
+ outsideYear,
61
82
  future,
62
83
  week: Math.floor(index / 7),
63
84
  weekday: index % 7,
@@ -66,21 +87,13 @@ export function buildUsageCalendar(daily, today = new Date(), weekCount = 53) {
66
87
  }
67
88
  const maximum = Math.max(0, ...visibleValues);
68
89
  for (const cell of cells) {
69
- cell.level = cell.future || cell.totalTokens <= 0 || maximum <= 0
90
+ cell.level = cell.outsideYear || cell.future || cell.totalTokens <= 0 || maximum <= 0
70
91
  ? 0
71
92
  : Math.max(1, Math.min(4, Math.ceil(Math.sqrt(cell.totalTokens / maximum) * 4)));
72
93
  }
73
- const months = [];
74
- for (let week = 0; week < Math.max(1, weekCount); week += 1) {
75
- const firstDay = cells[week * 7];
76
- const date = startOfLocalDay(`${firstDay.date}T00:00:00`);
77
- const previous = week > 0 ? startOfLocalDay(`${cells[(week - 1) * 7].date}T00:00:00`) : null;
78
- if (week === 0 || previous?.getMonth() !== date.getMonth()) {
79
- months.push({
80
- week,
81
- label: new Intl.DateTimeFormat("zh-CN", { month: "short" }).format(date)
82
- });
83
- }
84
- }
85
- return { cells, months, weekCount: Math.max(1, weekCount) };
94
+ const months = Array.from({ length: 12 }, (_, month) => ({
95
+ week: Math.floor((Date.UTC(selectedYear, month, 1) - start.getTime()) / (7 * 24 * 60 * 60_000)),
96
+ label: `${month + 1}月`
97
+ }));
98
+ return { cells, months, weekCount, year: selectedYear };
86
99
  }
@@ -1,7 +1,8 @@
1
1
  import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate, normalizeGalaxyMotionMode, renderRelationshipMindMap } from "/relationship-graph.js?v=20260817-relationship-canvas-scale-v1&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1";
2
2
  import { formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
3
- import { renderMarkdown } from "/markdown.js?v=20260731-no-external-images-v1";
3
+ import { renderMarkdown } from "/markdown.js?v=20260830-adjacent-blockquotes-v1";
4
4
  import { findAiMention, listAiMentionOptions, mergeAiReferenceScope, userMessageMentionNames } from "/ai-mentions.js?v=20260811-user-message-mentions-v1";
5
+ import { applyAiSkillCommand, findAiSkillCommand, listAiSkillOptions } from "/ai-skill-menu.js?v=20260830-ai-skill-slash-menu-v1";
5
6
  import {
6
7
  emptyRoleplayScenePin,
7
8
  normalizeRoleplayScenePin,
@@ -31,7 +32,7 @@ import { shouldSendAiPrompt } from "/ai-prompt-keyboard.js?v=20260713-enter-to-s
31
32
  import { estimateAiMessageTokens, formatAiMessageMeta } from "/ai-message-meta.js?v=20260814-ai-model-lock-v1";
32
33
  import { createStreamTypewriter, createStreamTypewriterSpeedController } from "/stream-typewriter.js?v=20260818-ai-agent-turn-process-v1";
33
34
  import { assertAiStreamCompleted, readAiEventStream } from "/ai-stream-protocol.js?v=20260812-ai-stream-complete-v1";
34
- import { buildUsageCalendar, formatCacheHitRate, formatEstimatedCost, formatTokenCount } from "/ai-usage.js?v=20260821-ai-usage-pricing-v1";
35
+ import { buildUsageCalendar, formatCacheHitRate, formatEstimatedCost, formatTokenCount, usageCalendarYears } from "/ai-usage.js?v=20260830-ai-usage-year-v1";
35
36
  import { formatAiMessageTime } from "/ai-message-time.js?v=20260801-month-day-time";
36
37
  import { formatAiContextUsagePercent, formatAiContextUsageTooltip, mergeAiContextUsage, normalizeAiContextTokenDistribution, resolveAiContextUsage } from "/ai-context-meter.js?v=20260828-context-output-usage-v1";
37
38
  import { isPhoneClient } from "/phone-client.js?v=20260819-phone-client-v1";
@@ -1526,6 +1527,7 @@ let moduleNavExpanded = false;
1526
1527
  const chapterAutoSaveDelay = 800;
1527
1528
  const chapterLineInputRenderDelay = 32;
1528
1529
  let aiMentionMatch = null;
1530
+ let aiSkillMatch = null;
1529
1531
  let aiMentionRange = null;
1530
1532
  let aiMentionActiveIndex = -1;
1531
1533
  let settingsReturnContext = null;
@@ -2705,7 +2707,7 @@ function resetAiFeed(
2705
2707
  const roleplayUserName = roleplayUserCharacter?.name;
2706
2708
  feed.innerHTML = roleplayName
2707
2709
  ? `<div class="assistant-message"><span class="message-heading"><span>${esc(roleplayName)}</span></span><div class="message-body"><p>正在扮演 ${esc(roleplayName)}。${roleplayUserName ? `你将以 ${esc(roleplayUserName)} 的身份与我互动。` : "我可以通过角色卡、人物关系、知情设定和故事正文回答。"}</p></div></div>`
2708
- : '<div class="assistant-message"><span class="message-heading"><span>助手</span></span><div class="message-body"><p>选择章节和模型后即可开始问答;提到续写或润色时会自动加载对应 Skill,也可用 /continue-writing 或 /polish-writing 强制加载。所有引用都基于已保存正文。</p></div></div>';
2710
+ : '<div class="assistant-message"><span class="message-heading"><span>助手</span></span><div class="message-body"><p>选择章节和模型后即可开始问答;所有引用都基于已保存正文。</p></div></div>';
2709
2711
  }
2710
2712
 
2711
2713
  function aiAssistantLabel(suffix = "", roleplayCharacter = state.aiRoleplayCharacter) {
@@ -4887,7 +4889,7 @@ async function addAiImageFiles(files) {
4887
4889
  persistActiveAiChatTab();
4888
4890
  }
4889
4891
 
4890
- function clearAiPromptComposer() {
4892
+ function clearAiPromptComposer({ collapseScenePanel = false } = {}) {
4891
4893
  state.aiCitations = [];
4892
4894
  state.aiReferences = [];
4893
4895
  state.aiImageAttachments = [];
@@ -4899,6 +4901,7 @@ function clearAiPromptComposer() {
4899
4901
  renderAiSemanticInjection();
4900
4902
  hideAiMentionMenu();
4901
4903
  syncAiSceneComposer();
4904
+ if (collapseScenePanel) setAiScenePanelExpanded(false);
4902
4905
  }
4903
4906
 
4904
4907
  function captureAiPromptComposer() {
@@ -4963,6 +4966,15 @@ function roleplaySceneComposerVisible() {
4963
4966
  return $("#ai-task").value === "roleplay" && Boolean(state.aiRoleplayCharacter);
4964
4967
  }
4965
4968
 
4969
+ function setAiScenePanelExpanded(expanded) {
4970
+ const button = $("#ai-scene-button");
4971
+ const panel = $("#ai-scene-panel");
4972
+ if (!button || !panel) return;
4973
+ const nextExpanded = Boolean(expanded && !button.classList.contains("hidden"));
4974
+ panel.classList.toggle("hidden", !nextExpanded);
4975
+ button.setAttribute("aria-expanded", String(nextExpanded));
4976
+ }
4977
+
4966
4978
  function syncAiSceneComposer() {
4967
4979
  const button = $("#ai-scene-button");
4968
4980
  const panel = $("#ai-scene-panel");
@@ -4973,10 +4985,7 @@ function syncAiSceneComposer() {
4973
4985
  button.classList.toggle("hidden", !visible);
4974
4986
  button.disabled = !visible || busy || readOnly;
4975
4987
  button.setAttribute("aria-hidden", String(!visible));
4976
- if (!visible) {
4977
- panel.classList.add("hidden");
4978
- button.setAttribute("aria-expanded", "false");
4979
- }
4988
+ if (!visible) setAiScenePanelExpanded(false);
4980
4989
  const hasContent = Boolean(aiSceneDirectionText().trim()) || roleplayScenePinHasContent(captureAiScenePin());
4981
4990
  button.classList.toggle("is-active", hasContent);
4982
4991
  }
@@ -4986,8 +4995,7 @@ function toggleAiScenePanel() {
4986
4995
  const panel = $("#ai-scene-panel");
4987
4996
  if (!button || !panel || button.classList.contains("hidden")) return;
4988
4997
  const willOpen = panel.classList.contains("hidden");
4989
- panel.classList.toggle("hidden", !willOpen);
4990
- button.setAttribute("aria-expanded", String(willOpen));
4998
+ setAiScenePanelExpanded(willOpen);
4991
4999
  if (willOpen) $("#ai-scene-direction")?.focus();
4992
5000
  }
4993
5001
 
@@ -5030,6 +5038,7 @@ function aiPromptTextBoundary(prompt, offset) {
5030
5038
 
5031
5039
  function hideAiMentionMenu() {
5032
5040
  aiMentionMatch = null;
5041
+ aiSkillMatch = null;
5033
5042
  aiMentionRange = null;
5034
5043
  aiMentionActiveIndex = -1;
5035
5044
  const prompt = $("#ai-prompt");
@@ -5086,21 +5095,37 @@ function syncAiReferencesWithPrompt() {
5086
5095
  function updateAiMentionMenu() {
5087
5096
  syncAiReferencesWithPrompt();
5088
5097
  const prompt = $("#ai-prompt");
5089
- const match = findAiMention(aiPromptTextBeforeCursor());
5090
- if (!match) return hideAiMentionMenu();
5098
+ const textBeforeCursor = aiPromptTextBeforeCursor();
5099
+ const skillMatch = $("#ai-task").value === "roleplay" ? null : findAiSkillCommand(textBeforeCursor);
5100
+ const mentionMatch = skillMatch ? null : findAiMention(textBeforeCursor);
5101
+ if (!skillMatch && !mentionMatch) return hideAiMentionMenu();
5091
5102
  const selection = window.getSelection();
5092
5103
  if (!selection?.rangeCount || !prompt.contains(selection.anchorNode)) return hideAiMentionMenu();
5093
- aiMentionMatch = match;
5104
+ aiSkillMatch = skillMatch;
5105
+ aiMentionMatch = mentionMatch;
5094
5106
  aiMentionRange = selection.getRangeAt(0).cloneRange();
5095
5107
  const menu = $("#ai-mention-menu");
5108
+ if (skillMatch) {
5109
+ const options = listAiSkillOptions(skillMatch.query);
5110
+ aiMentionActiveIndex = -1;
5111
+ prompt.removeAttribute("aria-activedescendant");
5112
+ menu.setAttribute("aria-label", "选择写作 Skill");
5113
+ menu.innerHTML = options.length
5114
+ ? options.map((item, index) => `<button id="ai-skill-option-${index}" class="ai-mention-option ai-skill-option" type="button" role="option" aria-selected="false" tabindex="-1" data-ai-skill-name="${esc(item.name)}"><small>Skill</small><span><strong>/${esc(item.name)}</strong><em>${esc(item.label)} · ${esc(item.description)}</em></span></button>`).join("")
5115
+ : '<p class="ai-mention-empty">没有匹配的写作 Skill</p>';
5116
+ menu.classList.remove("hidden");
5117
+ prompt.setAttribute("aria-expanded", "true");
5118
+ return;
5119
+ }
5096
5120
  const chapters = state.work?.volumes.flatMap((volume) => volume.chapters.map((chapter) => ({
5097
5121
  ...chapter,
5098
5122
  volumeTitle: volume.title
5099
5123
  }))) ?? [];
5100
- const options = listAiMentionOptions(state.characters, state.settings, chapters, match.query)
5124
+ const options = listAiMentionOptions(state.characters, state.settings, chapters, mentionMatch.query)
5101
5125
  .filter((item) => item.kind !== "context-settings" || $("#ai-task").value !== "roleplay");
5102
5126
  aiMentionActiveIndex = -1;
5103
5127
  prompt.removeAttribute("aria-activedescendant");
5128
+ menu.setAttribute("aria-label", "引用角色、设定、章节或上下文能力");
5104
5129
  menu.innerHTML = options.length
5105
5130
  ? options.map((item, index) => `<button id="ai-mention-option-${index}" class="ai-mention-option" type="button" role="option" aria-selected="false" tabindex="-1" data-ai-reference-kind="${esc(item.kind)}" data-ai-reference-id="${esc(item.id)}" data-ai-reference-name="${esc(item.name)}"><small>${esc(item.kindLabel)}</small><strong>${esc(item.name)}</strong></button>`).join("")
5106
5131
  : '<p class="ai-mention-empty">没有匹配的角色、设定、章节或上下文能力</p>';
@@ -5141,6 +5166,30 @@ function selectAiMention(button) {
5141
5166
  hideAiMentionMenu();
5142
5167
  }
5143
5168
 
5169
+ function selectAiSkill(button) {
5170
+ if (!aiSkillMatch || !aiMentionRange) return;
5171
+ const prompt = $("#ai-prompt");
5172
+ const cursorText = aiPromptTextFromRange(aiMentionRange, prompt);
5173
+ const localSkill = findAiSkillCommand(cursorText);
5174
+ if (!localSkill) return hideAiMentionMenu();
5175
+ const applied = applyAiSkillCommand(cursorText, localSkill, button.dataset.aiSkillName);
5176
+ const range = document.createRange();
5177
+ const startBoundary = aiPromptTextBoundary(prompt, localSkill.start);
5178
+ const endBoundary = aiPromptTextBoundary(prompt, cursorText.length);
5179
+ range.setStart(startBoundary.node, startBoundary.offset);
5180
+ range.setEnd(endBoundary.node, endBoundary.offset);
5181
+ range.deleteContents();
5182
+ const command = document.createTextNode(`${applied.command} `);
5183
+ range.insertNode(command);
5184
+ const selection = window.getSelection();
5185
+ selection?.removeAllRanges();
5186
+ range.setStartAfter(command);
5187
+ range.collapse(true);
5188
+ selection?.addRange(range);
5189
+ prompt.focus();
5190
+ hideAiMentionMenu();
5191
+ }
5192
+
5144
5193
  function addSelectedLinesAsCitation() {
5145
5194
  if (!state.chapter || !chapterLineSelection) return;
5146
5195
  const selection = selectedChapterLinePayload(chapterLineSelection.start, chapterLineSelection.end);
@@ -13301,20 +13350,22 @@ async function renderPlatformAiConfig() {
13301
13350
  }
13302
13351
 
13303
13352
  function tokenUsageDateLabel(date) {
13353
+ const [year, month, day] = String(date).split("-").map(Number);
13304
13354
  return new Intl.DateTimeFormat("zh-CN", {
13305
13355
  year: "numeric",
13306
13356
  month: "short",
13307
13357
  day: "numeric",
13308
- weekday: "short"
13309
- }).format(new Date(`${date}T00:00:00`));
13358
+ weekday: "short",
13359
+ timeZone: "UTC"
13360
+ }).format(new Date(Date.UTC(year, month - 1, day)));
13310
13361
  }
13311
13362
 
13312
- function tokenUsageCalendarMarkup(daily) {
13313
- const calendar = buildUsageCalendar(daily);
13363
+ function tokenUsageCalendarMarkup(daily, year, serverDate) {
13364
+ const calendar = buildUsageCalendar(daily, year, serverDate);
13314
13365
  const cells = calendar.cells.map((cell) => {
13315
13366
  const label = `${tokenUsageDateLabel(cell.date)}:${Number(cell.totalTokens).toLocaleString("zh-CN")} Token`;
13316
- return cell.future
13317
- ? `<span class="usage-calendar-cell is-future" data-level="${cell.level}" role="gridcell" aria-disabled="true"></span>`
13367
+ return cell.outsideYear || cell.future
13368
+ ? `<span class="usage-calendar-cell ${cell.outsideYear ? "is-outside-year" : "is-future"}" data-level="${cell.level}" role="gridcell" aria-disabled="true"></span>`
13318
13369
  : `<button class="usage-calendar-cell" type="button" data-level="${cell.level}" data-usage-calendar-label="${esc(label)}" role="gridcell" aria-label="${esc(label)}"></button>`;
13319
13370
  }).join("");
13320
13371
  const months = calendar.months.map((month) => `<span style="grid-column:${month.week + 1}">${esc(month.label)}</span>`).join("");
@@ -13324,7 +13375,7 @@ function tokenUsageCalendarMarkup(daily) {
13324
13375
  <div class="usage-calendar-months" aria-hidden="true">${months}</div>
13325
13376
  <div class="usage-calendar-body">
13326
13377
  <div class="usage-calendar-weekdays" aria-hidden="true"><span>一</span><span>三</span><span>五</span></div>
13327
- <div class="usage-calendar-grid" role="grid" aria-label="过去 53 周每日 Token 用量">${cells}</div>
13378
+ <div class="usage-calendar-grid" role="grid" aria-label="${calendar.year} 年每日 Token 用量">${cells}</div>
13328
13379
  </div>
13329
13380
  </div>
13330
13381
  </div>
@@ -13333,6 +13384,21 @@ function tokenUsageCalendarMarkup(daily) {
13333
13384
  <div class="usage-calendar-legend"><span>少</span>${[0, 1, 2, 3, 4].map((level) => `<i data-level="${level}" aria-hidden="true"></i>`).join("")}<span>多</span></div>`;
13334
13385
  }
13335
13386
 
13387
+ function bindUsageCalendar(root, usage) {
13388
+ root.querySelectorAll("[data-usage-calendar-year]").forEach((select) => {
13389
+ select.addEventListener("change", () => {
13390
+ const section = select.closest(".usage-calendar-section");
13391
+ const calendarHost = section?.querySelector("[data-usage-calendar-host]");
13392
+ if (!calendarHost) return;
13393
+ calendarHost.innerHTML = tokenUsageCalendarMarkup(usage?.daily, Number(select.value), usage?.serverDate);
13394
+ bindUsageCalendarInteractions(calendarHost);
13395
+ scrollUsageCalendarsToLatest(calendarHost);
13396
+ });
13397
+ });
13398
+ bindUsageCalendarInteractions(root);
13399
+ scrollUsageCalendarsToLatest(root);
13400
+ }
13401
+
13336
13402
  function bindUsageCalendarInteractions(root) {
13337
13403
  root.querySelectorAll(".usage-calendar-widget").forEach((widget) => {
13338
13404
  const tooltip = widget.querySelector(".usage-calendar-tooltip");
@@ -13556,6 +13622,15 @@ function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false
13556
13622
  const callTypeUsage = (Array.isArray(usage?.callTypes) ? usage.callTypes : [])
13557
13623
  .map((item) => `<span class="usage-call-type-chip"><strong>${esc(callTypeLabels[item.callType] ?? item.callType)}</strong><span>${esc(formatTokenCount(item.totalTokens))} Token · ${Number(item.requestCount || 0).toLocaleString("zh-CN")} 次</span></span>`)
13558
13624
  .join("");
13625
+ const calendarYears = usageCalendarYears(usage?.daily);
13626
+ const selectedCalendarYear = calendarYears[0] ?? null;
13627
+ const usageTimezone = String(usage?.timezone || "服务器本地时区");
13628
+ const calendarYearSelect = selectedCalendarYear === null
13629
+ ? ""
13630
+ : `<select class="usage-calendar-year-select" data-usage-calendar-year aria-label="每日用量年份">${calendarYears.map((year) => `<option value="${year}">${year} 年</option>`).join("")}</select>`;
13631
+ const calendarMarkup = selectedCalendarYear === null
13632
+ ? '<p class="usage-calendar-empty">尚无每日 Token 用量记录。</p>'
13633
+ : tokenUsageCalendarMarkup(usage?.daily, selectedCalendarYear, usage?.serverDate);
13559
13634
  return `<section class="usage-overview" aria-labelledby="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">
13560
13635
  <div class="config-section-header usage-overview-header"><div><h2 id="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">${esc(title || "Token 用量")}</h2><p>${esc(description || "统计该范围内的全部 AI 调用。")}</p></div><button class="ghost-button usage-details-button" type="button" data-token-usage-details aria-haspopup="dialog" aria-expanded="false" aria-controls="token-usage-details-toast">详细数据</button></div>
13561
13636
  <div class="usage-stat-grid">
@@ -13567,8 +13642,8 @@ function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false
13567
13642
  <p class="usage-measurement-note">${requestCount.toLocaleString("zh-CN")} 次有用量记录的调用。${esc(estimateNote)} 有 ${unpricedModelCount.toLocaleString("zh-CN")} 个模型在价格表中未找到对应价格</p>
13568
13643
  ${callTypeUsage ? `<div class="usage-call-types" aria-label="按调用类型区分的 Token 用量">${callTypeUsage}</div>` : ""}
13569
13644
  <section class="usage-calendar-section" aria-labelledby="${showWorks ? "platform-usage-calendar-title" : "work-usage-calendar-title"}">
13570
- <header><div><h3 id="${showWorks ? "platform-usage-calendar-title" : "work-usage-calendar-title"}">每日用量</h3><p>GitHub 风格网格展示过去 53 周;颜色越深,当天消耗越高。</p></div></header>
13571
- ${tokenUsageCalendarMarkup(usage?.daily)}
13645
+ <header><div><h3 id="${showWorks ? "platform-usage-calendar-title" : "work-usage-calendar-title"}">每日用量</h3><p>GitHub 风格网格按服务器时区(${esc(usageTimezone)})分年展示;颜色越深,当天消耗越高。</p></div>${calendarYearSelect}</header>
13646
+ <div data-usage-calendar-host>${calendarMarkup}</div>
13572
13647
  </section>
13573
13648
  ${showWorks ? `<section class="usage-work-section" aria-labelledby="usage-work-title"><header><div><h3 id="usage-work-title">各作品用量</h3><p>按 Token 总消耗从高到低排列,包含尚未使用 AI 的作品。</p></div></header><div class="usage-work-table-scroll"><table class="usage-work-table"><thead><tr><th>作品</th><th>总消耗</th><th>输入</th><th>输出</th><th>缓存命中率</th><th>调用</th></tr></thead><tbody>${workRows || '<tr><td colspan="6">还没有作品用量记录。</td></tr>'}</tbody></table></div></section>` : ""}
13574
13649
  </section>`;
@@ -13577,16 +13652,14 @@ function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false
13577
13652
  async function renderPlatformTokenUsage() {
13578
13653
  const host = $("#platform-usage-content");
13579
13654
  host.innerHTML = '<div class="empty-state">正在汇总 Token 用量……</div>';
13580
- const timezoneOffset = -new Date().getTimezoneOffset();
13581
- const usage = await api(`/api/platform/ai/usage?timezoneOffset=${timezoneOffset}`);
13655
+ const usage = await api("/api/platform/ai/usage");
13582
13656
  host.innerHTML = tokenUsageOverviewMarkup(usage, {
13583
13657
  title: "项目累计用量",
13584
13658
  description: "汇总所有作品迄今产生的输入与输出 Token;缓存命中率仅基于供应商返回了缓存明细的调用。",
13585
13659
  showWorks: true
13586
13660
  });
13587
13661
  bindTokenUsageDetails(host, usage, "项目累计用量");
13588
- bindUsageCalendarInteractions(host);
13589
- scrollUsageCalendarsToLatest(host);
13662
+ bindUsageCalendar(host, usage);
13590
13663
  }
13591
13664
 
13592
13665
  async function renderBookAiSettings() {
@@ -13606,7 +13679,7 @@ async function renderBookAiSettings() {
13606
13679
  moduleApi("ai-settings", `/api/works/${state.work.id}/task-defaults`),
13607
13680
  moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings/relationship-search-index`),
13608
13681
  moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings/semantic-search-index`),
13609
- moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings/usage?timezoneOffset=${-new Date().getTimezoneOffset()}`),
13682
+ moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings/usage`),
13610
13683
  moduleApi("ai-settings", "/api/platform/ai/protocols"),
13611
13684
  // 可写工具开关独立于 ai-settings 存储;加载失败时仍可展示其余配置。
13612
13685
  api(`/api/works/${state.work.id}/ai/tools`).catch(() => null),
@@ -13681,8 +13754,7 @@ async function renderBookAiSettings() {
13681
13754
  if (section.querySelector("h2")?.textContent === "Agent 工具调用上限") section.id = "agent-tool-call-limit-settings";
13682
13755
  });
13683
13756
  host.insertAdjacentHTML("beforeend", `<section class="config-section"><div class="config-section-header"><div><h2>AI 可写工具</h2><p>默认全部关闭:逐项开启后,侧边栏 AI 才能在对应模块提交修改计划。计划只包含操作描述与 AI 简述;确认前系统会按当前数据库生成字段级明细(含修改前后值),执行时整体原子完成并再次校验权限、开关与目标版本,全程可在「AI 操作审批中心」追溯。AI 不能删除任何条目,也不能改写正文。</p></div><div class="card-actions"><button id="open-ai-approval-center-from-settings" class="ghost-button" type="button">打开 AI 操作审批中心</button></div></div><div class="ai-agent-tools ai-write-tools">${AI_WRITE_TOOLS_META.map((tool) => `<label><input name="ai-write-tool" type="checkbox" value="${esc(tool.id)}" ${writeToolsState?.[tool.id] === true ? "checked" : ""}><span><strong>${esc(tool.label)}</strong><small>${esc(tool.description)}</small></span></label>`).join("")}</div><p class="usage-measurement-note">${writeTools ? `当前单次审批最多 ${writeToolsMaxOperations} 个操作,可通过环境变量 AI_WRITE_PLAN_MAX_OPERATIONS 调整。` : "工具开关状态暂时无法加载,显示的勾选可能不是最新值。"}</p><div class="card-actions"><button id="save-ai-write-tools" class="ghost-button config-save-button" type="button">保存开关设置</button></div></section>`);
13684
- bindUsageCalendarInteractions(host);
13685
- scrollUsageCalendarsToLatest(host);
13757
+ bindUsageCalendar(host, usage);
13686
13758
  host.querySelector('input[name="agent-tool"][value="search_story_entities"]').closest("label").insertAdjacentHTML(
13687
13759
  "beforebegin",
13688
13760
  `<label><input name="agent-tool" type="checkbox" value="grep" ${agentTools.has("grep") ? "checked" : ""}><span><strong>查询正文关键字</strong><small>从段落索引查询关键字,默认返回前 20 条完整段落和章节信息。</small></span></label>`
@@ -13693,7 +13765,7 @@ async function renderBookAiSettings() {
13693
13765
  );
13694
13766
  host.querySelector('input[name="agent-tool"][value="read_character_sections"]').closest("label").insertAdjacentHTML(
13695
13767
  "afterend",
13696
- `<label><input name="agent-tool" type="checkbox" value="semantic_search_story" ${agentTools.has("semantic_search_story") ? "checked" : ""} ${settings.semanticSearchEnabled ? "" : "disabled"}><span><strong>语义检索作品原文</strong><small>允许 Agent 显式调用 semantic_search_story;只影响保存后新建的对话,普通消息不会自动检索。</small></span></label>`
13768
+ `<label><input name="agent-tool" type="checkbox" value="semantic_search_story" ${agentTools.has("semantic_search_story") ? "checked" : ""} ${settings.semanticSearchEnabled ? "" : "disabled"}><span><strong>语义检索作品原文(RAG)</strong><small>允许 Agent 显式调用 semantic_search_story;只影响保存后新建的对话,普通消息不会自动检索。</small></span></label>`
13697
13769
  );
13698
13770
  host.querySelector(".ai-agent-tools").insertAdjacentHTML(
13699
13771
  "beforeend",
@@ -18176,7 +18248,7 @@ async function streamChat(requestHolder, body, idempotencyKey) {
18176
18248
  syncAiTaskOptions();
18177
18249
  renderAiRoleplayCharacterSelect();
18178
18250
  renderAiQuickActions();
18179
- clearAiPromptComposer();
18251
+ clearAiPromptComposer({ collapseScenePanel: Boolean(String(body.sceneDirection ?? "").trim()) });
18180
18252
  }
18181
18253
  mountAssistantMessage();
18182
18254
  }
@@ -18404,7 +18476,7 @@ function appendMessage(role, text, citations = [], createdAt = null, metadata =
18404
18476
  sceneBody.className = "user-message-scene-body";
18405
18477
  sceneBody.textContent = parsedUserTurn.sceneDirection;
18406
18478
  scene.append(sceneLabel, sceneBody);
18407
- message.querySelector(".message-body")?.before(scene);
18479
+ message.querySelector(".message-body")?.prepend(scene);
18408
18480
  }
18409
18481
  const mentionGroups = role === "user"
18410
18482
  ? [
@@ -20333,7 +20405,9 @@ $("#module-create-button").addEventListener("click", () => ({ drafts: openDraftD
20333
20405
  $("#ai-prompt").addEventListener("input", async () => {
20334
20406
  updateAiMentionMenu();
20335
20407
  setAiContextMeter(null);
20336
- if (!findAiMention(aiPromptTextBeforeCursor())) return;
20408
+ const textBeforeCursor = aiPromptTextBeforeCursor();
20409
+ if ($("#ai-task").value !== "roleplay" && findAiSkillCommand(textBeforeCursor)) return;
20410
+ if (!findAiMention(textBeforeCursor)) return;
20337
20411
  try {
20338
20412
  await ensureAiReferencesLoaded();
20339
20413
  updateAiMentionMenu();
@@ -20463,6 +20537,7 @@ $("#ai-task").addEventListener("change", async (event) => {
20463
20537
  }
20464
20538
  }
20465
20539
  setAiContextMeter(null);
20540
+ updateAiMentionMenu();
20466
20541
  });
20467
20542
  $("#ai-scope").addEventListener("change", (event) => {
20468
20543
  if (state.aiPromptSent) {
@@ -20473,6 +20548,8 @@ $("#ai-scope").addEventListener("change", (event) => {
20473
20548
  setAiContextMeter(null);
20474
20549
  });
20475
20550
  $("#ai-mention-menu").addEventListener("click", (event) => {
20551
+ const skillButton = event.target.closest("[data-ai-skill-name]");
20552
+ if (skillButton) return selectAiSkill(skillButton);
20476
20553
  const button = event.target.closest("[data-ai-reference-id]");
20477
20554
  if (button) selectAiMention(button);
20478
20555
  });
@@ -21016,7 +21093,8 @@ $("#ai-prompt").addEventListener("keydown", (event) => {
21016
21093
  const activeOption = $("#ai-mention-menu").querySelector('[role="option"][aria-selected="true"]');
21017
21094
  if (activeOption) {
21018
21095
  event.preventDefault();
21019
- selectAiMention(activeOption);
21096
+ if (activeOption.dataset.aiSkillName) selectAiSkill(activeOption);
21097
+ else selectAiMention(activeOption);
21020
21098
  return;
21021
21099
  }
21022
21100
  }
@@ -9,7 +9,7 @@
9
9
  <script src="/theme-init.js?v=20260827-reader-prefetch-v1"></script>
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
- <link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2">
12
+ <link rel="stylesheet" href="/styles.css?v=20260816-task-scope-volume-collapse-v2&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v3&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=galaxy-compact-controls-v2&feature=galaxy-motion-mode-v2&feature=chapter-search-replace-v3&feature=task-auto-run-ring-center-v3&feature=character-relationship-delete-v1&feature=ai-assistant-workspace-v2&feature=mobile-module-tab-position-v1&feature=volume-detail-icon-v1&feature=editor-actions-flow-v1&feature=reader-controls-subpanel-v1&feature=reader-focus-ring-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v2&feature=ai-composer-square-controls-v2&feature=annotation-line-counts-v1&feature=chapter-comment-filters-v1&feature=line-number-gutter-fill-v1&feature=ai-relationship-roleplay-v1&feature=ai-model-picker-v1&feature=markdown-word-count-five-digit-v2&feature=ai-stream-character-count-stable-v1&feature=ai-stream-character-count-five-digit-v1&feature=annotation-marker-offset-v1&feature=mobile-ai-entry-hidden-v1&feature=phone-client-entry-v1&feature=ai-stream-idle-timeout-v1&feature=ai-user-message-width-v2&feature=ai-chat-image-attachments-v9&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=toast-click-dismiss-v3&feature=character-avatar-v6&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-token-quota-positive-v5&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-stream-connection-seconds-v1&feature=ai-token-usage-estimated-price-v1&feature=record-favorites-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1&feature=character-card-header-alignment-v6&feature=character-card-title-fit-v2&feature=book-import-progress-v1&feature=editor-toolbar-compact-v2&feature=reader-first-frame-v1&feature=auth-compact-v2&feature=editor-preview-toggle-v2&feature=setting-title-chrome-v2&feature=entity-pin-icon-center-v1&feature=chapter-center-bottom-space-v1&feature=work-editor-preferences-v1&feature=ai-roleplay-memory-v2&feature=ai-roleplay-memory-v3&feature=ai-roleplay-memory-v5&feature=character-editor-header-actions-v1&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=roleplay-memory-pin-border-v1&feature=ai-write-tools-v2&feature=ai-write-card-actions-compact-v1&feature=ai-write-plan-actions-footer-v1&feature=ai-question-actions-footer-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v1&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=ai-settings-textarea-font-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=ai-skill-slash-menu-v1">
13
13
  </head>
14
14
  <body class="auth-pending">
15
15
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -351,7 +351,7 @@
351
351
  <div id="ai-feed" class="ai-feed" role="tabpanel">
352
352
  <div class="assistant-message">
353
353
  <span>助手</span>
354
- <div class="message-body"><p>选择章节和模型后即可开始问答;提到续写或润色时会自动加载对应 Skill,也可用 /continue-writing 或 /polish-writing 强制加载。所有引用都基于已保存正文。</p></div>
354
+ <div class="message-body"><p>选择章节和模型后即可开始问答;所有引用都基于已保存正文。</p></div>
355
355
  </div>
356
356
  </div>
357
357
  </div>
@@ -1322,6 +1322,6 @@
1322
1322
  </dialog>
1323
1323
 
1324
1324
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1325
- <script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v1&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v1&feature=ai-question-tool-result-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1"></script>
1325
+ <script type="module" src="/app.js?v=20260816-extended-thinking-effort-v1&feature=ai-tool-call-copy-feedback-v2&feature=ai-send-control-v2&feature=analysis-task-queue-refresh-v1&feature=character-gender-v1&feature=character-filter-state-v1&feature=relationship-canvas-scale-v1&feature=relationship-table-scroll-v1&feature=ai-session-id-copy-v2&feature=galaxy-motion-mode-v3&feature=galaxy-edge-label-threshold-v1&feature=calculate-time-tool-v2&feature=analysis-task-expired-toast-v1&feature=global-replace-volume-v1&feature=chapter-search-replace-v1&feature=chapter-save-toast-v1&feature=character-relationship-delete-v1&feature=character-relationship-group-v1&feature=analysis-task-stability-delay-v1&feature=ai-assistant-workspace-v2&feature=volume-detail-icon-v1&feature=volume-story-order-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-roleplay-message-reference-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1&feature=ai-relationship-roleplay-v1&feature=ai-roleplay-user-character-visibility-v1&feature=ai-roleplay-story-recall-v1&feature=ai-model-picker-v1&feature=ai-fork-model-unlock-v1&feature=context-percent-format-v1&feature=annotation-precise-locate-v1&feature=markdown-word-count-stable-v1&feature=ai-stream-character-count-stable-v2&feature=ai-process-empty-intermediate-v1&feature=ai-feed-scroll-follow-v1&feature=ai-message-retry-v1&feature=ai-stream-idle-timeout-v2&feature=ai-config-delete-v1&feature=ai-provider-protocol-options-v1&feature=ai-provider-thinking-type-v1&feature=ai-chat-image-attachments-v8&feature=ai-message-image-preview-v1&feature=ai-thinking-scroll-v2&feature=ai-image-conversation-model-lock-v1&feature=toast-click-dismiss-v2&feature=system-restart-dialog-delay-v1&feature=character-avatar-v6&feature=character-death-position-v1&feature=admin-ai-conversations-v1&feature=ai-usage-pricing-v1&feature=ai-usage-pricing-badge-v2&feature=ai-usage-pricing-label-v1&feature=ai-usage-token-breakdown-v1&feature=ai-usage-pricing-cache-v2&feature=ai-usage-pricing-manual-refresh-v1&feature=ai-monthly-token-quota-v1&feature=ai-provider-token-quota-v1&feature=ai-token-quota-positive-v6&feature=ai-model-thinking-label-v1&feature=ai-model-picker-focus-v1&feature=ai-provider-model-import-v1&feature=ai-assistant-brain-icon-v1&feature=ai-token-usage-details-v1&feature=ai-token-usage-details-centered-v1&feature=ai-token-usage-raw-input-v1&feature=ai-stream-connection-seconds-v1&feature=phone-client-entry-v1&feature=ai-usage-pricing-cache-v1&feature=ai-model-thinking-label-v3&feature=ai-roleplay-speaker-label-v1&feature=toast-modal-host-v1&feature=api-key-copy-existing-v1&feature=character-favorite-v1&feature=record-favorites-v1&feature=ai-token-usage-estimated-price-v1&feature=entity-detail-favorites-v1&feature=entity-pin-v1&feature=entity-pin-icon-v1&feature=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2&feature=admin-account-identity-v2&feature=ai-provider-analysis-timeout-v1&feature=task-detail-failure-orange-v1&feature=book-import-progress-v1&feature=presence-multiple-users-v1&feature=ai-context-input-output-v1&feature=editor-blank-lines-preserved-v1&feature=reader-first-frame-v1&feature=vditor-lazy-load-v1&feature=ui-module-preload-v1&feature=reader-initial-prefetch-v1&feature=editor-preview-toggle-v2&feature=vditor-fullscreen-disabled-v1&feature=chapter-auto-indent-v1&feature=chapter-centered-scroll-v1&feature=work-editor-preferences-v1&feature=ai-context-output-usage-v1&feature=ai-roleplay-memory-v4&feature=ai-roleplay-memory-v5&feature=roleplay-memory-header-actions-v1&feature=roleplay-memory-header-toolbar-v1&feature=roleplay-memory-action-icons-v1&feature=roleplay-memory-action-colors-v1&feature=annotation-line-anchor-v1&feature=stable-line-ids-v1&feature=live-annotation-anchors-v1&feature=chapter-comment-filters-v1&feature=ai-write-tools-v3&feature=ai-question-option-supplement-v1&feature=ai-question-selection-highlight-v1&feature=ai-question-continuation-ui-v1&feature=ai-question-tool-result-v1&feature=ai-question-submit-guidance-v1&feature=ai-question-answer-limit-v1&feature=semantic-search-v6&feature=chapter-title-renumber-v1&feature=ai-writing-skills-v3&feature=ai-cancel-preserve-process-v2&feature=remote-mcp-v1&feature=character-alias-chips-v2&feature=character-title-input-v1&feature=character-detail-value-wrap-v2&feature=chapter-batch-editor-refresh-v1&feature=ai-usage-stat-display-v1&feature=ai-usage-year-v1&feature=semantic-search-rag-label-v1&feature=ai-skill-slash-menu-v1&feature=ai-roleplay-scene-bubble-v1&feature=ai-roleplay-scene-collapse-v1&feature=markdown-adjacent-blockquotes-v1"></script>
1326
1326
  </body>
1327
1327
  </html>
@@ -201,6 +201,10 @@ export function renderMarkdown(value) {
201
201
  quote.push(quoteLine[1]);
202
202
  continue;
203
203
  }
204
+ if (!line.trim() && quote && /^>\s?/u.test(lines[lineIndex + 1] ?? "")) {
205
+ quote.push("");
206
+ continue;
207
+ }
204
208
  flushQuote();
205
209
  if (!line.trim()) {
206
210
  // CommonMark 松散列表:同类型列表项之间的空行不结束列表,