@musnows/scriverse 0.7.13 → 0.8.1

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.
Files changed (52) hide show
  1. package/README.en.md +4 -0
  2. package/README.md +5 -0
  3. package/dist/ai-chat-tab-limit.js +14 -0
  4. package/dist/ai-chat-tab-limit.js.map +1 -0
  5. package/dist/ai-protocol.js +10 -2
  6. package/dist/ai-protocol.js.map +1 -1
  7. package/dist/ai-retry.js +52 -0
  8. package/dist/ai-retry.js.map +1 -0
  9. package/dist/ai.js +547 -119
  10. package/dist/ai.js.map +1 -1
  11. package/dist/app.js +80 -22
  12. package/dist/app.js.map +1 -1
  13. package/dist/cli-contract.js +2 -1
  14. package/dist/cli-contract.js.map +1 -1
  15. package/dist/database.js +127 -2
  16. package/dist/database.js.map +1 -1
  17. package/dist/domain.js +1 -0
  18. package/dist/domain.js.map +1 -1
  19. package/dist/public/ai-chat-tabs.js +75 -0
  20. package/dist/public/ai-request-manager.js +32 -15
  21. package/dist/public/app.js +1305 -298
  22. package/dist/public/background-task-center.d.ts +9 -0
  23. package/dist/public/background-task-center.js +18 -0
  24. package/dist/public/chapter-search.d.ts +6 -0
  25. package/dist/public/chapter-search.js +23 -0
  26. package/dist/public/character-filters.d.ts +3 -1
  27. package/dist/public/character-filters.js +17 -2
  28. package/dist/public/character-version.js +1 -0
  29. package/dist/public/display-labels.d.ts +1 -0
  30. package/dist/public/display-labels.js +5 -1
  31. package/dist/public/index.html +67 -26
  32. package/dist/public/model-config.d.ts +3 -1
  33. package/dist/public/model-config.js +13 -0
  34. package/dist/public/relationship-filters.d.ts +5 -0
  35. package/dist/public/relationship-filters.js +31 -0
  36. package/dist/public/relationship-graph.js +289 -58
  37. package/dist/public/stream-typewriter.d.ts +1 -0
  38. package/dist/public/stream-typewriter.js +12 -0
  39. package/dist/public/styles.css +185 -41
  40. package/dist/public/work-permissions.d.ts +1 -1
  41. package/dist/public/work-permissions.js +13 -1
  42. package/dist/server-runtime.js +6 -0
  43. package/dist/server-runtime.js.map +1 -1
  44. package/dist/store.js +127 -44
  45. package/dist/store.js.map +1 -1
  46. package/dist/user-auth.js +34 -7
  47. package/dist/user-auth.js.map +1 -1
  48. package/dist/version.js +6 -1
  49. package/dist/version.js.map +1 -1
  50. package/dist/work-permissions.js +13 -2
  51. package/dist/work-permissions.js.map +1 -1
  52. package/package.json +1 -1
@@ -25,4 +25,13 @@ export function collectBackgroundTaskTransitions(
25
25
  transitions: BackgroundTaskTransition[];
26
26
  };
27
27
 
28
+ export function filterBackgroundTaskTransitionsForAnnouncement(
29
+ transitions: BackgroundTaskTransition[] | null | undefined,
30
+ previousExpiredNoticeTimes: Map<string, number>,
31
+ now?: number
32
+ ): {
33
+ noticeTimes: Map<string, number>;
34
+ transitions: BackgroundTaskTransition[];
35
+ };
36
+
28
37
  export function backgroundTaskPollDelay(activityCount: unknown, dialogOpen?: boolean): number;
@@ -1,5 +1,6 @@
1
1
  const activeTaskStatuses = new Set(["pending", "running"]);
2
2
  const terminalTaskStatuses = new Set(["review", "completed", "partial", "failed", "expired", "cancelled"]);
3
+ const expiredTransitionNoticeCooldown = 60_000;
3
4
 
