@klarkxy/dsh-current-title 0.1.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.
package/lib/index.js ADDED
@@ -0,0 +1,714 @@
1
+ import { PLUGIN_NAME, PROMPT_VERSION, PROVIDER_ID, PURPOSE_ID, RPC_CHANNEL, SCHEMA_VERSION, SETTINGS_KEY, defaultGenerateConfig, defaultSettings, isTitleLocaleMode } from "./contracts.js";
2
+ import { registerHostRpc } from "@klarkxy/dsh-ai-services/host-rpc";
3
+ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
4
+ import { z } from "zod";
5
+ //#region src/input.ts
6
+ const FRAME_PREFIX = "Choose the current task from these recent human messages, ordered oldest to newest:\n";
7
+ function frameMessages(messages) {
8
+ return `${FRAME_PREFIX}${JSON.stringify(messages)}`;
9
+ }
10
+ function utf8Bytes(value) {
11
+ return Buffer.byteLength(value, "utf8");
12
+ }
13
+ function middleSlice(codePoints, kept) {
14
+ if (kept >= codePoints.length) return codePoints.join("");
15
+ if (kept <= 0) return "";
16
+ if (kept === 1) return "…";
17
+ const contentPoints = kept - 1;
18
+ const headCount = Math.ceil(contentPoints / 2);
19
+ const tailCount = Math.floor(contentPoints / 2);
20
+ return `${codePoints.slice(0, headCount).join("")}…${tailCount === 0 ? "" : codePoints.slice(-tailCount).join("")}`;
21
+ }
22
+ function fitSingleMessage(message, maxInputBytes) {
23
+ if (utf8Bytes(frameMessages([{
24
+ ...message,
25
+ text: ""
26
+ }])) > maxInputBytes) throw new Error("dsh-current-title: maxInputBytes is too small for the input envelope");
27
+ if (utf8Bytes(frameMessages([message])) <= maxInputBytes) return message;
28
+ const points = Array.from(message.text);
29
+ let low = 0;
30
+ let high = points.length;
31
+ let best = "";
32
+ while (low <= high) {
33
+ const kept = Math.floor((low + high) / 2);
34
+ const text = middleSlice(points, kept);
35
+ if (utf8Bytes(frameMessages([{
36
+ ...message,
37
+ text
38
+ }])) <= maxInputBytes) {
39
+ best = text;
40
+ low = kept + 1;
41
+ } else high = kept - 1;
42
+ }
43
+ return {
44
+ ...message,
45
+ text: best
46
+ };
47
+ }
48
+ function selectRecentMessages(messages, maxRecentMessages, maxInputBytes) {
49
+ if (messages.length === 0) return [];
50
+ const selected = messages.slice(-maxRecentMessages).map((message) => ({ ...message }));
51
+ while (selected.length > 1 && utf8Bytes(frameMessages(selected)) > maxInputBytes) selected.shift();
52
+ if (utf8Bytes(frameMessages(selected)) > maxInputBytes) selected[0] = fitSingleMessage(selected[0], maxInputBytes);
53
+ return selected;
54
+ }
55
+ //#endregion
56
+ //#region src/output.ts
57
+ const TITLE_TYPE_KEYS = [
58
+ "feature",
59
+ "fix",
60
+ "optimize",
61
+ "refactor",
62
+ "test",
63
+ "docs",
64
+ "release",
65
+ "config",
66
+ "explore",
67
+ "discuss"
68
+ ];
69
+ const TITLE_TYPE_LABELS = {
70
+ zh: {
71
+ feature: "功能",
72
+ fix: "修复",
73
+ optimize: "优化",
74
+ refactor: "重构",
75
+ test: "测试",
76
+ docs: "文档",
77
+ release: "发布",
78
+ config: "配置",
79
+ explore: "探索",
80
+ discuss: "讨论"
81
+ },
82
+ en: {
83
+ feature: "Feature",
84
+ fix: "Fix",
85
+ optimize: "Optimize",
86
+ refactor: "Refactor",
87
+ test: "Test",
88
+ docs: "Docs",
89
+ release: "Release",
90
+ config: "Config",
91
+ explore: "Explore",
92
+ discuss: "Discussion"
93
+ }
94
+ };
95
+ const OSC_SEQUENCE = /(?:\u001B\]|\u009D)(?:(?!\u0007|\u001B\\)[\s\S])*(?:\u0007|\u001B\\|$)/gu;
96
+ const CSI_SEQUENCE = /(?:\u001B\[|\u009B)[0-?]*[ -/]*[@-~]/gu;
97
+ const ESC_SEQUENCE = /\u001B[@-_]/gu;
98
+ const CONTROL_CHARACTER = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/gu;
99
+ const DIRECTIONAL_CONTROL = /[\u200B\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF]/gu;
100
+ function cleanTitleText(input) {
101
+ return input.replace(OSC_SEQUENCE, "").replace(CSI_SEQUENCE, "").replace(ESC_SEQUENCE, "").replace(CONTROL_CHARACTER, "").replace(DIRECTIONAL_CONTROL, "").replace(/\s+/gu, " ").trim();
102
+ }
103
+ function truncateTitleUtf8(input, maxBytes) {
104
+ if (!Number.isInteger(maxBytes) || maxBytes <= 0) throw new Error("maxBytes must be a positive integer");
105
+ if (Buffer.byteLength(input, "utf8") <= maxBytes) return input;
106
+ let used = 0;
107
+ let output = "";
108
+ for (const character of input) {
109
+ const bytes = Buffer.byteLength(character, "utf8");
110
+ if (used + bytes > maxBytes) break;
111
+ output += character;
112
+ used += bytes;
113
+ }
114
+ return output;
115
+ }
116
+ /** Same sanitizing rules as DSH 0.1.7-alpha.1 `normalizeSessionTitle`. */
117
+ function normalizeSessionTitle(input, maxBytes) {
118
+ return truncateTitleUtf8(cleanTitleText(input), maxBytes).trimEnd();
119
+ }
120
+ function isTitleType(value) {
121
+ return TITLE_TYPE_KEYS.includes(value);
122
+ }
123
+ function resolveTitleLocale(mode, messages, preference) {
124
+ if (mode !== "auto") return mode;
125
+ const preferred = preference?.trim().toLowerCase().split("-")[0];
126
+ if (preferred === "zh" || preferred === "en") return preferred;
127
+ const text = messages.join("\n");
128
+ if (/[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(text)) return "en";
129
+ return /\p{Script=Han}/u.test(text) ? "zh" : "en";
130
+ }
131
+ function removeTitleSyntax(value) {
132
+ return value.replace(/[|`*_~#\[\]"“”]/gu, " ");
133
+ }
134
+ function truncateSummary(value, targetWords, targetCjkCharacters) {
135
+ if (/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u.test(value)) return Array.from(value.replace(/\s+/gu, "")).slice(0, targetCjkCharacters).join("");
136
+ return value.split(/\s+/u).filter(Boolean).slice(0, targetWords).join(" ");
137
+ }
138
+ function parseModelTitle(raw, targetWords, targetCjkCharacters) {
139
+ let value;
140
+ try {
141
+ value = JSON.parse(raw.trim());
142
+ } catch (cause) {
143
+ throw new Error("dsh-current-title: title model returned invalid JSON", { cause });
144
+ }
145
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("dsh-current-title: title model response must be an object");
146
+ const candidate = value;
147
+ if (Object.keys(candidate).sort().join(",") !== "summary,type") throw new Error("dsh-current-title: title model response must contain only type and summary");
148
+ if (typeof candidate.type !== "string" || !isTitleType(candidate.type)) throw new Error("dsh-current-title: title model returned an unsupported type");
149
+ if (typeof candidate.summary !== "string") throw new Error("dsh-current-title: title model summary must be a string");
150
+ const summary = truncateSummary(normalizeSessionTitle(removeTitleSyntax(candidate.summary), Number.MAX_SAFE_INTEGER), targetWords, targetCjkCharacters);
151
+ if (summary.length === 0) throw new Error("dsh-current-title: title model returned an empty summary");
152
+ return {
153
+ type: candidate.type,
154
+ summary
155
+ };
156
+ }
157
+ function localMonthDay(now) {
158
+ return `${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}`;
159
+ }
160
+ function formatTitle(parsed, locale, now = /* @__PURE__ */ new Date()) {
161
+ return `${localMonthDay(now)} | ${TITLE_TYPE_LABELS[locale][parsed.type]} | ${parsed.summary}`;
162
+ }
163
+ //#endregion
164
+ //#region src/messages.ts
165
+ function textFromContent(content) {
166
+ if (!Array.isArray(content)) return "";
167
+ return content.filter((block) => block && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
168
+ }
169
+ /** Eligible human prompts only. Plugin/tool/model user-role rows are excluded. */
170
+ function collectHumanMessages(events, throughSeq) {
171
+ const messages = [];
172
+ for (const event of events) {
173
+ if (throughSeq !== void 0 && event.seq > throughSeq) break;
174
+ if (event.type !== "user/message") continue;
175
+ if (event.data?.source?.kind !== "user") continue;
176
+ const text = textFromContent(event.data.content);
177
+ if (normalizeSessionTitle(text, Number.MAX_SAFE_INTEGER).length === 0) continue;
178
+ messages.push({
179
+ seq: event.seq,
180
+ text
181
+ });
182
+ }
183
+ return messages;
184
+ }
185
+ function sourceVersionOf(sessionId, messages) {
186
+ return `${sessionId}:${messages.at(-1)?.seq ?? -1}:${messages.length}`;
187
+ }
188
+ //#endregion
189
+ //#region src/generate.ts
190
+ function systemPrompt(config) {
191
+ return [
192
+ "Name the current task in an AI coding-assistant session.",
193
+ "Prioritize the newest human messages. Older messages are context only and must not keep a completed task in the title.",
194
+ "Allowed type values: feature, fix, optimize, refactor, test, docs, release, config, explore, discuss.",
195
+ "Return exactly one JSON object: {\"type\":\"<type>\",\"summary\":\"<summary>\"}.",
196
+ "Return no Markdown, code fence, explanation, date, or extra key.",
197
+ "Keep the summary in the language of the recent messages.",
198
+ `Aim for at most ${config.targetWords} non-CJK words or ${config.targetCjkCharacters} CJK characters.`
199
+ ].join("\n");
200
+ }
201
+ function registerTitlePurpose(scope) {
202
+ return scope.registerPurpose({
203
+ id: PURPOSE_ID,
204
+ label: "当前标题",
205
+ defaultTarget: {
206
+ kind: "role",
207
+ role: "weak"
208
+ },
209
+ maxOutputTokens: 64
210
+ });
211
+ }
212
+ async function generateCurrentTitle(options) {
213
+ options.signal.throwIfAborted();
214
+ if (!options.scope.active || !options.isCurrent()) throw Object.assign(/* @__PURE__ */ new Error("dsh-current-title: generation is no longer current"), { code: "superseded" });
215
+ const selected = selectRecentMessages(options.messages, options.config.maxRecentMessages, options.config.maxInputBytes);
216
+ if (selected.length === 0) throw new Error("dsh-current-title: at least one source message is required");
217
+ const framedInput = frameMessages(selected);
218
+ const locale = resolveTitleLocale(options.localeMode ?? options.config.locale, selected.map((message) => message.text), options.localePreference);
219
+ const sourceVersion = sourceVersionOf(options.sessionId, selected);
220
+ let settled = false;
221
+ const aborted = new Promise((_, reject) => {
222
+ const fail = () => {
223
+ if (settled) return;
224
+ reject(options.signal.reason ?? /* @__PURE__ */ new Error("dsh-current-title: generation cancelled"));
225
+ };
226
+ if (options.signal.aborted) {
227
+ fail();
228
+ return;
229
+ }
230
+ options.signal.addEventListener("abort", fail, { once: true });
231
+ });
232
+ let result;
233
+ try {
234
+ result = await Promise.race([options.scope.run({
235
+ purpose: PURPOSE_ID,
236
+ sessionId: options.sessionId,
237
+ input: framedInput,
238
+ system: systemPrompt(options.config),
239
+ sourceVersion,
240
+ contractVersion: 1,
241
+ promptVersion: PROMPT_VERSION,
242
+ schemaVersion: SCHEMA_VERSION,
243
+ signal: options.signal,
244
+ isCurrent: () => options.scope.active && options.isCurrent(),
245
+ priority: "background"
246
+ }), aborted]);
247
+ } finally {
248
+ settled = true;
249
+ }
250
+ options.signal.throwIfAborted();
251
+ if (!options.scope.active || !options.isCurrent()) throw Object.assign(/* @__PURE__ */ new Error("dsh-current-title: generation is no longer current"), { code: "superseded" });
252
+ if (result.receipt.status === "cancelled" || result.receipt.status === "superseded") throw Object.assign(/* @__PURE__ */ new Error("dsh-current-title: generation did not complete"), { code: result.receipt.status });
253
+ const title = normalizeSessionTitle(formatTitle(parseModelTitle(result.text, options.config.targetWords, options.config.targetCjkCharacters), locale, options.now ?? /* @__PURE__ */ new Date()), options.config.maxTitleBytes);
254
+ if (!title) throw new Error("dsh-current-title: title model returned an empty summary");
255
+ const route = result.receipt.route;
256
+ return {
257
+ title,
258
+ messageSeqs: selected.map((message) => message.seq),
259
+ ...route?.provider && route.model ? { model: {
260
+ provider: route.provider,
261
+ model: route.model
262
+ } } : {},
263
+ sourceVersion
264
+ };
265
+ }
266
+ //#endregion
267
+ //#region src/ownership.ts
268
+ /** Exclusive title-provider ownership. Enable claims the slot; disable releases only while this id still holds it. */
269
+ async function claimTitleSlot(slot, id) {
270
+ return {
271
+ id,
272
+ dispose: await slot.occupy(id)
273
+ };
274
+ }
275
+ async function releaseTitleSlot(slot, claim) {
276
+ if (!claim) return "idle";
277
+ if (slot.owner() !== claim.id) return "left-other-owner";
278
+ await claim.dispose();
279
+ if (slot.owner() && slot.owner() !== claim.id) return "left-other-owner";
280
+ return "released";
281
+ }
282
+ function occupiedProviderId(error) {
283
+ const message = error instanceof Error ? error.message : String(error);
284
+ return /session-title provider "([^"]+)" is already registered/.exec(message)?.[1];
285
+ }
286
+ //#endregion
287
+ //#region src/native-slot.ts
288
+ function catalogLookupId(entryId) {
289
+ const colon = entryId.indexOf(":");
290
+ return colon === -1 ? entryId : entryId.slice(colon + 1);
291
+ }
292
+ function entryName(entry) {
293
+ return String(entry.options?.name ?? "");
294
+ }
295
+ function entryDisabled(entry) {
296
+ return entry.options?.disabled === true;
297
+ }
298
+ function displacementIdentity(entry) {
299
+ const { disabled: _disabled, ...rest } = entry.options ?? {};
300
+ return JSON.stringify({
301
+ id: entry.id,
302
+ name: entryName(entry),
303
+ rest
304
+ });
305
+ }
306
+ function captureDisplacement(entry) {
307
+ return {
308
+ id: entry.id,
309
+ name: entryName(entry),
310
+ identity: displacementIdentity(entry)
311
+ };
312
+ }
313
+ function displacementUnchanged(snapshot, entry) {
314
+ return entry.id === snapshot.id && entryName(entry) === snapshot.name && displacementIdentity(entry) === snapshot.identity;
315
+ }
316
+ function findEntryById(loader, id) {
317
+ for (const entry of loader.entries()) if (entry.id === id) return entry;
318
+ }
319
+ function findLoaderEntry(loader, providerId) {
320
+ for (const entry of loader.entries()) {
321
+ const name = entryName(entry);
322
+ if ([
323
+ entry.id,
324
+ entry.options?.id,
325
+ name,
326
+ catalogLookupId(entry.id)
327
+ ].some((value) => value === providerId)) return entry;
328
+ if (name.endsWith(`/${providerId}`) || name.endsWith(providerId)) return entry;
329
+ if (providerId === "session-title-first-prompt-llm" && catalogLookupId(entry.id) === "session-title-llm") return entry;
330
+ }
331
+ }
332
+ async function restoreOwnDisplacement(loader, snapshot) {
333
+ if (!loader || !snapshot) return "skipped";
334
+ const current = findEntryById(loader, snapshot.id);
335
+ if (!current || !entryDisabled(current) || !displacementUnchanged(snapshot, current)) return "skipped";
336
+ await loader.update(snapshot.id, { disabled: false });
337
+ return "restored";
338
+ }
339
+ function createNativeTitleSlot(options) {
340
+ let owner;
341
+ const slot = {
342
+ owner: () => owner,
343
+ async occupy(id) {
344
+ const provider = {
345
+ id: PROVIDER_ID,
346
+ automatic: "all-prompts",
347
+ generate: options.generate
348
+ };
349
+ try {
350
+ const dispose = options.sessionTitle.register(provider);
351
+ owner = id;
352
+ return async () => {
353
+ await dispose();
354
+ if (owner === id) owner = void 0;
355
+ };
356
+ } catch (error) {
357
+ const occupied = occupiedProviderId(error);
358
+ if (!occupied) throw error;
359
+ const entry = findLoaderEntry(options.loader, occupied);
360
+ if (!entry || entryDisabled(entry)) throw error;
361
+ const snapshot = captureDisplacement(entry);
362
+ await options.loader.update(entry.id, { disabled: true });
363
+ const afterDisable = findEntryById(options.loader, entry.id);
364
+ if (!afterDisable || !displacementUnchanged(snapshot, afterDisable)) throw error;
365
+ try {
366
+ const dispose = options.sessionTitle.register(provider);
367
+ slot.displacement = snapshot;
368
+ owner = id;
369
+ return async () => {
370
+ await dispose();
371
+ if (owner === id) owner = void 0;
372
+ };
373
+ } catch (second) {
374
+ if (findEntryById(options.loader, snapshot.id) && displacementUnchanged(snapshot, findEntryById(options.loader, snapshot.id))) await options.loader.update(snapshot.id, { disabled: false });
375
+ throw second;
376
+ }
377
+ }
378
+ }
379
+ };
380
+ return slot;
381
+ }
382
+ //#endregion
383
+ //#region src/storage.ts
384
+ const titleSettingsSchema = z.object({
385
+ revision: z.number().int().nonnegative(),
386
+ locale: z.enum([
387
+ "auto",
388
+ "zh",
389
+ "en"
390
+ ])
391
+ }).strict();
392
+ const titleDomain = defineDomain({
393
+ name: "dsh_editor_current_title",
394
+ version: 1,
395
+ tables: { settings: domainTable(titleSettingsSchema) }
396
+ });
397
+ function parseTitleSettings(value) {
398
+ const parsed = titleSettingsSchema.safeParse(value);
399
+ if (!parsed.success) return void 0;
400
+ if (!isTitleLocaleMode(parsed.data.locale)) return void 0;
401
+ return parsed.data;
402
+ }
403
+ function storedSettings(value, fallbackLocale = "auto") {
404
+ return parseTitleSettings(value) ?? defaultSettings(fallbackLocale);
405
+ }
406
+ //#endregion
407
+ //#region src/service.ts
408
+ var CurrentTitleService = class {
409
+ options;
410
+ settings;
411
+ config;
412
+ scope;
413
+ unregisterPurpose;
414
+ claim;
415
+ nativeSlot;
416
+ generation = 0;
417
+ inFlight = /* @__PURE__ */ new Map();
418
+ support;
419
+ constructor(options) {
420
+ this.options = options;
421
+ const stored = storedSettings(options.store?.load(), options.initialLocale ?? "auto");
422
+ this.settings = stored;
423
+ this.config = defaultGenerateConfig(this.settings.locale);
424
+ this.support = {
425
+ nativeOwner: null,
426
+ weOwn: false
427
+ };
428
+ }
429
+ async start() {
430
+ const { ai, sessionTitle, loader, sessions } = this.options;
431
+ if (!ai || !sessionTitle?.register || !sessions || !loader) {
432
+ this.support.weOwn = false;
433
+ this.support.limitation = "required host services are missing";
434
+ return;
435
+ }
436
+ this.scope = ai.activate(this.options.plugin);
437
+ this.unregisterPurpose = registerTitlePurpose(this.scope);
438
+ this.nativeSlot = createNativeTitleSlot({
439
+ sessionTitle,
440
+ loader,
441
+ generate: (request) => this.nativeGenerate(request)
442
+ });
443
+ try {
444
+ this.claim = await claimTitleSlot(this.nativeSlot, PROVIDER_ID);
445
+ this.support.weOwn = true;
446
+ this.support.nativeOwner = PROVIDER_ID;
447
+ delete this.support.limitation;
448
+ } catch (error) {
449
+ this.support.weOwn = false;
450
+ this.support.nativeOwner = this.nativeSlot.owner() ?? null;
451
+ this.support.limitation = error instanceof Error ? error.message : "native title provider slot is occupied";
452
+ }
453
+ }
454
+ async dispose() {
455
+ this.generation += 1;
456
+ for (const controller of this.inFlight.values()) controller.abort(/* @__PURE__ */ new Error("current-title disabled"));
457
+ this.inFlight.clear();
458
+ const snapshot = this.nativeSlot?.displacement;
459
+ const outcome = await releaseTitleSlot(this.nativeSlot ?? {
460
+ owner: () => void 0,
461
+ occupy: async () => () => {}
462
+ }, this.claim);
463
+ if (outcome === "released") await restoreOwnDisplacement(this.options.loader, snapshot);
464
+ this.claim = void 0;
465
+ this.support.weOwn = false;
466
+ this.support.nativeOwner = this.nativeSlot?.owner() ?? null;
467
+ if (outcome === "left-other-owner") this.support.limitation = "another title provider owns the slot; previous owner was not restored";
468
+ this.unregisterPurpose?.();
469
+ this.unregisterPurpose = void 0;
470
+ this.scope?.dispose();
471
+ this.scope = void 0;
472
+ }
473
+ status(sessionId) {
474
+ const session = sessionId && this.options.sessions ? this.options.sessions.get(sessionId) : void 0;
475
+ const snapshot = session ? this.read(session) : void 0;
476
+ return {
477
+ settings: { ...this.settings },
478
+ support: {
479
+ ...this.support,
480
+ nativeOwner: this.nativeSlot?.owner() ?? this.support.nativeOwner
481
+ },
482
+ ...sessionId ? { session: {
483
+ sessionId,
484
+ title: snapshot?.title,
485
+ sourceKind: snapshot?.source.kind,
486
+ generating: this.inFlight.has(sessionId),
487
+ pinned: snapshot?.source.kind === "user"
488
+ } } : {}
489
+ };
490
+ }
491
+ async updateLocale(locale, expectedRevision) {
492
+ if (this.settings.revision !== expectedRevision) throw Object.assign(/* @__PURE__ */ new Error("current-title settings changed"), { code: "revision" });
493
+ const next = {
494
+ revision: this.settings.revision + 1,
495
+ locale
496
+ };
497
+ if (this.options.store) await this.options.store.save(next);
498
+ this.settings = next;
499
+ this.config = {
500
+ ...this.config,
501
+ locale
502
+ };
503
+ return { ...this.settings };
504
+ }
505
+ async regenerate(sessionId, signal) {
506
+ if (!this.support.weOwn) throw Object.assign(/* @__PURE__ */ new Error("title provider is inactive"), { code: "unavailable" });
507
+ const session = this.options.sessions?.get(sessionId);
508
+ if (!session) throw Object.assign(/* @__PURE__ */ new Error("session not found"), { code: "not-found" });
509
+ await this.options.sessionTitle?.refresh(session, signal);
510
+ return this.read(session);
511
+ }
512
+ read(session) {
513
+ const snapshot = this.options.sessionTitle?.get(session);
514
+ if (!snapshot?.title) return void 0;
515
+ const kind = snapshot.source?.kind;
516
+ return {
517
+ title: snapshot.title,
518
+ messageSeqs: snapshot.messageSeqs ? [...snapshot.messageSeqs] : [],
519
+ source: {
520
+ kind: kind === "user" || kind === "fallback" || kind === "provider" ? kind : "provider",
521
+ ...typeof snapshot.source?.provider === "string" ? { provider: snapshot.source.provider } : {}
522
+ },
523
+ eventSeq: Number(snapshot.eventSeq ?? 0),
524
+ updatedAt: snapshot.updatedAt ?? 0
525
+ };
526
+ }
527
+ async nativeGenerate(request) {
528
+ const scope = this.scope;
529
+ if (!scope) throw new Error("dsh-current-title: aiServices is not available");
530
+ const generation = this.generation;
531
+ const controller = new AbortController();
532
+ this.inFlight.get(request.session.id)?.abort(/* @__PURE__ */ new Error("newer title generation superseded older work"));
533
+ this.inFlight.set(request.session.id, controller);
534
+ try {
535
+ const combined = AbortSignal.any([
536
+ request.signal,
537
+ controller.signal,
538
+ scope.signal
539
+ ]);
540
+ const session = this.options.sessions?.get(request.session.id);
541
+ const messages = collectHumanMessages(request.messages.map((message) => ({
542
+ type: "user/message",
543
+ seq: message.seq,
544
+ data: {
545
+ source: { kind: "user" },
546
+ content: [{
547
+ type: "text",
548
+ text: message.text
549
+ }]
550
+ }
551
+ })));
552
+ const generated = await generateCurrentTitle({
553
+ scope,
554
+ config: this.config,
555
+ sessionId: request.session.id,
556
+ messages: messages.length > 0 ? messages : [...request.messages],
557
+ localePreference: this.options.localePreference?.(),
558
+ signal: combined,
559
+ isCurrent: () => this.generation === generation && scope.active && this.support.weOwn && (!session || this.options.sessions?.get(request.session.id) === session),
560
+ now: this.options.now?.()
561
+ });
562
+ return {
563
+ title: generated.title,
564
+ messageSeqs: generated.messageSeqs,
565
+ model: generated.model
566
+ };
567
+ } finally {
568
+ if (this.inFlight.get(request.session.id) === controller) this.inFlight.delete(request.session.id);
569
+ }
570
+ }
571
+ };
572
+ function hostLocalePreference(host) {
573
+ try {
574
+ const value = host.get("settings")?.get?.("locale");
575
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
576
+ const preference = value.preference;
577
+ return typeof preference === "string" ? preference : void 0;
578
+ } catch {
579
+ return;
580
+ }
581
+ }
582
+ async function handleRpc(service, endpoint, payload, signal) {
583
+ signal.throwIfAborted();
584
+ const body = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
585
+ if (endpoint === "status") return {
586
+ ok: true,
587
+ value: service.status(typeof body.sessionId === "string" ? body.sessionId : void 0)
588
+ };
589
+ if (endpoint === "settings") {
590
+ if (!isTitleLocaleMode(body.locale) || typeof body.expectedRevision !== "number") return {
591
+ ok: false,
592
+ error: {
593
+ code: "bad-request",
594
+ message: "标题语言设置无效。"
595
+ }
596
+ };
597
+ try {
598
+ return {
599
+ ok: true,
600
+ value: await service.updateLocale(body.locale, body.expectedRevision)
601
+ };
602
+ } catch (error) {
603
+ const code = error instanceof Error && "code" in error ? String(error.code) : "failed";
604
+ if (code === "revision") return {
605
+ ok: false,
606
+ error: {
607
+ code,
608
+ message: "设置已更新,请刷新后重试。"
609
+ }
610
+ };
611
+ return {
612
+ ok: false,
613
+ error: {
614
+ code: "failed",
615
+ message: "无法保存标题设置。"
616
+ }
617
+ };
618
+ }
619
+ }
620
+ if (endpoint === "regenerate") {
621
+ if (typeof body.sessionId !== "string" || !body.sessionId) return {
622
+ ok: false,
623
+ error: {
624
+ code: "bad-request",
625
+ message: "缺少会话。"
626
+ }
627
+ };
628
+ try {
629
+ return {
630
+ ok: true,
631
+ value: await service.regenerate(body.sessionId, signal) ?? null
632
+ };
633
+ } catch (error) {
634
+ const code = error instanceof Error && "code" in error ? String(error.code) : "failed";
635
+ if (code === "not-found") return {
636
+ ok: false,
637
+ error: {
638
+ code,
639
+ message: "找不到该会话。"
640
+ }
641
+ };
642
+ if (code === "unavailable") return {
643
+ ok: false,
644
+ error: {
645
+ code,
646
+ message: "标题服务未就绪。"
647
+ }
648
+ };
649
+ if (signal.aborted) return {
650
+ ok: false,
651
+ error: {
652
+ code: "cancelled",
653
+ message: "已取消。"
654
+ }
655
+ };
656
+ return {
657
+ ok: false,
658
+ error: {
659
+ code: "failed",
660
+ message: "标题生成失败。"
661
+ }
662
+ };
663
+ }
664
+ }
665
+ return {
666
+ ok: false,
667
+ error: {
668
+ code: "bad-request",
669
+ message: "未知操作。"
670
+ }
671
+ };
672
+ }
673
+ //#endregion
674
+ //#region src/index.ts
675
+ const name = PLUGIN_NAME;
676
+ const inject = [
677
+ "sessionTitle",
678
+ "aiServices",
679
+ "sessions",
680
+ "loader",
681
+ "storageDomain",
682
+ "connection",
683
+ "webServer"
684
+ ];
685
+ async function apply(ctx, config = {}) {
686
+ const host = ctx;
687
+ const domain = await host.storageDomain.open(titleDomain);
688
+ const table = domain.table("settings");
689
+ const locale = isTitleLocaleMode(config.locale) ? config.locale : "auto";
690
+ const service = new CurrentTitleService({
691
+ plugin: PLUGIN_NAME,
692
+ ai: host.aiServices,
693
+ sessionTitle: host.sessionTitle,
694
+ loader: host.loader,
695
+ sessions: host.sessions,
696
+ store: {
697
+ load: () => parseTitleSettings(table.get(SETTINGS_KEY)),
698
+ save: (settings) => table.put(SETTINGS_KEY, settings)
699
+ },
700
+ initialLocale: locale,
701
+ localePreference: () => hostLocalePreference(host)
702
+ });
703
+ await service.start();
704
+ ctx.provide("currentTitle", service);
705
+ ctx.effect(() => async () => {
706
+ await service.dispose();
707
+ await domain.close();
708
+ }, "current-title.lifecycle");
709
+ ctx.effect(() => registerHostRpc(host, RPC_CHANNEL, (endpoint, payload, signal) => handleRpc(service, endpoint, payload, signal)), "current-title.rpc");
710
+ }
711
+ //#endregion
712
+ export { CurrentTitleService, apply, formatTitle, frameMessages, inject, name, parseModelTitle, resolveTitleLocale, selectRecentMessages };
713
+
714
+ //# sourceMappingURL=index.js.map