@musnows/scriverse 0.9.6 → 0.9.7

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
  }
@@ -2,6 +2,7 @@ import { buildRelationshipGraph, createGalaxyRenderer, normalizeGalaxyFrameRate,
2
2
  import { formatDateTime, normalizeParagraphSpacing } from "/text-formatting.js?v=20260713-saved-at-seconds";
3
3
  import { renderMarkdown } from "/markdown.js?v=20260731-no-external-images-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) {
@@ -5030,6 +5032,7 @@ function aiPromptTextBoundary(prompt, offset) {
5030
5032
 
5031
5033
  function hideAiMentionMenu() {
5032
5034
  aiMentionMatch = null;
5035
+ aiSkillMatch = null;
5033
5036
  aiMentionRange = null;
5034
5037
  aiMentionActiveIndex = -1;
5035
5038
  const prompt = $("#ai-prompt");
@@ -5086,21 +5089,37 @@ function syncAiReferencesWithPrompt() {
5086
5089
  function updateAiMentionMenu() {
5087
5090
  syncAiReferencesWithPrompt();
5088
5091
  const prompt = $("#ai-prompt");
5089
- const match = findAiMention(aiPromptTextBeforeCursor());
5090
- if (!match) return hideAiMentionMenu();
5092
+ const textBeforeCursor = aiPromptTextBeforeCursor();
5093
+ const skillMatch = $("#ai-task").value === "roleplay" ? null : findAiSkillCommand(textBeforeCursor);
5094
+ const mentionMatch = skillMatch ? null : findAiMention(textBeforeCursor);
5095
+ if (!skillMatch && !mentionMatch) return hideAiMentionMenu();
5091
5096
  const selection = window.getSelection();
5092
5097
  if (!selection?.rangeCount || !prompt.contains(selection.anchorNode)) return hideAiMentionMenu();
5093
- aiMentionMatch = match;
5098
+ aiSkillMatch = skillMatch;
5099
+ aiMentionMatch = mentionMatch;
5094
5100
  aiMentionRange = selection.getRangeAt(0).cloneRange();
5095
5101
  const menu = $("#ai-mention-menu");
5102
+ if (skillMatch) {
5103
+ const options = listAiSkillOptions(skillMatch.query);
5104
+ aiMentionActiveIndex = -1;
5105
+ prompt.removeAttribute("aria-activedescendant");
5106
+ menu.setAttribute("aria-label", "选择写作 Skill");
5107
+ menu.innerHTML = options.length
5108
+ ? 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("")
5109
+ : '<p class="ai-mention-empty">没有匹配的写作 Skill</p>';
5110
+ menu.classList.remove("hidden");
5111
+ prompt.setAttribute("aria-expanded", "true");
5112
+ return;
5113
+ }
5096
5114
  const chapters = state.work?.volumes.flatMap((volume) => volume.chapters.map((chapter) => ({
5097
5115
  ...chapter,
5098
5116
  volumeTitle: volume.title
5099
5117
  }))) ?? [];
5100
- const options = listAiMentionOptions(state.characters, state.settings, chapters, match.query)
5118
+ const options = listAiMentionOptions(state.characters, state.settings, chapters, mentionMatch.query)
5101
5119
  .filter((item) => item.kind !== "context-settings" || $("#ai-task").value !== "roleplay");
5102
5120
  aiMentionActiveIndex = -1;
5103
5121
  prompt.removeAttribute("aria-activedescendant");
5122
+ menu.setAttribute("aria-label", "引用角色、设定、章节或上下文能力");
5104
5123
  menu.innerHTML = options.length
5105
5124
  ? 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
5125
  : '<p class="ai-mention-empty">没有匹配的角色、设定、章节或上下文能力</p>';
@@ -5141,6 +5160,30 @@ function selectAiMention(button) {
5141
5160
  hideAiMentionMenu();
5142
5161
  }
5143
5162
 
5163
+ function selectAiSkill(button) {
5164
+ if (!aiSkillMatch || !aiMentionRange) return;
5165
+ const prompt = $("#ai-prompt");
5166
+ const cursorText = aiPromptTextFromRange(aiMentionRange, prompt);
5167
+ const localSkill = findAiSkillCommand(cursorText);
5168
+ if (!localSkill) return hideAiMentionMenu();
5169
+ const applied = applyAiSkillCommand(cursorText, localSkill, button.dataset.aiSkillName);
5170
+ const range = document.createRange();
5171
+ const startBoundary = aiPromptTextBoundary(prompt, localSkill.start);
5172
+ const endBoundary = aiPromptTextBoundary(prompt, cursorText.length);
5173
+ range.setStart(startBoundary.node, startBoundary.offset);
5174
+ range.setEnd(endBoundary.node, endBoundary.offset);
5175
+ range.deleteContents();
5176
+ const command = document.createTextNode(`${applied.command} `);
5177
+ range.insertNode(command);
5178
+ const selection = window.getSelection();
5179
+ selection?.removeAllRanges();
5180
+ range.setStartAfter(command);
5181
+ range.collapse(true);
5182
+ selection?.addRange(range);
5183
+ prompt.focus();
5184
+ hideAiMentionMenu();
5185
+ }
5186
+
5144
5187
  function addSelectedLinesAsCitation() {
5145
5188
  if (!state.chapter || !chapterLineSelection) return;
5146
5189
  const selection = selectedChapterLinePayload(chapterLineSelection.start, chapterLineSelection.end);
@@ -13301,20 +13344,22 @@ async function renderPlatformAiConfig() {
13301
13344
  }
13302
13345
 
13303
13346
  function tokenUsageDateLabel(date) {
13347
+ const [year, month, day] = String(date).split("-").map(Number);
13304
13348
  return new Intl.DateTimeFormat("zh-CN", {
13305
13349
  year: "numeric",
13306
13350
  month: "short",
13307
13351
  day: "numeric",
13308
- weekday: "short"
13309
- }).format(new Date(`${date}T00:00:00`));
13352
+ weekday: "short",
13353
+ timeZone: "UTC"
13354
+ }).format(new Date(Date.UTC(year, month - 1, day)));
13310
13355
  }
13311
13356
 
13312
- function tokenUsageCalendarMarkup(daily) {
13313
- const calendar = buildUsageCalendar(daily);
13357
+ function tokenUsageCalendarMarkup(daily, year, serverDate) {
13358
+ const calendar = buildUsageCalendar(daily, year, serverDate);
13314
13359
  const cells = calendar.cells.map((cell) => {
13315
13360
  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>`
13361
+ return cell.outsideYear || cell.future
13362
+ ? `<span class="usage-calendar-cell ${cell.outsideYear ? "is-outside-year" : "is-future"}" data-level="${cell.level}" role="gridcell" aria-disabled="true"></span>`
13318
13363
  : `<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
13364
  }).join("");
13320
13365
  const months = calendar.months.map((month) => `<span style="grid-column:${month.week + 1}">${esc(month.label)}</span>`).join("");
@@ -13324,7 +13369,7 @@ function tokenUsageCalendarMarkup(daily) {
13324
13369
  <div class="usage-calendar-months" aria-hidden="true">${months}</div>
13325
13370
  <div class="usage-calendar-body">
13326
13371
  <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>
13372
+ <div class="usage-calendar-grid" role="grid" aria-label="${calendar.year} 年每日 Token 用量">${cells}</div>
13328
13373
  </div>
13329
13374
  </div>
13330
13375
  </div>
@@ -13333,6 +13378,21 @@ function tokenUsageCalendarMarkup(daily) {
13333
13378
  <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
13379
  }
13335
13380
 
13381
+ function bindUsageCalendar(root, usage) {
13382
+ root.querySelectorAll("[data-usage-calendar-year]").forEach((select) => {
13383
+ select.addEventListener("change", () => {
13384
+ const section = select.closest(".usage-calendar-section");
13385
+ const calendarHost = section?.querySelector("[data-usage-calendar-host]");
13386
+ if (!calendarHost) return;
13387
+ calendarHost.innerHTML = tokenUsageCalendarMarkup(usage?.daily, Number(select.value), usage?.serverDate);
13388
+ bindUsageCalendarInteractions(calendarHost);
13389
+ scrollUsageCalendarsToLatest(calendarHost);
13390
+ });
13391
+ });
13392
+ bindUsageCalendarInteractions(root);
13393
+ scrollUsageCalendarsToLatest(root);
13394
+ }
13395
+
13336
13396
  function bindUsageCalendarInteractions(root) {
13337
13397
  root.querySelectorAll(".usage-calendar-widget").forEach((widget) => {
13338
13398
  const tooltip = widget.querySelector(".usage-calendar-tooltip");
@@ -13556,6 +13616,15 @@ function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false
13556
13616
  const callTypeUsage = (Array.isArray(usage?.callTypes) ? usage.callTypes : [])
13557
13617
  .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
13618
  .join("");
13619
+ const calendarYears = usageCalendarYears(usage?.daily);
13620
+ const selectedCalendarYear = calendarYears[0] ?? null;
13621
+ const usageTimezone = String(usage?.timezone || "服务器本地时区");
13622
+ const calendarYearSelect = selectedCalendarYear === null
13623
+ ? ""
13624
+ : `<select class="usage-calendar-year-select" data-usage-calendar-year aria-label="每日用量年份">${calendarYears.map((year) => `<option value="${year}">${year} 年</option>`).join("")}</select>`;
13625
+ const calendarMarkup = selectedCalendarYear === null
13626
+ ? '<p class="usage-calendar-empty">尚无每日 Token 用量记录。</p>'
13627
+ : tokenUsageCalendarMarkup(usage?.daily, selectedCalendarYear, usage?.serverDate);
13559
13628
  return `<section class="usage-overview" aria-labelledby="${showWorks ? "platform-usage-overview-title" : "work-usage-overview-title"}">
13560
13629
  <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
13630
  <div class="usage-stat-grid">
@@ -13567,8 +13636,8 @@ function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false
13567
13636
  <p class="usage-measurement-note">${requestCount.toLocaleString("zh-CN")} 次有用量记录的调用。${esc(estimateNote)} 有 ${unpricedModelCount.toLocaleString("zh-CN")} 个模型在价格表中未找到对应价格</p>
13568
13637
  ${callTypeUsage ? `<div class="usage-call-types" aria-label="按调用类型区分的 Token 用量">${callTypeUsage}</div>` : ""}
13569
13638
  <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)}
13639
+ <header><div><h3 id="${showWorks ? "platform-usage-calendar-title" : "work-usage-calendar-title"}">每日用量</h3><p>GitHub 风格网格按服务器时区(${esc(usageTimezone)})分年展示;颜色越深,当天消耗越高。</p></div>${calendarYearSelect}</header>
13640
+ <div data-usage-calendar-host>${calendarMarkup}</div>
13572
13641
  </section>
13573
13642
  ${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
13643
  </section>`;
@@ -13577,16 +13646,14 @@ function tokenUsageOverviewMarkup(usage, { title, description, showWorks = false
13577
13646
  async function renderPlatformTokenUsage() {
13578
13647
  const host = $("#platform-usage-content");
13579
13648
  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}`);
13649
+ const usage = await api("/api/platform/ai/usage");
13582
13650
  host.innerHTML = tokenUsageOverviewMarkup(usage, {
13583
13651
  title: "项目累计用量",
13584
13652
  description: "汇总所有作品迄今产生的输入与输出 Token;缓存命中率仅基于供应商返回了缓存明细的调用。",
13585
13653
  showWorks: true
13586
13654
  });
13587
13655
  bindTokenUsageDetails(host, usage, "项目累计用量");
13588
- bindUsageCalendarInteractions(host);
13589
- scrollUsageCalendarsToLatest(host);
13656
+ bindUsageCalendar(host, usage);
13590
13657
  }
13591
13658
 
13592
13659
  async function renderBookAiSettings() {
@@ -13606,7 +13673,7 @@ async function renderBookAiSettings() {
13606
13673
  moduleApi("ai-settings", `/api/works/${state.work.id}/task-defaults`),
13607
13674
  moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings/relationship-search-index`),
13608
13675
  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()}`),
13676
+ moduleApi("ai-settings", `/api/works/${state.work.id}/ai-settings/usage`),
13610
13677
  moduleApi("ai-settings", "/api/platform/ai/protocols"),
13611
13678
  // 可写工具开关独立于 ai-settings 存储;加载失败时仍可展示其余配置。
13612
13679
  api(`/api/works/${state.work.id}/ai/tools`).catch(() => null),
@@ -13681,8 +13748,7 @@ async function renderBookAiSettings() {
13681
13748
  if (section.querySelector("h2")?.textContent === "Agent 工具调用上限") section.id = "agent-tool-call-limit-settings";
13682
13749
  });
13683
13750
  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);
13751
+ bindUsageCalendar(host, usage);
13686
13752
  host.querySelector('input[name="agent-tool"][value="search_story_entities"]').closest("label").insertAdjacentHTML(
13687
13753
  "beforebegin",
13688
13754
  `<label><input name="agent-tool" type="checkbox" value="grep" ${agentTools.has("grep") ? "checked" : ""}><span><strong>查询正文关键字</strong><small>从段落索引查询关键字,默认返回前 20 条完整段落和章节信息。</small></span></label>`
@@ -13693,7 +13759,7 @@ async function renderBookAiSettings() {
13693
13759
  );
13694
13760
  host.querySelector('input[name="agent-tool"][value="read_character_sections"]').closest("label").insertAdjacentHTML(
13695
13761
  "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>`
13762
+ `<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
13763
  );
13698
13764
  host.querySelector(".ai-agent-tools").insertAdjacentHTML(
13699
13765
  "beforeend",
@@ -20333,7 +20399,9 @@ $("#module-create-button").addEventListener("click", () => ({ drafts: openDraftD
20333
20399
  $("#ai-prompt").addEventListener("input", async () => {
20334
20400
  updateAiMentionMenu();
20335
20401
  setAiContextMeter(null);
20336
- if (!findAiMention(aiPromptTextBeforeCursor())) return;
20402
+ const textBeforeCursor = aiPromptTextBeforeCursor();
20403
+ if ($("#ai-task").value !== "roleplay" && findAiSkillCommand(textBeforeCursor)) return;
20404
+ if (!findAiMention(textBeforeCursor)) return;
20337
20405
  try {
20338
20406
  await ensureAiReferencesLoaded();
20339
20407
  updateAiMentionMenu();
@@ -20463,6 +20531,7 @@ $("#ai-task").addEventListener("change", async (event) => {
20463
20531
  }
20464
20532
  }
20465
20533
  setAiContextMeter(null);
20534
+ updateAiMentionMenu();
20466
20535
  });
20467
20536
  $("#ai-scope").addEventListener("change", (event) => {
20468
20537
  if (state.aiPromptSent) {
@@ -20473,6 +20542,8 @@ $("#ai-scope").addEventListener("change", (event) => {
20473
20542
  setAiContextMeter(null);
20474
20543
  });
20475
20544
  $("#ai-mention-menu").addEventListener("click", (event) => {
20545
+ const skillButton = event.target.closest("[data-ai-skill-name]");
20546
+ if (skillButton) return selectAiSkill(skillButton);
20476
20547
  const button = event.target.closest("[data-ai-reference-id]");
20477
20548
  if (button) selectAiMention(button);
20478
20549
  });
@@ -21016,7 +21087,8 @@ $("#ai-prompt").addEventListener("keydown", (event) => {
21016
21087
  const activeOption = $("#ai-mention-menu").querySelector('[role="option"][aria-selected="true"]');
21017
21088
  if (activeOption) {
21018
21089
  event.preventDefault();
21019
- selectAiMention(activeOption);
21090
+ if (activeOption.dataset.aiSkillName) selectAiSkill(activeOption);
21091
+ else selectAiMention(activeOption);
21020
21092
  return;
21021
21093
  }
21022
21094
  }
@@ -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"></script>
1326
1326
  </body>
1327
1327
  </html>
@@ -697,14 +697,35 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
697
697
  .usage-stat > span { color: var(--muted); font-size: 10px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }
698
698
  .usage-stat-label > span:first-child { min-width: 0; overflow: hidden; color: var(--muted); font-size: 10px; font-weight: 600; letter-spacing: .06em; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; }
699
699
  .usage-cost-bubble { flex: 0 0 auto; max-width: 100%; padding: 6px 10px; overflow: hidden; border: 1px solid color-mix(in srgb, var(--accent) 62%, var(--line)); border-radius: 999px; background: color-mix(in srgb, var(--accent) 22%, var(--surface)); box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent-dark); font: 700 10px/1.1 var(--font-latin), var(--font-cjk), monospace; letter-spacing: .02em; text-overflow: ellipsis; white-space: nowrap; }
700
- .usage-stat strong { margin: 10px 0 5px; overflow: hidden; color: var(--ink); font: 600 clamp(23px, 2.8vw, 34px)/1 var(--font-latin), ui-monospace, monospace; letter-spacing: -.035em; text-overflow: ellipsis; white-space: nowrap; }
700
+ .usage-stat strong { margin: 10px 0 5px; overflow: hidden; color: var(--ink); font: 600 clamp(21px, 2.5vw, 30px)/1 var(--font-latin), ui-monospace, monospace; letter-spacing: -.035em; text-overflow: ellipsis; white-space: nowrap; }
701
701
  .usage-stat.is-primary strong { color: var(--accent-dark); }
702
702
  .usage-stat small { overflow-wrap: anywhere; color: var(--muted); font-size: 9px; line-height: 1.45; }
703
703
  .usage-measurement-note { margin: 11px 0 0; color: var(--muted); font-size: 10px; line-height: 1.6; }
704
704
  .usage-calendar-section, .usage-work-section { margin-top: 26px; padding-top: 22px; border-top: 1px solid var(--line); }
705
705
  .usage-calendar-section > header, .usage-work-section > header { display: flex; align-items: end; justify-content: space-between; gap: 18px; margin-bottom: 14px; }
706
+ .usage-calendar-section > header { align-items: flex-start; }
706
707
  .usage-calendar-section h3, .usage-work-section h3 { margin: 0 0 4px; font-size: 15px; font-weight: 650; }
707
708
  .usage-calendar-section header p, .usage-work-section header p { margin: 0; color: var(--muted); font-size: 10px; }
709
+ .usage-calendar-year-select {
710
+ appearance: none;
711
+ -webkit-appearance: none;
712
+ flex: 0 0 auto;
713
+ min-width: 78px;
714
+ min-height: 30px;
715
+ padding: 5px 27px 5px 9px;
716
+ border: 1px solid var(--line);
717
+ border-radius: 4px;
718
+ background-color: var(--surface);
719
+ background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
720
+ background-position: right 13px center, right 9px center;
721
+ background-repeat: no-repeat;
722
+ background-size: 4px 4px, 4px 4px;
723
+ color: var(--ink);
724
+ font: 10px/1.2 var(--font-latin), var(--font-cjk), monospace;
725
+ cursor: pointer;
726
+ }
727
+ .usage-calendar-year-select:focus-visible { border-color: var(--accent); outline: 2px solid color-mix(in srgb, var(--accent) 18%, transparent); outline-offset: 1px; }
728
+ .usage-calendar-empty { margin: 0; padding: 24px 18px; border: 1px dashed var(--line); border-radius: 6px; color: var(--muted); font-size: 10px; text-align: center; }
708
729
  .usage-calendar-widget { position: relative; }
709
730
  .usage-calendar-scroll { max-width: 100%; padding: 10px 12px 12px; overflow-x: auto; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); }
710
731
  .usage-calendar-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@@ -738,8 +759,8 @@ body.is-panel-resizing { cursor: col-resize; user-select: none; }
738
759
  .usage-calendar-cell[data-level="2"], .usage-calendar-legend i[data-level="2"] { border-color: color-mix(in srgb, var(--accent) 46%, var(--line)); background: color-mix(in srgb, var(--accent) 42%, var(--surface)); }
739
760
  .usage-calendar-cell[data-level="3"], .usage-calendar-legend i[data-level="3"] { border-color: color-mix(in srgb, var(--accent) 72%, var(--line)); background: color-mix(in srgb, var(--accent) 70%, var(--surface)); }
740
761
  .usage-calendar-cell[data-level="4"], .usage-calendar-legend i[data-level="4"] { border-color: var(--accent-dark); background: var(--accent-dark); }
741
- .usage-calendar-cell.is-future { border-color: transparent; background: transparent; cursor: default; }
742
- .usage-calendar-cell:hover, .usage-calendar-cell:focus-visible { position: relative; z-index: 1; border-color: var(--ink); outline: 2px solid color-mix(in srgb, var(--ink) 48%, transparent); outline-offset: 1px; }
762
+ .usage-calendar-cell.is-future, .usage-calendar-cell.is-outside-year { border-color: transparent; background: transparent; cursor: default; }
763
+ button.usage-calendar-cell:hover, button.usage-calendar-cell:focus-visible { position: relative; z-index: 1; border-color: var(--ink); outline: 2px solid color-mix(in srgb, var(--ink) 48%, transparent); outline-offset: 1px; }
743
764
  .usage-calendar-tooltip { position: absolute; z-index: 4; width: max-content; max-width: min(280px, calc(100% - 16px)); padding: 7px 9px; border: 1px solid color-mix(in srgb, var(--ink) 24%, transparent); border-radius: 5px; background: color-mix(in srgb, var(--ink) 94%, transparent); box-shadow: 0 8px 22px rgba(0,0,0,.22); color: var(--panel); font: 10px/1.45 var(--font-latin), var(--font-cjk), monospace; overflow-wrap: anywhere; text-align: center; pointer-events: none; transform: translate(-50%, -100%); }
744
765
  .usage-calendar-tooltip[data-placement="bottom"] { transform: translateX(-50%); }
745
766
  .usage-calendar-tooltip[hidden] { display: none; }
@@ -3156,6 +3177,8 @@ select:focus, input:focus, textarea:focus { border-color: #a77768; box-shadow: 0
3156
3177
  .ai-mention-option:hover, .ai-mention-option:focus-visible, .ai-mention-option.is-active { background: var(--paper-deep); }
3157
3178
  .ai-mention-option small { color: var(--accent); font-size: 8px; }
3158
3179
  .ai-mention-option strong { overflow: hidden; font-size: 10px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
3180
+ .ai-skill-option > span { display: grid; gap: 2px; min-width: 0; }
3181
+ .ai-skill-option em { color: var(--muted); font-size: 9px; font-style: normal; line-height: 1.35; overflow-wrap: anywhere; }
3159
3182
  .ai-mention-empty { margin: 0; padding: 10px; color: var(--muted); font-size: 10px; text-align: center; }
3160
3183
  .ai-prompt { display: block; width: 100%; min-height: 104px; margin: 0; padding: 10px 10px 44px; overflow-y: auto; border: 1px solid var(--line); border-radius: 4px; background: var(--surface); color: var(--ink); font-size: 12px; line-height: 1.45; white-space: pre-wrap; word-break: break-word; cursor: text; }
3161
3184
  .ai-prompt:empty::before { color: var(--muted); content: attr(data-placeholder); pointer-events: none; }
@@ -4519,6 +4542,7 @@ body { font-size: var(--ui-font-size); }
4519
4542
  .eyebrow, .field-label, .dialog-header-meta, .settings-hub-card small, .book-card small, .book-info > span { font-size: calc(10px * var(--ui-font-scale)); }
4520
4543
  .panel-heading, .dialog-fields label, .dialog-fields .form-field-note, .appearance-grid label,
4521
4544
  .config-section-header p, .config-section .config-save-button { font-size: calc(11px * var(--ui-font-scale)); }
4545
+ #work-system-prompt, #remote-mcp-config { font-size: calc(11px * var(--ui-font-scale)); }
4522
4546
  .settings-hub-card strong, .book-info strong { font-size: calc(16px * var(--ui-font-scale)); }
4523
4547
  .shelf-header p, .topbar { font-size: var(--ui-font-size); }
4524
4548
  .appearance-grid select, .dialog-fields input, .dialog-fields textarea, .dialog-fields select { font-size: calc(13px * var(--ui-font-scale)); }
@@ -4550,6 +4574,7 @@ body { font-size: var(--ui-font-size); }
4550
4574
  .ai-context-warning p, .ai-context-warning button { font-size: calc(9px * var(--ai-font-scale)); }
4551
4575
  .prompt-options select, .ai-model-popover > label, .ai-model-option-copy strong, .ai-model-options-empty, .ai-mention-option strong, .ai-mention-empty { font-size: calc(10px * var(--ai-font-scale)); }
4552
4576
  .ai-mention-option small { font-size: calc(8px * var(--ai-font-scale)); }
4577
+ .ai-skill-option em { font-size: calc(9px * var(--ai-font-scale)); }
4553
4578
  .ai-prompt { font-size: var(--ai-font-size); }
4554
4579
  .ai-scene-panel label { font-size: calc(9px * var(--ai-font-scale)); }
4555
4580
  .ai-scene-panel input, .ai-scene-panel textarea { font-size: calc(10px * var(--ai-font-scale)); }
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- export const APP_VERSION = "0.9.6";
1
+ export const APP_VERSION = "0.9.7";
2
2
  export const SCRIVERSE_BETA_COMMIT_ENV = "SCRIVERSE_BETA_COMMIT";
3
3
  export function resolveBetaVersionLabel(environment) {
4
4
  const commit = environment[SCRIVERSE_BETA_COMMIT_ENV]?.trim().toLocaleLowerCase() ?? "";