4
5
  export function backgroundTaskActivityCount(taskPage, relationshipIndex) {
5
6
  const pendingCount = Number(taskPage?.stats?.pendingCount ?? 0);
@@ -25,6 +26,23 @@ export function collectBackgroundTaskTransitions(previousSnapshots, tasks, initi
25
26
  return { snapshots, transitions };
26
27
  }
27
28
 
29
+ export function filterBackgroundTaskTransitionsForAnnouncement(transitions, previousExpiredNoticeTimes, now = Date.now()) {
30
+ const noticeTimes = new Map(previousExpiredNoticeTimes instanceof Map ? previousExpiredNoticeTimes : []);
31
+ const selected = [];
32
+ for (const transition of Array.isArray(transitions) ? transitions : []) {
33
+ if (transition?.status !== "expired") {
34
+ selected.push(transition);
35
+ continue;
36
+ }
37
+ const taskType = String(transition.task?.taskType ?? "");
38
+ const lastNoticeAt = noticeTimes.get(taskType);
39
+ if (Number.isFinite(lastNoticeAt) && now - lastNoticeAt < expiredTransitionNoticeCooldown) continue;
40
+ noticeTimes.set(taskType, now);
41
+ selected.push(transition);
42
+ }
43
+ return { noticeTimes, transitions: selected };
44
+ }
45
+
28
46
  export function backgroundTaskPollDelay(activityCount, dialogOpen = false) {
29
47
  return dialogOpen || Number(activityCount) > 0 ? 5_000 : 15_000;
30
48
  }
@@ -0,0 +1,6 @@
1
+ export declare function findTextMatches(value: unknown, query: unknown): number[];
2
+
3
+ export declare function replaceTextMatches(value: unknown, query: unknown, replacement: unknown): {
4
+ content: string;
5
+ matches: number;
6
+ };
@@ -0,0 +1,23 @@
1
+ export function findTextMatches(value, query) {
2
+ const source = String(value ?? "");
3
+ const needle = String(query ?? "");
4
+ if (!needle) return [];
5
+ const matches = [];
6
+ let offset = 0;
7
+ while (true) {
8
+ const index = source.indexOf(needle, offset);
9
+ if (index < 0) return matches;
10
+ matches.push(index);
11
+ offset = index + needle.length;
12
+ }
13
+ }
14
+
15
+ export function replaceTextMatches(value, query, replacement) {
16
+ const source = String(value ?? "");
17
+ const needle = String(query ?? "");
18
+ const matches = findTextMatches(source, needle);
19
+ return {
20
+ content: matches.length ? source.replaceAll(needle, () => String(replacement ?? "")) : source,
21
+ matches: matches.length
22
+ };
23
+ }
@@ -3,11 +3,13 @@ type CharacterFilterItem = {
3
3
  race?: { id?: string | null } | null;
4
4
  raceId?: string | null;
5
5
  organizations?: Array<{ id?: string | null }> | null;
6
+ gender?: string | null;
7
+ isDead?: boolean | null;
6
8
  };
7
9
 
8
10
  export declare function filterCharacters<T extends CharacterFilterItem>(
9
11
  characters: T[],
10
- filters?: { raceIds?: string[]; organizationIds?: string[] }
12
+ filters?: { raceIds?: string[]; organizationIds?: string[]; genderValues?: string[]; deathState?: "all" | "alive" | "dead" }
11
13
  ): T[];
12
14
 
13
15
  export declare function paginateCharacters<T>(
@@ -8,14 +8,29 @@ function characterOrganizationIds(character) {
8
8
  .filter(Boolean);
9
9
  }
10
10
 
11
- export function filterCharacters(characters, { raceIds = [], organizationIds = [] } = {}) {
11
+ function characterGender(character) {
12
+ const gender = String(character?.gender ?? "unknown");
13
+ return ["male", "female", "none", "unknown"].includes(gender) ? gender : "unknown";
14
+ }
15
+
16
+ /**
17
+ * @param {unknown[]} characters
18
+ * @param {{ raceIds?: string[]; organizationIds?: string[]; genderValues?: string[]; deathState?: string }} [options]
19
+ */
20
+ export function filterCharacters(characters, options = {}) {
21
+ const { raceIds = [], organizationIds = [], genderValues = [], deathState = "all" } = options;
12
22
  const selectedRaceIds = new Set(raceIds.map(String).filter(Boolean));
13
23
  const selectedOrganizationIds = new Set(organizationIds.map(String).filter(Boolean));
24
+ const selectedGenderValues = new Set(genderValues.map(String).filter(Boolean));
25
+ const selectedDeathState = deathState === "alive" || deathState === "dead" ? deathState : "all";
14
26
  return characters.filter((character) => {
15
27
  const matchesRace = selectedRaceIds.size === 0 || selectedRaceIds.has(characterRaceId(character));
16
28
  const matchesOrganization = selectedOrganizationIds.size === 0
17
29
  || characterOrganizationIds(character).some((organizationId) => selectedOrganizationIds.has(organizationId));
18
- return matchesRace && matchesOrganization;
30
+ const matchesGender = selectedGenderValues.size === 0 || selectedGenderValues.has(characterGender(character));
31
+ const matchesDeathState = selectedDeathState === "all"
32
+ || (selectedDeathState === "dead" ? Boolean(character?.isDead) : !Boolean(character?.isDead));
33
+ return matchesRace && matchesOrganization && matchesGender && matchesDeathState;
19
34
  });
20
35
  }
21
36
 
@@ -1,5 +1,6 @@
1
1
  const fieldLabels = Object.freeze({
2
2
  name: "标准名",
3
+ gender: "性别",
3
4
  isDead: "死亡标识",
4
5
  code: "编号",
5
6
  aliases: "别名",
@@ -15,3 +15,4 @@ export function chapterVersionSourceLabel(value: unknown): string;
15
15
  export function occurrenceRoleLabel(value: unknown): string;
16
16
  export function searchResultTypeLabel(value: unknown): string;
17
17
  export function characterStateFieldLabel(value: unknown): string;
18
+ export function characterGenderLabel(value: unknown): string;
@@ -83,7 +83,7 @@ export function occurrenceRoleLabel(value) {
83
83
 
84
84
  export function searchResultTypeLabel(value) {
85
85
  return enumLabel({
86
- chapter: "章节",
86
+ chapter: "正文章节",
87
87
  setting: "设定",
88
88
  character: "角色",
89
89
  race: "种族",
@@ -105,3 +105,7 @@ export function characterStateFieldLabel(value) {
105
105
  condition: "状况"
106
106
  }, normalized, normalized || "未命名字段");
107
107
  }
108
+
109
+ export function characterGenderLabel(value) {
110
+ return enumLabel({ male: "男 / 雄", female: "女 / 雌", none: "无性别", unknown: "未知" }, value, "未知");
111
+ }
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
13
- <link rel="stylesheet" href="/styles.css?v=20260815-ai-history-favorite-v3&feature=ai-tool-call-copy-v1">
13
+ <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=line-number-gutter-fill-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -134,6 +134,7 @@
134
134
  <button type="button" data-module="races"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="4" r="2"/><circle cx="18" cy="8" r="2"/><circle cx="20" cy="16" r="2"/><path d="M9 10a5 5 0 0 1 5 5v3.5a3.5 3.5 0 0 1-6.84 1.045Q6.52 17.48 4.46 16.84A3.5 3.5 0 0 1 5.5 10Z"/></svg>种族</button>
135
135
  <button class="ai-analysis-entry" type="button" data-module="tasks"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg>AI 分析</button>
136
136
  <button id="module-more-button" type="button" aria-expanded="false"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg><span class="nav-label">更多</span></button>
137
+ <button id="ai-assistant-entry" class="module-nav-secondary hidden ai-assistant-entry" type="button" aria-controls="ai-panel" aria-expanded="false"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v8a2.5 2.5 0 0 1-2.5 2.5H11l-4.5 4v-4h0A2.5 2.5 0 0 1 4 13.5z"/><path d="M8 8h8M8 11h5"/></svg>创作助手</button>
137
138
  <button id="reader-open-button" class="module-nav-secondary hidden" type="button" aria-label="打开沉浸式阅读预览" title="阅读预览"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 5.5A2.5 2.5 0 0 1 5.5 3H11v16H5.5A2.5 2.5 0 0 0 3 21.5Z"/><path d="M21 5.5A2.5 2.5 0 0 0 18.5 3H13v16h5.5a2.5 2.5 0 0 1 2.5 2.5Z"/></svg>阅读预览</button>
138
139
  <button class="module-nav-secondary hidden" type="button" data-module="comments"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4Z"/><path d="M8 9h8M8 13h5"/></svg><span class="nav-label">正文评论</span></button>
139
140
  <button class="module-nav-secondary hidden" type="button" data-module="outlines"><svg class="nav-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg><span class="nav-label">大纲/伏笔</span></button>
@@ -257,6 +258,23 @@
257
258
  <button id="tidy-blank-lines-button" class="ghost-button" type="button">整理空行</button>
258
259
  <button id="save-button" class="primary-button" type="button">保存正文</button>
259
260
  </div>
261
+ <section id="chapter-search-panel" class="chapter-search-panel hidden" role="dialog" aria-labelledby="chapter-search-title" aria-describedby="chapter-search-description" aria-hidden="true">
262
+ <header class="chapter-search-panel-header">
263
+ <div><span class="eyebrow">正文工具</span><h2 id="chapter-search-title">本章搜索与替换</h2><p id="chapter-search-description">只搜索当前打开的正文章节,不需要选择章节。</p></div>
264
+ <button id="chapter-search-close" class="dialog-close" type="button" aria-label="关闭本章搜索">×</button>
265
+ </header>
266
+ <div class="chapter-search-fields">
267
+ <label>查找本章内容<input id="chapter-search-query" type="search" maxlength="500" autocomplete="off" placeholder="输入要查找的文字"></label>
268
+ <label>替换为<input id="chapter-replace-query" type="text" maxlength="200000" autocomplete="off" placeholder="留空表示删除"></label>
269
+ </div>
270
+ <div class="chapter-search-actions">
271
+ <span id="chapter-search-status" role="status" aria-live="polite">输入关键词后开始搜索</span>
272
+ <button id="chapter-search-previous" class="ghost-button" type="button">上一个</button>
273
+ <button id="chapter-search-next" class="ghost-button" type="button">下一个</button>
274
+ <button id="chapter-search-replace" class="ghost-button" type="button">替换</button>
275
+ <button id="chapter-search-replace-all" class="primary-button" type="button">全部替换</button>
276
+ </div>
277
+ </section>
260
278
  </div>
261
279
  <section id="chapter-foreshadow-reminder" class="chapter-foreshadow-reminder hidden" aria-live="polite" aria-labelledby="chapter-foreshadow-reminder-title">
262
280
  <div class="chapter-foreshadow-reminder-summary">
@@ -305,27 +323,36 @@
305
323
  </section>
306
324
  </main>
307
325
 
308
- <aside class="ai-panel">
326
+ <aside id="ai-panel" class="ai-panel">
309
327
  <div id="ai-panel-resize" class="panel-resize-handle" role="separator" aria-label="调整创作助手宽度" aria-orientation="vertical" tabindex="0"></div>
310
328
  <div class="ai-heading">
311
329
  <button id="ai-panel-toggle" class="panel-collapse-button" type="button" aria-label="收起创作助手" aria-expanded="true">›</button>
312
330
  <div class="ai-heading-title"><span id="ai-status-dot" class="status-dot" role="status" aria-label="创作助手状态:正常" title="创作助手状态:正常"></span><strong>创作助手</strong></div>
313
331
  <div class="ai-heading-actions">
314
- <span id="ai-conversation-title" title="当前对话">新对话</span>
332
+ <button id="ai-conversation-switcher" class="ai-conversation-switcher" type="button" aria-label="切换打开的对话" aria-controls="ai-conversation-switcher-menu" aria-expanded="false" aria-haspopup="dialog" title="切换打开的对话"><span id="ai-conversation-title">新对话</span><small id="ai-open-conversation-count">1</small><svg viewBox="0 0 12 8" aria-hidden="true" focusable="false"><path d="M1 1.5 6 6.5l5-5"></path></svg></button>
333
+ <button id="ai-workspace-close" class="hidden" type="button" aria-label="退出多会话工作台">退出</button>
315
334
  <button id="ai-history-toggle" type="button" aria-label="历史记录" aria-controls="ai-history-dialog" aria-expanded="false" aria-haspopup="dialog" title="历史记录"><svg class="ai-heading-action-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"></path><path d="M3 3v5h5M12 7v5l3.5 2"></path></svg></button>
316
335
  <button id="ai-new-conversation" type="button" aria-label="新建对话" title="新建对话">+</button>
317
336
  </div>
318
337
  </div>
338
+ <section id="ai-conversation-switcher-menu" class="ai-conversation-switcher-menu hidden" role="dialog" aria-labelledby="ai-conversation-switcher-title">
339
+ <header><div><strong id="ai-conversation-switcher-title">打开的对话</strong><small id="ai-conversation-switcher-limit">1 / 5</small></div><button id="ai-conversation-switcher-close" type="button" aria-label="关闭对话切换器">×</button></header>
340
+ <div id="ai-open-conversation-list" class="ai-open-conversation-list" role="list"></div>
341
+ <footer><button id="ai-switcher-history" type="button">打开历史记录</button><button id="ai-workspace-open" type="button">进入多会话工作台</button></footer>
342
+ </section>
343
+ <div id="ai-chat-tabs" class="ai-chat-tabs hidden" role="tablist" aria-label="Agent 对话页签" aria-hidden="true"></div>
319
344
  <div class="quick-actions" aria-label="快捷指令">
320
345
  <button type="button" data-task="chat" data-prompt="总结当前章节,并列出尚未解决的冲突。">总结本章</button>
321
346
  <button type="button" data-task="continue" data-prompt="续写当前章节,保持现有叙事风格和人物状态。">续写</button>
322
347
  <button type="button" data-task="chat" data-prompt="提供三种后续剧情方向,并说明各自与既有设定的兼容性。">三种方向</button>
323
348
  <button type="button" data-task="chat" data-prompt="检查当前章节与锁定设定、人物状态和时间线的冲突。">检查冲突</button>
324
349
  </div>
325
- <div id="ai-feed" class="ai-feed">
326
- <div class="assistant-message">
327
- <span>助手</span>
328
- <div class="message-body"><p>选择章节和模型后,可以问答、续写或校对。所有引用都基于已保存正文。</p></div>
350
+ <div id="ai-chat-panels" class="ai-chat-panels">
351
+ <div id="ai-feed" class="ai-feed" role="tabpanel">
352
+ <div class="assistant-message">
353
+ <span>助手</span>
354
+ <div class="message-body"><p>选择章节和模型后,可以问答、续写或校对。所有引用都基于已保存正文。</p></div>
355
+ </div>
329
356
  </div>
330
357
  </div>
331
358
  <div class="prompt-box">
@@ -365,7 +392,7 @@
365
392
  <header class="ai-context-popover-header"><div><strong id="ai-context-popover-title">Token 分布</strong><small id="ai-context-popover-description">按当前模型上下文窗口计算</small></div><button id="ai-context-popover-close" class="ai-context-popover-close" type="button" aria-label="关闭 Token 分布">×</button></header>
366
393
  <div id="ai-context-distribution" class="ai-context-distribution" role="list" aria-label="当前 Token 分布"></div>
367
394
  </section>
368
- <button id="ai-send" class="ai-send-button" type="button" aria-label="发送消息">发送</button>
395
+ <button id="ai-send" class="ai-send-button" type="button" data-state="send" aria-label="发送消息" title="发送消息"><svg class="ai-send-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M21 3 9.5 14.5M21 3l-7 18-4.5-6.5L3 10l18-7Z"></path></svg></button>
369
396
  </div>
370
397
  </div>
371
398
  </div>
@@ -380,24 +407,30 @@
380
407
  <h1 id="reader-title">阅读预览</h1>
381
408
  </div>
382
409
  <details id="reader-settings" class="reader-settings">
383
- <summary>显示设置</summary>
410
+ <summary>阅读设置</summary>
384
411
  <div class="reader-settings-panel">
385
- <label>字号<select id="reader-font-size" aria-label="阅读字号">
386
- <option value="16">16 像素</option><option value="18">18 像素</option><option value="20">20 像素</option><option value="22">22 像素</option><option value="24">24 像素</option>
387
- </select></label>
388
- <label>行距<select id="reader-line-height" aria-label="阅读行距">
389
- <option value="1.6">紧凑 1.6</option><option value="1.8">适中 1.8</option><option value="1.9">舒展 1.9</option><option value="2">宽松 2.0</option><option value="2.2">宽松 2.2</option>
390
- </select></label>
391
- <label>主题<select id="reader-theme" aria-label="阅读主题">
392
- <option value="auto">跟随工作台</option><option value="paper">纸张</option><option value="light">明亮</option><option value="dark">夜间</option>
393
- </select></label>
412
+ <section class="reader-settings-group" aria-labelledby="reader-display-settings-title">
413
+ <span id="reader-display-settings-title" class="reader-settings-section-title">显示设置</span>
414
+ <label>字号<select id="reader-font-size" aria-label="阅读字号">
415
+ <option value="16">16 像素</option><option value="18">18 像素</option><option value="20">20 像素</option><option value="22">22 像素</option><option value="24">24 像素</option>
416
+ </select></label>
417
+ <label>行距<select id="reader-line-height" aria-label="阅读行距">
418
+ <option value="1.6">紧凑 1.6</option><option value="1.8">适中 1.8</option><option value="1.9">舒展 1.9</option><option value="2">宽松 2.0</option><option value="2.2">宽松 2.2</option>
419
+ </select></label>
420
+ <label>主题<select id="reader-theme" aria-label="阅读主题">
421
+ <option value="auto">跟随工作台</option><option value="paper">纸张</option><option value="light">明亮</option><option value="dark">夜间</option>
422
+ </select></label>
423
+ </section>
424
+ <section class="reader-settings-group reader-navigation-group" aria-labelledby="reader-navigation-title">
425
+ <span id="reader-navigation-title" class="reader-settings-section-title">阅读导航</span>
426
+ <div class="reader-jump-controls" aria-label="阅读导航与模式">
427
+ <label>分卷<select id="reader-volume" aria-label="从分卷开始阅读"></select></label>
428
+ <label>章节<select id="reader-chapter" aria-label="快速跳章"></select></label>
429
+ <label>方式<select id="reader-mode" aria-label="阅读方式"><option value="scroll">连续滚动</option><option value="paged">翻页阅读</option></select></label>
430
+ </div>
431
+ </section>
394
432
  </div>
395
433
  </details>
396
- <div class="reader-jump-controls" aria-label="阅读导航与模式">
397
- <label>分卷<select id="reader-volume" aria-label="从分卷开始阅读"></select></label>
398
- <label>章节<select id="reader-chapter" aria-label="快速跳章"></select></label>
399
- <label>方式<select id="reader-mode" aria-label="阅读方式"><option value="scroll">连续滚动</option><option value="paged">翻页阅读</option></select></label>
400
- </div>
401
434
  </header>
402
435
  <main id="reader-viewport" class="reader-viewport" tabindex="0" aria-describedby="reader-keyboard-hint">
403
436
  <article id="reader-document" class="reader-document" aria-live="polite">
@@ -699,7 +732,7 @@
699
732
  <label>关键词<input id="search-query" name="query" type="search" maxlength="100" placeholder="搜索正文、设定、人物、时间线、关系、大纲、伏笔或 Agent 历史" required></label>
700
733
  <label>资料类型<select id="search-type" name="type">
701
734
  <option value="">全部资料</option>
702
- <option value="chapter">章节</option>
735
+ <option value="chapter">正文章节</option>
703
736
  <option value="setting">设定</option>
704
737
  <option value="character">角色</option>
705
738
  <option value="race">种族</option>
@@ -729,6 +762,8 @@
729
762
  <div class="replace-dialog-body">
730
763
  <label class="replace-field">查找内容<input id="replace-find" name="find" type="text" maxlength="500" autocomplete="off" required placeholder="输入要查找的文字"></label>
731
764
  <label class="replace-field">替换为<textarea id="replace-with" name="replacement" maxlength="200000" rows="4" placeholder="输入替换后的文字;留空表示删除"></textarea></label>
765
+ <label class="replace-field">正文分卷<select id="replace-volume" name="volumeId" aria-describedby="replace-volume-description"><option value="">全部分卷(整部作品正文)</option></select></label>
766
+ <p id="replace-volume-description" class="replace-range-note">选择分卷后,只替换该分卷的章节正文;设定库不按分卷筛选。</p>
732
767
  <fieldset class="replace-scope-fieldset" aria-describedby="replace-scope-description">
733
768
  <legend>替换范围</legend>
734
769
  <p id="replace-scope-description">默认只替换章节正文内容。</p>
@@ -1071,7 +1106,7 @@
1071
1106
  <button type="button" data-permission-preset="write">全部可编辑</button>
1072
1107
  </div>
1073
1108
  <div id="member-permission-grid" class="member-permission-grid"></div>
1074
- <small class="member-permission-help">“可编辑”自动包含读取权限;未授权模块不会显示,也不能通过 API 读取。</small>
1109
+ <small class="member-permission-help">正文评论和正文待办的“可添加”只控制新增;删除、内容更新和状态更新需要正文可编辑权限。未授权模块不会显示,也不能通过 API 读取。</small>
1075
1110
  <button id="member-permission-submit" class="primary-button" type="submit">保存成员权限</button>
1076
1111
  </fieldset>
1077
1112
  </form>
@@ -1122,6 +1157,10 @@
1122
1157
  <span>收藏对话</span>
1123
1158
  <small>收藏后不会被清理</small>
1124
1159
  </button>
1160
+ <button type="button" role="menuitem" data-ai-history-action="copy-session-id">
1161
+ <span>复制 Session ID</span>
1162
+ <small>复制用于后端问题定位的对话标识</small>
1163
+ </button>
1125
1164
  <button type="button" role="menuitem" data-ai-history-action="export">
1126
1165
  <span>导出 Markdown</span>
1127
1166
  <small>下载当前对话的角色、时间和消息正文</small>
@@ -1147,6 +1186,8 @@
1147
1186
  <button id="galaxy-close" class="galaxy-close" type="button" aria-label="关闭人物关系银河图" data-testid="galaxy-close">×</button>
1148
1187
  <output id="galaxy-stats" class="galaxy-stats" aria-live="polite" data-testid="galaxy-stats"></output>
1149
1188
  <aside class="galaxy-controls" aria-label="银河图控制">
1189
+ <label class="galaxy-motion-field" for="galaxy-motion-mode"><span>动效</span><select id="galaxy-motion-mode" aria-label="银河图动效模式" aria-describedby="galaxy-motion-status" data-testid="galaxy-motion-mode"><option value="auto">自动</option><option value="on">开启</option><option value="reduced">减少</option><option value="off">关闭</option></select></label>
1190
+ <output id="galaxy-motion-status" class="galaxy-motion-status" aria-live="polite" data-testid="galaxy-motion-status"></output>
1150
1191
  <button id="galaxy-stars" type="button" aria-pressed="true" data-testid="galaxy-stars">隐藏背景星点</button>
1151
1192
  <button id="galaxy-grid" type="button" aria-pressed="false" data-testid="galaxy-grid">显示空间网格</button>
1152
1193
  <button id="galaxy-rotation" type="button" aria-pressed="false" data-testid="galaxy-rotation">暂停旋转</button>
@@ -1164,6 +1205,6 @@
1164
1205
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1165
1206
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1166
1207
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1167
- <script type="module" src="/app.js?v=20260815-ai-stream-persistence-v4&feature=ai-tool-call-copy-v1"></script>
1208
+ <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=calculate-time-tool-v1&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-v1&feature=volume-detail-icon-v1&feature=reader-manual-chapter-navigation-v1&feature=ai-message-reference-badges-v1&feature=ai-message-actions-v1&feature=assistant-responsive-navigation-v3&feature=annotation-permissions-v1&feature=annotation-line-counts-v1"></script>
1168
1209
  </body>
1169
1210
  </html>
@@ -1,4 +1,5 @@
1
1
  export const MODEL_PURPOSE_OPTIONS: ReadonlyArray<readonly [string, string]>;
2
+ export const MODEL_THINKING_EFFORT_OPTIONS: ReadonlyArray<readonly ["default" | "low" | "medium" | "high" | "xhigh" | "max", string]>;
2
3
  export const MIN_MODEL_CONTEXT_WINDOW: number;
3
4
  export const RECOMMENDED_MODEL_CONTEXT_WINDOW: number;
4
5
  export function supportsMultimodalModelProtocol(protocol: string | null | undefined): boolean;
@@ -11,6 +12,7 @@ export type ModelFormValues = {
11
12
  temperature: number;
12
13
  maxTokens: number;
13
14
  thinkingEnabled: boolean;
15
+ thinkingEffort: "default" | "low" | "medium" | "high" | "xhigh" | "max";
14
16
  multimodalEnabled: boolean;
15
17
  imageToolDefault: boolean;
16
18
  enabled: boolean;
@@ -20,5 +22,5 @@ export function normalizeModelPurposes(purposes: unknown): string[];
20
22
  export function isKimiModelId(modelId: unknown): boolean;
21
23
  export function modelContextWindowGuidance(value: unknown): { belowMinimum: boolean; showRecommendation: boolean };
22
24
  export function modelFormValues(model?: Record<string, unknown> | null): ModelFormValues;
23
- export function modelPayload(values: ModelFormValues, existingPreset?: Record<string, unknown>): Record<string, unknown> & { thinkingEnabled: boolean };
25
+ export function modelPayload(values: ModelFormValues, existingPreset?: Record<string, unknown>): Record<string, unknown> & { thinkingEnabled: boolean; thinkingEffort: "default" | "low" | "medium" | "high" | "xhigh" | "max" };
24
26
  export function modelOptionLabel(model: Record<string, unknown> | null | undefined): string;
@@ -9,6 +9,17 @@ export const MODEL_PURPOSE_OPTIONS = Object.freeze([
9
9
  ["consistency-check", "一致性校对"]
10
10
  ]);
11
11
 
12
+ export const MODEL_THINKING_EFFORT_OPTIONS = Object.freeze([
13
+ ["default", "模型默认"],
14
+ ["low", "低(low)"],
15
+ ["medium", "中(medium)"],
16
+ ["high", "高(high)"],
17
+ ["xhigh", "超高(xhigh)"],
18
+ ["max", "最高(max)"]
19
+ ]);
20
+
21
+ const modelThinkingEfforts = new Set(MODEL_THINKING_EFFORT_OPTIONS.map(([value]) => value));
22
+
12
23
  export const MIN_MODEL_CONTEXT_WINDOW = 32_768;
13
24
  export const RECOMMENDED_MODEL_CONTEXT_WINDOW = 128_000;
14
25
 
@@ -56,6 +67,7 @@ export function modelFormValues(model = null) {
56
67
  : (configuredTemperature ?? 0.7),
57
68
  maxTokens: model?.preset?.max_tokens ?? 32000,
58
69
  thinkingEnabled: model?.thinkingEnabled ?? true,
70
+ thinkingEffort: modelThinkingEfforts.has(model?.thinkingEffort) ? model.thinkingEffort : "default",
59
71
  multimodalEnabled: model?.multimodalEnabled ?? false,
60
72
  imageToolDefault: model?.imageToolDefault ?? false,
61
73
  enabled: model?.enabled ?? true
@@ -75,6 +87,7 @@ export function modelPayload(values, existingPreset = {}) {
75
87
  max_tokens: Number(values.maxTokens)
76
88
  },
77
89
  thinkingEnabled: Boolean(values.thinkingEnabled),
90
+ thinkingEffort: modelThinkingEfforts.has(values.thinkingEffort) ? values.thinkingEffort : "default",
78
91
  multimodalEnabled: Boolean(values.multimodalEnabled),
79
92
  imageToolDefault: Boolean(values.imageToolDefault),
80
93
  enabled: Boolean(values.enabled)
@@ -8,3 +8,8 @@ export declare function filterRelationships<T extends RelationshipFilterItem>(
8
8
  relationships: T[],
9
9
  filters?: { fromCharacterIds?: string[]; toCharacterIds?: string[] }
10
10
  ): T[];
11
+
12
+ export declare function sortCharacterRelationships<T extends RelationshipFilterItem & { confidence?: number | string | null; createdAt?: string | null }>(
13
+ relationships: T[],
14
+ characterId: string | null | undefined
15
+ ): T[];
@@ -9,3 +9,34 @@ export function filterRelationships(relationships, { fromCharacterIds = [], toCh
9
9
  return matchesFromCharacter && matchesToCharacter;
10
10
  });
11
11
  }
12
+
13
+ function compareRelationshipText(left, right) {
14
+ const leftText = String(left ?? "");
15
+ const rightText = String(right ?? "");
16
+ if (leftText < rightText) return -1;
17
+ if (leftText > rightText) return 1;
18
+ return 0;
19
+ }
20
+
21
+ function compareRelationshipDetails(left, right) {
22
+ const confidenceDifference = Number(right?.confidence ?? 0) - Number(left?.confidence ?? 0);
23
+ if (confidenceDifference !== 0) return confidenceDifference;
24
+ return compareRelationshipText(left?.createdAt, right?.createdAt)
25
+ || compareRelationshipText(left?.id, right?.id);
26
+ }
27
+
28
+ export function sortCharacterRelationships(relationships, characterId) {
29
+ const currentCharacterId = String(characterId ?? "");
30
+ return [...relationships].sort((left, right) => {
31
+ const leftFromCharacterId = String(left?.fromCharacterId ?? "");
32
+ const rightFromCharacterId = String(right?.fromCharacterId ?? "");
33
+ const leftOtherCharacterId = leftFromCharacterId === currentCharacterId
34
+ ? String(left?.toCharacterId ?? "")
35
+ : leftFromCharacterId;
36
+ const rightOtherCharacterId = rightFromCharacterId === currentCharacterId
37
+ ? String(right?.toCharacterId ?? "")
38
+ : rightFromCharacterId;
39
+ return compareRelationshipText(leftOtherCharacterId, rightOtherCharacterId)
40
+ || compareRelationshipDetails(left, right);
41
+ });
42
+ }