@musnows/scriverse 0.8.8 → 0.9.0

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,21 @@
1
+ import { APP_VERSION } from "./version.js";
2
+ export const DESKTOP_PRODUCT_ID = "scriverse";
3
+ export const DESKTOP_MINIMUM_VERSION = "0.0.1";
4
+ export const DESKTOP_SHELL_PROTOCOL = Object.freeze({ min: 1, max: 1 });
5
+ export const DESKTOP_SYNC_PROTOCOL = Object.freeze({
6
+ min: 1,
7
+ max: 1,
8
+ entityTypes: Object.freeze(["chapter", "setting"]),
9
+ maxMutationBytes: 2_500_000
10
+ });
11
+ export function desktopCompatibilityMetadata() {
12
+ return {
13
+ product: DESKTOP_PRODUCT_ID,
14
+ serverVersion: APP_VERSION,
15
+ webAssetVersion: APP_VERSION,
16
+ shellProtocol: DESKTOP_SHELL_PROTOCOL,
17
+ minimumDesktopVersion: DESKTOP_MINIMUM_VERSION,
18
+ syncProtocol: DESKTOP_SYNC_PROTOCOL
19
+ };
20
+ }
21
+ //# sourceMappingURL=desktop-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"desktop-protocol.js","sourceRoot":"","sources":["../src/desktop-protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,CAAC,MAAM,kBAAkB,GAAG,WAAW,CAAC;AAC9C,MAAM,CAAC,MAAM,uBAAuB,GAAG,OAAO,CAAC;AAC/C,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;AACxE,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC,MAAM,CAAC;IACjD,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,CAAC;IACN,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,SAAS,CAAU,CAAC;IAC3D,gBAAgB,EAAE,SAAS;CAC5B,CAAC,CAAC;AAEH,MAAM,UAAU,4BAA4B;IAQ1C,OAAO;QACL,OAAO,EAAE,kBAAkB;QAC3B,aAAa,EAAE,WAAW;QAC1B,eAAe,EAAE,WAAW;QAC5B,aAAa,EAAE,sBAAsB;QACrC,qBAAqB,EAAE,uBAAuB;QAC9C,YAAY,EAAE,qBAAqB;KACpC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,436 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { AppError } from "./errors.js";
3
+ import { countWords } from "./utils.js";
4
+ export const SYNC_SNAPSHOT_TTL_MS = 15 * 60 * 1_000;
5
+ export const SYNC_SNAPSHOT_PAGE_BYTE_LIMIT = 4 * 1024 * 1024;
6
+ const MAX_ACTIVE_SYNC_SNAPSHOTS = 128;
7
+ function recordArray(value) {
8
+ return Array.isArray(value)
9
+ ? value.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item))
10
+ : [];
11
+ }
12
+ function snapshotItem(sequence, entityType, data) {
13
+ const item = {
14
+ sequence,
15
+ entityType,
16
+ entityId: String(data.id ?? ""),
17
+ versionNo: Number(data.versionNo ?? 0),
18
+ data
19
+ };
20
+ return { ...item, byteLength: Buffer.byteLength(JSON.stringify(item), "utf8") };
21
+ }
22
+ function stableJson(value) {
23
+ if (Array.isArray(value))
24
+ return `[${value.map((item) => stableJson(item)).join(",")}]`;
25
+ if (value && typeof value === "object") {
26
+ return `{${Object.entries(value)
27
+ .filter(([, item]) => item !== undefined)
28
+ .sort(([left], [right]) => left.localeCompare(right))
29
+ .map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`)
30
+ .join(",")}}`;
31
+ }
32
+ return JSON.stringify(value) ?? "null";
33
+ }
34
+ export class OfflineSyncService {
35
+ database;
36
+ store;
37
+ snapshots = new Map();
38
+ now;
39
+ snapshotTtlMs;
40
+ constructor(database, store, options = {}) {
41
+ this.database = database;
42
+ this.store = store;
43
+ this.now = options.now ?? Date.now;
44
+ this.snapshotTtlMs = options.snapshotTtlMs ?? SYNC_SNAPSHOT_TTL_MS;
45
+ }
46
+ createSnapshot(workId, userId) {
47
+ const captured = this.database.transaction(() => {
48
+ this.assertOfflineAccessEnabled(workId);
49
+ const tree = this.store.getWorkTree(workId);
50
+ const volumes = recordArray(tree.volumes);
51
+ const { volumes: _volumes, modulePermissions: _modulePermissions, accessRole: _accessRole, ...work } = tree;
52
+ const rawItems = [
53
+ { entityType: "work", data: work }
54
+ ];
55
+ for (const volumeWithChapters of volumes) {
56
+ const chapters = recordArray(volumeWithChapters.chapters);
57
+ const { chapters: _chapters, ...volume } = volumeWithChapters;
58
+ rawItems.push({ entityType: "volume", data: volume });
59
+ for (const chapter of chapters)
60
+ rawItems.push({ entityType: "chapter", data: chapter });
61
+ }
62
+ for (const setting of this.store.listSettings(workId, true)) {
63
+ rawItems.push({ entityType: "setting", data: setting });
64
+ }
65
+ const cutoffCursor = Number(this.database.get("SELECT COALESCE(MAX(cursor), 0) AS cursor FROM sync_changes WHERE work_id = ?", workId)?.cursor ?? 0);
66
+ return {
67
+ cutoffCursor,
68
+ items: rawItems.map((item, index) => snapshotItem(index + 1, item.entityType, item.data))
69
+ };
70
+ });
71
+ const createdAtMs = this.now();
72
+ this.pruneExpired(createdAtMs);
73
+ for (const [snapshotId, snapshot] of this.snapshots) {
74
+ if (snapshot.userId === userId && snapshot.workId === workId)
75
+ this.snapshots.delete(snapshotId);
76
+ }
77
+ while (this.snapshots.size >= MAX_ACTIVE_SYNC_SNAPSHOTS) {
78
+ const oldest = [...this.snapshots.values()].sort((left, right) => left.createdAtMs - right.createdAtMs)[0];
79
+ if (!oldest)
80
+ break;
81
+ this.snapshots.delete(oldest.snapshotId);
82
+ }
83
+ const snapshot = {
84
+ snapshotId: randomUUID(),
85
+ workId,
86
+ userId,
87
+ cutoffCursor: captured.cutoffCursor,
88
+ itemCount: captured.items.length,
89
+ createdAtMs,
90
+ expiresAtMs: createdAtMs + this.snapshotTtlMs,
91
+ expiresAt: new Date(createdAtMs + this.snapshotTtlMs).toISOString(),
92
+ syncProtocol: 1,
93
+ items: captured.items
94
+ };
95
+ this.snapshots.set(snapshot.snapshotId, snapshot);
96
+ return this.descriptor(snapshot);
97
+ }
98
+ describeOwnedSnapshot(snapshotId, userId) {
99
+ return this.descriptor(this.ownedSnapshot(snapshotId, userId));
100
+ }
101
+ readSnapshotPage(snapshotId, userId, after, limit) {
102
+ const snapshot = this.ownedSnapshot(snapshotId, userId);
103
+ this.assertOfflineAccessEnabled(snapshot.workId);
104
+ const candidates = snapshot.items.filter((item) => item.sequence > after).slice(0, limit);
105
+ const selected = [];
106
+ let selectedBytes = 0;
107
+ for (const item of candidates) {
108
+ if (selected.length > 0 && selectedBytes + item.byteLength > SYNC_SNAPSHOT_PAGE_BYTE_LIMIT)
109
+ break;
110
+ selected.push(item);
111
+ selectedBytes += item.byteLength;
112
+ }
113
+ const lastSequence = selected.at(-1)?.sequence ?? after;
114
+ const hasMore = snapshot.items.some((item) => item.sequence > lastSequence);
115
+ return {
116
+ snapshotId,
117
+ workId: snapshot.workId,
118
+ cutoffCursor: snapshot.cutoffCursor,
119
+ after,
120
+ nextAfter: hasMore ? lastSequence : null,
121
+ hasMore,
122
+ items: selected.map(({ byteLength: _byteLength, ...item }) => item)
123
+ };
124
+ }
125
+ deleteSnapshot(snapshotId, userId) {
126
+ this.ownedSnapshot(snapshotId, userId);
127
+ this.snapshots.delete(snapshotId);
128
+ }
129
+ listChanges(workId, after, limit) {
130
+ return this.database.transaction(() => {
131
+ this.assertOfflineAccessEnabled(workId);
132
+ const rows = this.database.all(`SELECT cursor, entity_type, entity_id, operation, version_no, changed_by_user_id, changed_at
133
+ FROM sync_changes WHERE work_id = ? AND cursor > ? ORDER BY cursor LIMIT ?`, workId, after, limit + 1);
134
+ const hasMore = rows.length > limit;
135
+ const pageRows = rows.slice(0, limit);
136
+ const items = pageRows.map((row) => this.mapChange(row));
137
+ const latestCursor = Number(this.database.get("SELECT COALESCE(MAX(cursor), 0) AS cursor FROM sync_changes WHERE work_id = ?", workId)?.cursor ?? 0);
138
+ return {
139
+ workId,
140
+ after,
141
+ nextCursor: items.at(-1)?.cursor ?? after,
142
+ latestCursor,
143
+ hasMore,
144
+ items
145
+ };
146
+ });
147
+ }
148
+ pushMutations(workId, userId, clientId, mutations) {
149
+ this.assertOfflineAccessEnabled(workId);
150
+ const results = mutations.map((mutation) => this.processMutation(workId, userId, clientId, mutation));
151
+ return {
152
+ clientId,
153
+ results,
154
+ summary: {
155
+ applied: results.filter((result) => result.status === "applied").length,
156
+ conflict: results.filter((result) => result.status === "conflict").length,
157
+ rejected: results.filter((result) => result.status === "rejected").length,
158
+ replayed: results.filter((result) => result.replayed).length
159
+ }
160
+ };
161
+ }
162
+ getMutationResult(workId, userId, mutationId) {
163
+ this.assertOfflineAccessEnabled(workId);
164
+ const row = this.database.get(`SELECT result_json FROM sync_mutation_results
165
+ WHERE mutation_id = ? AND work_id = ? AND user_id = ?`, mutationId, workId, userId);
166
+ if (!row)
167
+ throw new AppError(404, "SYNC_MUTATION_NOT_FOUND", "同步变更结果不存在");
168
+ return this.parseMutationResult(String(row.result_json));
169
+ }
170
+ dispose() {
171
+ this.snapshots.clear();
172
+ }
173
+ ownedSnapshot(snapshotId, userId) {
174
+ const snapshot = this.snapshots.get(snapshotId);
175
+ if (!snapshot || snapshot.userId !== userId) {
176
+ throw new AppError(404, "SYNC_SNAPSHOT_NOT_FOUND", "同步快照不存在");
177
+ }
178
+ if (snapshot.expiresAtMs <= this.now()) {
179
+ this.snapshots.delete(snapshotId);
180
+ throw new AppError(410, "SYNC_SNAPSHOT_EXPIRED", "同步快照已过期,请重新创建");
181
+ }
182
+ return snapshot;
183
+ }
184
+ assertOfflineAccessEnabled(workId) {
185
+ const work = this.database.get("SELECT offline_access_enabled FROM works WHERE id = ? AND deleted_at IS NULL", workId);
186
+ if (!work)
187
+ throw new AppError(404, "WORK_NOT_FOUND", "作品不存在");
188
+ if (Number(work.offline_access_enabled) !== 1) {
189
+ throw new AppError(403, "OFFLINE_ACCESS_DISABLED", "作品所有者尚未允许 Desktop 离线访问");
190
+ }
191
+ }
192
+ mapChange(row) {
193
+ const entityType = String(row.entity_type) === "setting" ? "setting" : "chapter";
194
+ const operation = String(row.operation) === "delete" ? "delete" : "upsert";
195
+ const entityId = String(row.entity_id);
196
+ const versionNo = Number(row.version_no);
197
+ return {
198
+ cursor: Number(row.cursor),
199
+ entityType,
200
+ entityId,
201
+ operation,
202
+ versionNo,
203
+ changedByUserId: row.changed_by_user_id === null || row.changed_by_user_id === undefined
204
+ ? null
205
+ : String(row.changed_by_user_id),
206
+ changedAt: String(row.changed_at),
207
+ data: operation === "delete" ? null : entityType === "chapter"
208
+ ? this.chapterVersionData(entityId, versionNo)
209
+ : this.settingVersionData(entityId, versionNo)
210
+ };
211
+ }
212
+ processMutation(workId, userId, clientId, mutation) {
213
+ const requestHash = createHash("sha256")
214
+ .update(stableJson({ workId, userId, clientId, mutation }))
215
+ .digest("hex");
216
+ return this.database.transaction(() => {
217
+ const existing = this.database.get("SELECT request_hash, result_json FROM sync_mutation_results WHERE mutation_id = ?", mutation.mutationId);
218
+ if (existing) {
219
+ if (String(existing.request_hash) !== requestHash) {
220
+ throw new AppError(409, "MUTATION_ID_REUSED", "mutationId 已用于不同的同步请求");
221
+ }
222
+ return { ...this.parseMutationResult(String(existing.result_json)), replayed: true };
223
+ }
224
+ const result = this.applyMutation(workId, mutation);
225
+ const timestamp = new Date(this.now()).toISOString();
226
+ this.database.run(`INSERT INTO sync_mutation_results (
227
+ mutation_id, client_id, user_id, work_id, request_hash, status, applied_version_no,
228
+ conflict_version_no, error_code, result_json, created_at, updated_at
229
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, mutation.mutationId, clientId, userId, workId, requestHash, result.status, result.appliedVersionNo, result.conflictVersionNo, result.errorCode, JSON.stringify(result), timestamp, timestamp);
230
+ return result;
231
+ });
232
+ }
233
+ applyMutation(workId, mutation) {
234
+ const state = this.currentMutationEntityState(mutation.entityType, mutation.entityId);
235
+ if (!state)
236
+ return this.rejectedMutation(mutation, "SYNC_ENTITY_NOT_FOUND");
237
+ if (state.workId !== workId)
238
+ return this.rejectedMutation(mutation, "SYNC_ENTITY_NOT_FOUND");
239
+ const baseSnapshot = this.mutationVersionData(mutation.entityType, mutation.entityId, mutation.baseVersionNo);
240
+ const serverSnapshot = state.active
241
+ ? this.mutationVersionData(mutation.entityType, mutation.entityId, state.versionNo)
242
+ : { id: mutation.entityId, workId, versionNo: state.versionNo, deleted: true };
243
+ if (!baseSnapshot) {
244
+ return this.rejectedMutation(mutation, "SYNC_BASE_VERSION_MISSING", null, serverSnapshot);
245
+ }
246
+ const localSnapshot = { ...baseSnapshot, ...mutation.localSnapshot, versionNo: mutation.baseVersionNo };
247
+ if (!state.active || state.versionNo !== mutation.baseVersionNo) {
248
+ return {
249
+ mutationId: mutation.mutationId,
250
+ entityType: mutation.entityType,
251
+ entityId: mutation.entityId,
252
+ status: "conflict",
253
+ baseVersionNo: mutation.baseVersionNo,
254
+ appliedVersionNo: null,
255
+ conflictVersionNo: state.versionNo,
256
+ errorCode: "VERSION_CONFLICT",
257
+ baseSnapshot,
258
+ localSnapshot,
259
+ serverSnapshot,
260
+ replayed: false
261
+ };
262
+ }
263
+ try {
264
+ const updated = mutation.entityType === "chapter"
265
+ ? this.store.saveChapter(mutation.entityId, mutation.localSnapshot, "desktop-sync", mutation.mutationId, mutation.changeNote, mutation.baseVersionNo)
266
+ : this.store.updateSetting(mutation.entityId, mutation.localSnapshot, "desktop-sync", mutation.mutationId, mutation.changeNote, mutation.baseVersionNo);
267
+ return {
268
+ mutationId: mutation.mutationId,
269
+ entityType: mutation.entityType,
270
+ entityId: mutation.entityId,
271
+ status: "applied",
272
+ baseVersionNo: mutation.baseVersionNo,
273
+ appliedVersionNo: Number(updated.versionNo),
274
+ conflictVersionNo: null,
275
+ errorCode: null,
276
+ baseSnapshot,
277
+ localSnapshot,
278
+ serverSnapshot: updated,
279
+ replayed: false
280
+ };
281
+ }
282
+ catch (error) {
283
+ if (!(error instanceof AppError))
284
+ throw error;
285
+ const latest = this.currentMutationEntityState(mutation.entityType, mutation.entityId);
286
+ if (error.code === "VERSION_CONFLICT" && latest) {
287
+ return {
288
+ mutationId: mutation.mutationId,
289
+ entityType: mutation.entityType,
290
+ entityId: mutation.entityId,
291
+ status: "conflict",
292
+ baseVersionNo: mutation.baseVersionNo,
293
+ appliedVersionNo: null,
294
+ conflictVersionNo: latest.versionNo,
295
+ errorCode: error.code,
296
+ baseSnapshot,
297
+ localSnapshot,
298
+ serverSnapshot: latest.active
299
+ ? this.mutationVersionData(mutation.entityType, mutation.entityId, latest.versionNo)
300
+ : { id: mutation.entityId, workId, versionNo: latest.versionNo, deleted: true },
301
+ replayed: false
302
+ };
303
+ }
304
+ return this.rejectedMutation(mutation, error.code, baseSnapshot, serverSnapshot, localSnapshot);
305
+ }
306
+ }
307
+ rejectedMutation(mutation, errorCode, baseSnapshot = null, serverSnapshot = null, localSnapshot = null) {
308
+ return {
309
+ mutationId: mutation.mutationId,
310
+ entityType: mutation.entityType,
311
+ entityId: mutation.entityId,
312
+ status: "rejected",
313
+ baseVersionNo: mutation.baseVersionNo,
314
+ appliedVersionNo: null,
315
+ conflictVersionNo: null,
316
+ errorCode,
317
+ baseSnapshot,
318
+ localSnapshot,
319
+ serverSnapshot,
320
+ replayed: false
321
+ };
322
+ }
323
+ currentMutationEntityState(entityType, entityId) {
324
+ if (entityType === "chapter") {
325
+ const chapter = this.database.get("SELECT work_id, version_no, deleted_at FROM chapters WHERE id = ?", entityId);
326
+ if (chapter) {
327
+ return {
328
+ workId: String(chapter.work_id),
329
+ versionNo: Number(chapter.version_no),
330
+ active: chapter.deleted_at === null || chapter.deleted_at === undefined
331
+ };
332
+ }
333
+ const version = this.database.get("SELECT work_id, MAX(version_no) AS version_no FROM chapter_versions WHERE chapter_id = ?", entityId);
334
+ return version?.work_id
335
+ ? { workId: String(version.work_id), versionNo: Number(version.version_no), active: false }
336
+ : null;
337
+ }
338
+ const setting = this.database.get("SELECT work_id FROM settings WHERE id = ?", entityId);
339
+ const version = this.database.get(`SELECT work_id, MAX(version_no) AS version_no FROM entity_versions
340
+ WHERE entity_type = 'setting' AND entity_id = ?`, entityId);
341
+ if (!version?.work_id)
342
+ return null;
343
+ return {
344
+ workId: String(version.work_id),
345
+ versionNo: Number(version.version_no),
346
+ active: Boolean(setting)
347
+ };
348
+ }
349
+ mutationVersionData(entityType, entityId, versionNo) {
350
+ try {
351
+ return entityType === "chapter"
352
+ ? this.chapterVersionData(entityId, versionNo)
353
+ : this.settingVersionData(entityId, versionNo);
354
+ }
355
+ catch (error) {
356
+ if (error instanceof AppError && error.code === "SYNC_CHANGE_HISTORY_MISSING")
357
+ return null;
358
+ throw error;
359
+ }
360
+ }
361
+ parseMutationResult(value) {
362
+ try {
363
+ const parsed = JSON.parse(value);
364
+ if (!parsed || typeof parsed !== "object" || !parsed.mutationId || !parsed.entityId)
365
+ throw new Error("Invalid mutation result");
366
+ if (!["applied", "conflict", "rejected"].includes(parsed.status)) {
367
+ throw new Error("Invalid mutation status");
368
+ }
369
+ return { ...parsed, replayed: false };
370
+ }
371
+ catch {
372
+ throw new AppError(500, "SYNC_MUTATION_RESULT_INVALID", "同步变更结果无法解析");
373
+ }
374
+ }
375
+ chapterVersionData(chapterId, versionNo) {
376
+ const version = this.database.get(`SELECT work_id, chapter_id, version_no, title, content, volume_id, sort_order, chapter_type, created_at
377
+ FROM chapter_versions WHERE chapter_id = ? AND version_no = ?`, chapterId, versionNo);
378
+ if (!version) {
379
+ throw new AppError(500, "SYNC_CHANGE_HISTORY_MISSING", "章节同步历史不完整,请重新下载离线副本");
380
+ }
381
+ const content = String(version.content);
382
+ return {
383
+ id: String(version.chapter_id),
384
+ workId: String(version.work_id),
385
+ volumeId: version.volume_id === null || version.volume_id === undefined ? null : String(version.volume_id),
386
+ title: String(version.title),
387
+ content,
388
+ chapterType: String(version.chapter_type ?? "正文"),
389
+ sortOrder: version.sort_order === null || version.sort_order === undefined ? null : Number(version.sort_order),
390
+ wordCount: countWords(content),
391
+ versionNo: Number(version.version_no),
392
+ updatedAt: String(version.created_at)
393
+ };
394
+ }
395
+ settingVersionData(settingId, versionNo) {
396
+ const version = this.database.get(`SELECT work_id, entity_id, version_no, snapshot_json, created_at
397
+ FROM entity_versions WHERE entity_type = 'setting' AND entity_id = ? AND version_no = ?`, settingId, versionNo);
398
+ if (!version) {
399
+ throw new AppError(500, "SYNC_CHANGE_HISTORY_MISSING", "设定同步历史不完整,请重新下载离线副本");
400
+ }
401
+ let snapshot;
402
+ try {
403
+ const parsed = JSON.parse(String(version.snapshot_json));
404
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
405
+ throw new Error("Invalid setting snapshot");
406
+ snapshot = parsed;
407
+ }
408
+ catch {
409
+ throw new AppError(500, "SYNC_CHANGE_HISTORY_INVALID", "设定同步历史无法解析,请重新下载离线副本");
410
+ }
411
+ return {
412
+ id: String(version.entity_id),
413
+ workId: String(version.work_id),
414
+ ...snapshot,
415
+ versionNo: Number(version.version_no),
416
+ updatedAt: String(version.created_at)
417
+ };
418
+ }
419
+ pruneExpired(timestamp) {
420
+ for (const [snapshotId, snapshot] of this.snapshots) {
421
+ if (snapshot.expiresAtMs <= timestamp)
422
+ this.snapshots.delete(snapshotId);
423
+ }
424
+ }
425
+ descriptor(snapshot) {
426
+ return {
427
+ snapshotId: snapshot.snapshotId,
428
+ workId: snapshot.workId,
429
+ cutoffCursor: snapshot.cutoffCursor,
430
+ itemCount: snapshot.itemCount,
431
+ expiresAt: snapshot.expiresAt,
432
+ syncProtocol: 1
433
+ };
434
+ }
435
+ }
436
+ //# sourceMappingURL=offline-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-sync.js","sourceRoot":"","sources":["../src/offline-sync.ts"],"names":[],"mappings":"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"}
@@ -121,6 +121,10 @@ import {
121
121
  resizeCropRect
122
122
  } from "/avatar-crop.js?v=20260725-avatar-crop";
123
123
 
124
+ const DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS = 300;
125
+ const MIN_AI_ANALYSIS_TIMEOUT_SECONDS = 30;
126
+ const MAX_AI_ANALYSIS_TIMEOUT_SECONDS = 3_600;
127
+
124
128
  const defaultPageSizes = Object.freeze({
125
129
  drafts: 30,
126
130
  settings: 30,
@@ -5143,11 +5147,17 @@ function showAuth(setupRequired, registrationOpen = false, setupTokenRequired =
5143
5147
  function applyAuthenticatedUser(session) {
5144
5148
  state.user = session.user;
5145
5149
  state.csrfToken = session.csrfToken;
5150
+ const isSystemAdmin = session.user.isSystemAdmin === true;
5151
+ const accountButton = $("#account-button");
5146
5152
  $("#account-name").textContent = session.user.displayName;
5147
5153
  renderUserAvatar($("#account-avatar"), session.user);
5154
+ accountButton.setAttribute("aria-label", isSystemAdmin
5155
+ ? `账户:${session.user.displayName},系统管理员`
5156
+ : `账户:${session.user.displayName}`);
5157
+ $("#account-admin-mark").classList.toggle("hidden", !isSystemAdmin);
5148
5158
  $("#account-menu-display-name").textContent = session.user.displayName;
5149
5159
  $("#account-menu-username").textContent = `@${session.user.username}`;
5150
- $("#account-menu-role").textContent = session.user.role === "admin" ? "系统管理员" : "普通用户";
5160
+ $("#account-menu-role").textContent = isSystemAdmin ? "系统管理员" : "普通用户";
5151
5161
  $("#auth-view").classList.add("hidden");
5152
5162
  document.documentElement.classList.remove("login-route");
5153
5163
  if (!session.csrfToken) document.body.classList.remove("auth-pending");
@@ -11375,7 +11385,7 @@ function openTaskDetailDialog(task, trace) {
11375
11385
  <p><strong>状态</strong> ${esc(analysisTaskStatusLabel(task.status))} · 进度 ${Number(task.progress ?? 0)}%</p>
11376
11386
  <p><strong>范围摘要</strong> ${esc(task.scopeSummary || "未指定")}</p>
11377
11387
  <div><strong>范围详情</strong><ul>${detailHtml}</ul></div>
11378
- <div><strong>失败信息</strong>${failureHtml}${identityRepairHtml}</div>
11388
+ <div class="task-detail-failures"><strong>失败信息</strong>${failureHtml}${identityRepairHtml}</div>
11379
11389
  <div><strong>结果摘要</strong>${resultPreview}</div>
11380
11390
  ${canRerunAnalysisTask(task) ? `<div class="task-detail-actions"><button class="primary-button" type="button" data-rerun-task-detail="${esc(task.id)}">按原配置重新执行</button><button class="ghost-button" type="button" data-rerun-task-model="${esc(task.id)}">换模型重试</button><small>新任务会重新读取当前正文、设定和人物资料,旧任务记录保持不变。</small></div>` : ""}
11381
11391
  </section>
@@ -11414,7 +11424,7 @@ function renderProviderCards(providers, models, protocolOptions) {
11414
11424
  : "";
11415
11425
  return `
11416
11426
  <article class="record-card provider-card ${provider.status === "disabled" ? "is-disabled" : ""}"><div class="provider-card-meta"><small>平台级 · ${esc(providerProtocolLabel(provider.protocol, protocolOptions))} · ${esc(providerConnectionLabel(provider.connectionStatus))}</small><span class="provider-status-badge ${providerStatusClass}">${esc(providerStatusLabel(provider.status))}</span></div><h3>${esc(provider.name)}</h3>
11417
- ${disabledNotice}<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n最大输出参数:${esc(provider.maxTokensParameter ?? "max_tokens")}\n思考类型:${esc(provider.thinkingType ?? "enabled")}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit}\n每日 Token 额度:${provider.dailyTokenQuota === null || provider.dailyTokenQuota === undefined ? "未限制" : Number(provider.dailyTokenQuota).toLocaleString("zh-CN")} · 每月 Token 额度:${provider.monthlyTokenQuota === null || provider.monthlyTokenQuota === undefined ? "未限制" : Number(provider.monthlyTokenQuota).toLocaleString("zh-CN")}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
11427
+ ${disabledNotice}<p>${esc(provider.baseUrl)}\n密钥:${esc(provider.apiKey)}\n最大输出参数:${esc(provider.maxTokensParameter ?? "max_tokens")}\n思考类型:${esc(provider.thinkingType ?? "enabled")}\n并发:${provider.concurrencyLimit} · 每分钟请求:${provider.rpmLimit}\n分析请求超时:${Number(provider.analysisTimeoutSeconds ?? DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS).toLocaleString("zh-CN")} 秒\n每日 Token 额度:${provider.dailyTokenQuota === null || provider.dailyTokenQuota === undefined ? "未限制" : Number(provider.dailyTokenQuota).toLocaleString("zh-CN")} · 每月 Token 额度:${provider.monthlyTokenQuota === null || provider.monthlyTokenQuota === undefined ? "未限制" : Number(provider.monthlyTokenQuota).toLocaleString("zh-CN")}${provider.lastError ? `\n错误:${esc(provider.lastError)}` : ""}</p>
11418
11428
  <div class="provider-models">${providerModels.map((model) => {
11419
11429
  const modelUnavailable = !isSelectableModel({ ...model, providerStatus: provider.status, providerConnectionStatus: provider.connectionStatus });
11420
11430
  const modelStatus = !model.enabled
@@ -15810,6 +15820,7 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
15810
15820
  const monthlyTokenQuota = item?.monthlyTokenQuota ?? null;
15811
15821
  const providerTokenQuotaFields = `<div class="form-field provider-token-quota-fields" data-provider-token-quota-fields><span>供应商 Token 额度</span><small>按服务器部署时区统计该供应商跨所有小说的输入与输出 Token;与单个小说额度独立。额度必须设置为大于 0;低于每日 10,000 或每月 1,000,000 时仅提示风险。</small><div class="provider-token-quota-row"><label class="checkbox-field provider-token-quota-toggle"><input name="dailyTokenQuotaEnabled" type="checkbox" ${dailyTokenQuota === null ? "" : "checked"}><span>启用每日额度</span></label><label class="provider-token-quota-input">每日额度<input name="dailyTokenQuota" type="number" min="1" max="2000000000" step="1" value="${esc(String(dailyTokenQuota ?? 10000))}" aria-label="供应商每日 Token 额度" ${dailyTokenQuota === null ? "disabled" : ""}></label></div><div class="provider-token-quota-row"><label class="checkbox-field provider-token-quota-toggle"><input name="monthlyTokenQuotaEnabled" type="checkbox" ${monthlyTokenQuota === null ? "" : "checked"}><span>启用每月额度</span></label><label class="provider-token-quota-input">每月额度<input name="monthlyTokenQuota" type="number" min="1" max="2000000000" step="1" value="${esc(String(monthlyTokenQuota ?? 10000))}" aria-label="供应商每月 Token 额度" ${monthlyTokenQuota === null ? "disabled" : ""}></label></div></div>`;
15812
15822
  const thinkingTypeField = `<div class="form-field provider-thinking-type-field" data-provider-thinking-type-field><span>思考类型(开启时)</span><label class="checkbox-field"><input name="useAdaptiveThinking" type="checkbox" ${useAdaptiveThinking ? "checked" : ""} aria-describedby="provider-thinking-type-hint"><span>使用 adaptive(关闭时发送 enabled)</span></label><small id="provider-thinking-type-hint">关闭模型思考时仍发送 disabled;请只在供应商支持 adaptive 时开启。</small></div>`;
15823
+ const analysisTimeoutField = `<div class="form-field provider-analysis-timeout-field" data-provider-analysis-timeout-field><span>分析请求超时</span><label>单次请求(秒)<input name="analysisTimeoutSeconds" type="number" min="${MIN_AI_ANALYSIS_TIMEOUT_SECONDS}" max="${MAX_AI_ANALYSIS_TIMEOUT_SECONDS}" step="1" value="${esc(String(item?.analysisTimeoutSeconds ?? DEFAULT_AI_ANALYSIS_TIMEOUT_SECONDS))}" aria-describedby="provider-analysis-timeout-hint"></label><small id="provider-analysis-timeout-hint">用于全书分析和关系分析的单次非流式请求;默认 300 秒,可设置 30–3600 秒。</small></div>`;
15813
15824
  openDialog(
15814
15825
  item ? "编辑 AI 供应商" : "新建 AI 供应商",
15815
15826
  field("name", "显示名称", "text", item?.name)
@@ -15820,6 +15831,7 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
15820
15831
  + thinkingTypeField
15821
15832
  + field("concurrencyLimit", "最大并发请求数", "number", item?.concurrencyLimit ?? 10)
15822
15833
  + field("rpmLimit", "每分钟请求上限", "number", item?.rpmLimit ?? 10)
15834
+ + analysisTimeoutField
15823
15835
  + providerTokenQuotaFields
15824
15836
  + field("note", "用途备注", "textarea", item?.note)
15825
15837
  + field("enabled", item ? "启用供应商" : "立即启用", "checkbox", item ? item.status === "enabled" : true),
@@ -15832,11 +15844,17 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
15832
15844
  thinkingType: form.get("useAdaptiveThinking") === "on" ? "adaptive" : "enabled",
15833
15845
  concurrencyLimit: Number(form.get("concurrencyLimit")),
15834
15846
  rpmLimit: Number(form.get("rpmLimit")),
15847
+ analysisTimeoutSeconds: Number(form.get("analysisTimeoutSeconds")),
15835
15848
  dailyTokenQuota: form.get("dailyTokenQuotaEnabled") === "on" ? Number(form.get("dailyTokenQuota")) : null,
15836
15849
  monthlyTokenQuota: form.get("monthlyTokenQuotaEnabled") === "on" ? Number(form.get("monthlyTokenQuota")) : null,
15837
15850
  note: form.get("note"),
15838
15851
  status: form.get("enabled") === "on" ? "enabled" : "disabled"
15839
15852
  };
15853
+ if (
15854
+ !Number.isInteger(body.analysisTimeoutSeconds)
15855
+ || body.analysisTimeoutSeconds < MIN_AI_ANALYSIS_TIMEOUT_SECONDS
15856
+ || body.analysisTimeoutSeconds > MAX_AI_ANALYSIS_TIMEOUT_SECONDS
15857
+ ) throw new Error("分析请求超时必须设置为 30–3600 秒的整数");
15840
15858
  if (!item || String(form.get("apiKey") ?? "").trim()) body.apiKey = form.get("apiKey");
15841
15859
  for (const [period, label] of [["daily", "每日"], ["monthly", "每月"]]) {
15842
15860
  const value = body[`${period}TokenQuota`];
@@ -15849,7 +15867,7 @@ function openProviderDialog(item, protocolOptions = platformAiProtocolOptions) {
15849
15867
  await loadModels();
15850
15868
  if (item) toast(connectivityConfigurationSavedToast("provider"));
15851
15869
  },
15852
- item ? "协议、限流与凭据" : "供应商协议、限流与凭据", {
15870
+ item ? "协议、超时、限流与凭据" : "供应商协议、超时、限流与凭据", {
15853
15871
  dangerAction: item ? { label: "删除供应商", onClick: () => deletePlatformProvider(item) } : null
15854
15872
  }
15855
15873
  );
@@ -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=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&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=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=character-persona-summary-v1&feature=ai-roleplay-scene-turn-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&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=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=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v1&feature=admin-account-identity-v2&feature=task-detail-failure-orange-v1">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -82,7 +82,7 @@
82
82
  <div id="presence-list" class="presence-list" aria-live="polite"></div>
83
83
  </section>
84
84
  </div>
85
- <button id="account-button" class="account-button" type="button" aria-haspopup="menu" aria-expanded="false"><span id="account-avatar" class="user-avatar account-avatar" aria-hidden="true"><span class="user-avatar-fallback">作</span></span><span id="account-name">账户</span></button>
85
+ <button id="account-button" class="account-button" type="button" aria-label="账户" aria-haspopup="menu" aria-expanded="false"><span class="account-avatar-frame"><span id="account-avatar" class="user-avatar account-avatar" aria-hidden="true"><span class="user-avatar-fallback">作</span></span><span id="account-admin-mark" class="account-admin-mark hidden" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M8 1.8 13 3.7v3.7c0 3.1-2 5.6-5 6.8-3-1.2-5-3.7-5-6.8V3.7L8 1.8Z"/><path d="m5.5 7.8 1.6 1.6 3.5-3.5"/></svg></span></span><span class="account-identity"><span id="account-name">账户</span></span></button>
86
86
  <div id="account-menu" class="account-menu hidden" role="menu">
87
87
  <strong id="account-menu-name" class="account-menu-name"><span id="account-menu-display-name">账户</span><span id="account-menu-username" class="account-menu-username">@account</span></strong><small id="account-menu-role">普通用户</small>
88
88
  <button id="onboarding-menu-button" type="button" role="menuitem">功能导览</button>
@@ -1258,6 +1258,6 @@
1258
1258
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
1259
1259
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
1260
1260
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
1261
- <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-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=roleplay-favorite-label-v1&feature=ai-roleplay-knowledge-tools-v1&feature=character-persona-summary-v1&feature=ai-roleplay-scene-turn-v2"></script>
1261
+ <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-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=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"></script>
1262
1262
  </body>
1263
1263
  </html>