@keyflow2/keyflow-kit-tone-rewrite 0.2.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/ui/app/main.js ADDED
@@ -0,0 +1,668 @@
1
+ const KIT_ID = "tone-rewrite";
2
+ const SURFACE = "panel";
3
+ const BUILD_ID = "0.2.0";
4
+ const DEBUG_LOGS = false;
5
+ const SUPPORTED_TONES = ["shorter", "longer", "polite", "casual", "emoji"];
6
+
7
+ function debugLog(message) {
8
+ if (DEBUG_LOGS) {
9
+ console.info(message);
10
+ }
11
+ }
12
+
13
+ debugLog(`[tone-rewrite] boot ${BUILD_ID}`);
14
+
15
+ function safeText(value) {
16
+ return typeof value === "string" ? value : "";
17
+ }
18
+
19
+ function toneMeta(tone) {
20
+ switch (tone) {
21
+ case "shorter":
22
+ return {
23
+ tone,
24
+ label: "更短",
25
+ hint: "压缩赘述,保留重点",
26
+ detail: "压缩成更利落的短消息,不丢掉关键信息。",
27
+ taskTitle: "语气改写:更短",
28
+ instruction: "把原文压缩得更短、更干净,删掉重复和赘述,但不要丢失核心意思。",
29
+ temperature: 0.2,
30
+ maxTokens: 256
31
+ };
32
+ case "longer":
33
+ return {
34
+ tone,
35
+ label: "更长",
36
+ hint: "适度展开,补足表达",
37
+ detail: "在不改变原意的前提下适度展开,让表达更完整。",
38
+ taskTitle: "语气改写:更长",
39
+ instruction: "在不改变原意的前提下适度展开,补足必要语气、解释或承接,但仍保持像真实聊天消息。",
40
+ temperature: 0.4,
41
+ maxTokens: 640
42
+ };
43
+ case "polite":
44
+ return {
45
+ tone,
46
+ label: "更礼貌",
47
+ hint: "更有分寸,更客气",
48
+ detail: "提升礼貌和分寸感,但不要变成生硬模板腔。",
49
+ taskTitle: "语气改写:更礼貌",
50
+ instruction: "把语气改得更礼貌、更有分寸,但仍然自然,不能太官方或太生硬。",
51
+ temperature: 0.28,
52
+ maxTokens: 512
53
+ };
54
+ case "casual":
55
+ return {
56
+ tone,
57
+ label: "更口语",
58
+ hint: "更像真人聊天",
59
+ detail: "改成更自然、更口语化的说法,减少书面感。",
60
+ taskTitle: "语气改写:更口语",
61
+ instruction: "改得更口语、更自然,像真人聊天会说的话,减少书面感和刻意修饰。",
62
+ temperature: 0.38,
63
+ maxTokens: 512
64
+ };
65
+ case "emoji":
66
+ return {
67
+ tone,
68
+ label: "加个 emoji",
69
+ hint: "点到为止,更有气氛",
70
+ detail: "只补 1 到 2 个贴切 emoji,增强氛围,不要堆砌。",
71
+ taskTitle: "语气改写:加个 emoji",
72
+ instruction: "保持原意和整体语气,只在合适位置补 1 到 2 个贴切的 emoji,不要堆砌,也不要显得幼稚。",
73
+ temperature: 0.32,
74
+ maxTokens: 512
75
+ };
76
+ default:
77
+ return toneMeta("shorter");
78
+ }
79
+ }
80
+
81
+ function toneFromBindingId(bindingId) {
82
+ const id = safeText(bindingId);
83
+ const suffix = id.split(".")[1] || "";
84
+ return SUPPORTED_TONES.includes(suffix) ? suffix : null;
85
+ }
86
+
87
+ function viewFromBindingId(bindingId) {
88
+ const tone = toneFromBindingId(bindingId);
89
+ return tone === "casual" ? "panel" : "preview";
90
+ }
91
+
92
+ function parseOpenInvocationHash() {
93
+ const rawHash = safeText(window.location && window.location.hash);
94
+ if (!rawHash || rawHash === "#") return null;
95
+ const query = rawHash.startsWith("#") ? rawHash.slice(1) : rawHash;
96
+ const params = new URLSearchParams(query);
97
+ const kind = safeText(params.get("fk_intent") || params.get("intent"));
98
+ if (kind !== "open_invocation") return null;
99
+ return {
100
+ kind,
101
+ invocationId: safeText(params.get("invocationId")),
102
+ bindingId: safeText(params.get("bindingId")),
103
+ bindingTitle: safeText(params.get("bindingTitle"))
104
+ };
105
+ }
106
+
107
+ function buildPrompt(tone, sourceText) {
108
+ const meta = toneMeta(tone);
109
+ const systemPrompt =
110
+ "你是中文聊天消息的语气改写器。要求:\\n" +
111
+ "1) 保持原意,不编造事实,不添加无关内容。\\n" +
112
+ "2) 尽量保留原有格式(换行/表情/标点)。\\n" +
113
+ "3) 只输出改写后的消息正文,不要解释,不要加引号,不要列点。";
114
+
115
+ const userPrompt =
116
+ `请把下面这段聊天消息改写成「${meta.label}」:\\n\\n` +
117
+ `原文:\\n${sourceText.trim()}\\n\\n` +
118
+ `改写要求:${meta.instruction}`;
119
+
120
+ return {
121
+ systemPrompt,
122
+ userPrompt,
123
+ taskTitle: meta.taskTitle,
124
+ temperature: meta.temperature,
125
+ maxTokens: meta.maxTokens
126
+ };
127
+ }
128
+
129
+ function cleanAiText(text) {
130
+ let value = safeText(text).trim();
131
+ if (!value) return "";
132
+ // Common model artifacts: surrounding quotes or code fences.
133
+ value = value.replace(/^```[a-zA-Z]*\\s*/g, "").replace(/```\\s*$/g, "").trim();
134
+ value = value.replace(/^["“”']+/, "").replace(/["“”']+$/, "").trim();
135
+ return value;
136
+ }
137
+
138
+ function extractContextText(ctx) {
139
+ const safeContext = ctx && typeof ctx === "object" ? ctx : {};
140
+ const before = safeText(safeContext.beforeCursor);
141
+ const selected = safeText(safeContext.selectedText);
142
+ const after = safeText(safeContext.afterCursor);
143
+ const preedit = safeText(safeContext.preeditText);
144
+ const selectedTrimmed = selected.trim();
145
+ if (selectedTrimmed) return selectedTrimmed;
146
+ if (preedit.trim()) return preedit.trim();
147
+ const combinedTrimmed = `${before}${after}`.trim();
148
+ if (combinedTrimmed) return combinedTrimmed;
149
+ return "";
150
+ }
151
+
152
+ function extractSourceText(invocation) {
153
+ const clipboardText = safeText(invocation && invocation.clipboardText);
154
+ const contextText = extractContextText(invocation && invocation.context);
155
+ const trigger = safeText(invocation && invocation.trigger).toLowerCase();
156
+ if (trigger === "clipboard" && clipboardText.trim()) return clipboardText.trim();
157
+ if (contextText) return contextText;
158
+ if (clipboardText.trim()) return clipboardText.trim();
159
+ return "";
160
+ }
161
+
162
+ function userFacingErrorMessage(error) {
163
+ const code = safeText(error && error.code);
164
+ if (code === "permission_denied") return "权限不足:请在功能件设置中允许所需能力。";
165
+ if (code === "ai_request_not_ready") return "宿主 AI 未就绪:请先在输入法设置里配置 AI。";
166
+ return "执行失败:请稍后重试。";
167
+ }
168
+
169
+ const kit = window.FunctionKitRuntimeSDK.createKit({ kitId: KIT_ID, surface: SURFACE });
170
+
171
+ function App() {
172
+ let runSeq = 0;
173
+ let lastHandledInvocationId = null;
174
+ let lastOpenIntentInvocationId = null;
175
+ let expectedInvocationId = null;
176
+ let lastAcceptedInvocationAtEpochMs = 0;
177
+ let connectPromise = null;
178
+ let pendingOpenFallbackToken = 0;
179
+ const autoInvocationStates = new Map();
180
+ const AUTO_INVOCATION_STATE_LIMIT = 24;
181
+
182
+ function pruneAutoInvocationStates() {
183
+ while (autoInvocationStates.size > AUTO_INVOCATION_STATE_LIMIT) {
184
+ const oldestKey = autoInvocationStates.keys().next().value;
185
+ if (!oldestKey) break;
186
+ autoInvocationStates.delete(oldestKey);
187
+ }
188
+ }
189
+
190
+ function reserveAutoInvocation(invocationId) {
191
+ const id = safeText(invocationId);
192
+ if (!id) return true;
193
+ const currentState = autoInvocationStates.get(id);
194
+ if (currentState === "processing" || currentState === "done") {
195
+ return false;
196
+ }
197
+ autoInvocationStates.set(id, "processing");
198
+ pruneAutoInvocationStates();
199
+ return true;
200
+ }
201
+
202
+ function completeAutoInvocation(invocationId) {
203
+ const id = safeText(invocationId);
204
+ if (!id) return;
205
+ autoInvocationStates.set(id, "done");
206
+ pruneAutoInvocationStates();
207
+ }
208
+
209
+ const state = {
210
+ status: "idle", // idle | loading | ready | done | error
211
+ statusText: "选效果 → 点生成",
212
+ statusKind: "muted", // muted | success | error
213
+ busy: false,
214
+ presentation: "panel", // panel | preview
215
+ tone: "shorter",
216
+ actionTitle: "",
217
+ actionBindingId: "",
218
+ actionInvocationId: "",
219
+ sourceText: "",
220
+ resultText: "",
221
+ lastTone: null,
222
+ lastInvocation: null,
223
+ bridgeReady: false,
224
+ toneOptions: SUPPORTED_TONES.map((tone) => toneMeta(tone)),
225
+
226
+ get toneLabel() {
227
+ return toneMeta(this.tone).label;
228
+ },
229
+
230
+ get currentToneHint() {
231
+ return toneMeta(this.tone).detail;
232
+ },
233
+
234
+ get actionSubtitle() {
235
+ const title = safeText(this.actionTitle);
236
+ if (title) return `动作:${title}`;
237
+ const tone = safeText(this.toneLabel);
238
+ return tone ? `效果:${tone}` : "";
239
+ },
240
+
241
+ get canApply() {
242
+ return !this.busy && this.status === "ready" && this.resultText.trim().length > 0;
243
+ },
244
+ get canCopy() {
245
+ return !this.busy && (this.status === "ready" || this.status === "done") && this.resultText.trim().length > 0;
246
+ },
247
+ get canRerun() {
248
+ return !this.busy && this.sourceText.trim().length > 0 && (this.status === "ready" || this.status === "done");
249
+ },
250
+ get canGenerate() {
251
+ return !this.busy && this.sourceText.trim().length > 0;
252
+ },
253
+
254
+ resolveView(invocation) {
255
+ const binding = invocation && invocation.binding && typeof invocation.binding === "object" ? invocation.binding : {};
256
+ const entry = binding.entry && typeof binding.entry === "object" ? binding.entry : {};
257
+ const entryView = safeText(entry.view).toLowerCase();
258
+ if (entryView === "apply" || entryView === "preview" || entryView === "panel") {
259
+ return entryView;
260
+ }
261
+ const preferred = safeText(binding.preferredPresentation).toLowerCase();
262
+ if (preferred.startsWith("apply")) return "apply";
263
+ if (preferred.includes("preview")) return "preview";
264
+ if (preferred.startsWith("panel")) return "panel";
265
+ return "panel";
266
+ },
267
+
268
+ resolveTone(invocation) {
269
+ const binding = invocation && invocation.binding && typeof invocation.binding === "object" ? invocation.binding : {};
270
+ const entry = binding.entry && typeof binding.entry === "object" ? binding.entry : {};
271
+ const entryTone = safeText(entry.tone);
272
+ const inferred =
273
+ entryTone ||
274
+ safeText(binding.id).split(".")[1] ||
275
+ safeText(invocation && invocation.bindingId).split(".")[1] ||
276
+ "shorter";
277
+ return SUPPORTED_TONES.includes(inferred) ? inferred : "shorter";
278
+ },
279
+
280
+ async ensureConnected() {
281
+ if (this.bridgeReady) return true;
282
+ if (!connectPromise) {
283
+ connectPromise = kit
284
+ .connect()
285
+ .then(() => {
286
+ this.bridgeReady = true;
287
+ return true;
288
+ })
289
+ .catch(() => false)
290
+ .finally(() => {
291
+ if (!this.bridgeReady) {
292
+ connectPromise = null;
293
+ }
294
+ });
295
+ }
296
+ return connectPromise;
297
+ },
298
+
299
+ async handleInvocation(invocation) {
300
+ const inv = invocation && typeof invocation === "object" ? invocation : null;
301
+ if (!inv) return;
302
+ const invocationId = safeText(inv.invocationId);
303
+ debugLog(`[tone-rewrite] handleInvocation id=${invocationId || "-"} expected=${expectedInvocationId || "-"}`);
304
+ if (expectedInvocationId && invocationId && invocationId !== expectedInvocationId) {
305
+ return;
306
+ }
307
+ const createdAtEpochMs = Number.isFinite(inv.createdAtEpochMs) ? inv.createdAtEpochMs : 0;
308
+ if (
309
+ !expectedInvocationId &&
310
+ createdAtEpochMs > 0 &&
311
+ createdAtEpochMs < lastAcceptedInvocationAtEpochMs &&
312
+ invocationId !== this.actionInvocationId
313
+ ) {
314
+ return;
315
+ }
316
+ if (invocationId && !reserveAutoInvocation(invocationId)) {
317
+ debugLog(`[tone-rewrite] skip duplicate invocation id=${invocationId}`);
318
+ return;
319
+ }
320
+ try {
321
+ if (invocationId) {
322
+ lastHandledInvocationId = invocationId;
323
+ }
324
+ pendingOpenFallbackToken += 1;
325
+
326
+ const ready = await this.ensureConnected();
327
+ if (!ready) {
328
+ this.status = "error";
329
+ this.statusKind = "error";
330
+ this.statusText = "功能件桥接未就绪";
331
+ return;
332
+ }
333
+
334
+ if (createdAtEpochMs > 0) {
335
+ lastAcceptedInvocationAtEpochMs = Math.max(lastAcceptedInvocationAtEpochMs, createdAtEpochMs);
336
+ }
337
+ if (!expectedInvocationId || !invocationId || expectedInvocationId === invocationId) {
338
+ expectedInvocationId = null;
339
+ }
340
+
341
+ this.lastInvocation = inv;
342
+ const binding = inv.binding && typeof inv.binding === "object" ? inv.binding : {};
343
+ this.actionTitle = safeText(binding.title);
344
+ this.actionBindingId = safeText(binding.id);
345
+ this.actionInvocationId = invocationId;
346
+ const tone = this.resolveTone(inv);
347
+ const view = this.resolveView(inv);
348
+
349
+ this.tone = tone;
350
+ // Headless runs should still land on a lightweight preview when user taps "打开".
351
+ this.presentation = view === "preview" || view === "apply" ? "preview" : "panel";
352
+ this.sourceText = extractSourceText(inv);
353
+ this.resultText = "";
354
+ this.lastTone = null;
355
+ this.status = "idle";
356
+ this.statusKind = "muted";
357
+ this.statusText = "准备生成…";
358
+
359
+ if (!this.sourceText.trim()) {
360
+ await this.useCurrentInput();
361
+ }
362
+
363
+ if (!this.sourceText.trim()) {
364
+ this.status = "error";
365
+ this.statusKind = "error";
366
+ this.statusText = "没有可改写的文本";
367
+ return;
368
+ }
369
+
370
+ if (view === "apply") {
371
+ await this.runTone(this.tone);
372
+ if (this.status === "ready") {
373
+ this.statusText = `已生成(${this.toneLabel}):点一下替换`;
374
+ }
375
+ return;
376
+ }
377
+
378
+ await this.runTone(this.tone);
379
+ if (view === "preview" && this.status === "ready") {
380
+ this.statusText = `已生成预览(${this.toneLabel}):点一下替换`;
381
+ }
382
+ } finally {
383
+ completeAutoInvocation(invocationId);
384
+ }
385
+ },
386
+
387
+ async init() {
388
+ kit.bindings.onInvoke(({ invocation }) => {
389
+ this.handleInvocation(invocation);
390
+ });
391
+ this.scheduleOpenInvocationFallback = async ({ invocationId, tone }) => {
392
+ const token = ++pendingOpenFallbackToken;
393
+ debugLog(`[tone-rewrite] scheduleFallback id=${invocationId || "-"} tone=${tone} token=${token}`);
394
+ await new Promise((resolve) => window.setTimeout(resolve, 180));
395
+ if (token !== pendingOpenFallbackToken) {
396
+ debugLog(`[tone-rewrite] cancelFallback id=${invocationId || "-"} token=${token} current=${pendingOpenFallbackToken}`);
397
+ return;
398
+ }
399
+ if (invocationId && lastHandledInvocationId === invocationId) {
400
+ debugLog(`[tone-rewrite] fallbackAlreadyHandled id=${invocationId}`);
401
+ return;
402
+ }
403
+ if (invocationId && expectedInvocationId && expectedInvocationId !== invocationId) {
404
+ debugLog(`[tone-rewrite] fallbackMismatched id=${invocationId} expected=${expectedInvocationId}`);
405
+ return;
406
+ }
407
+ if (!this.sourceText.trim()) {
408
+ await this.useCurrentInput();
409
+ }
410
+ if (this.sourceText.trim()) {
411
+ debugLog(`[tone-rewrite] fallbackRun id=${invocationId || "-"} tone=${tone} sourceLen=${this.sourceText.trim().length}`);
412
+ await this.runTone(tone);
413
+ }
414
+ };
415
+ this.handleOpenInvocationIntent = async (intent) => {
416
+ const kind = safeText(intent && intent.kind);
417
+ if (kind !== "open_invocation") return;
418
+ const ready = await this.ensureConnected();
419
+ if (!ready) {
420
+ this.status = "error";
421
+ this.statusKind = "error";
422
+ this.statusText = "功能件桥接未就绪";
423
+ return;
424
+ }
425
+
426
+ const id = safeText(intent && intent.invocationId);
427
+ const bindingId = safeText(intent && intent.bindingId);
428
+ debugLog(`[tone-rewrite] handleOpenInvocationIntent id=${id || "-"} binding=${bindingId || "-"} lastHandled=${lastHandledInvocationId || "-"} lastOpen=${lastOpenIntentInvocationId || "-"}`);
429
+ if (id && id === lastHandledInvocationId) {
430
+ return;
431
+ }
432
+ if (id && id === lastOpenIntentInvocationId) {
433
+ return;
434
+ }
435
+ if (id) {
436
+ lastOpenIntentInvocationId = id;
437
+ }
438
+ if (id) {
439
+ expectedInvocationId = id;
440
+ }
441
+
442
+ const bindingTitle = safeText(intent && intent.bindingTitle);
443
+ const isNewInvocation = id && id !== this.actionInvocationId;
444
+ if (isNewInvocation) {
445
+ this.lastInvocation = null;
446
+ this.sourceText = "";
447
+ this.resultText = "";
448
+ this.lastTone = null;
449
+ this.status = "idle";
450
+ this.statusKind = "muted";
451
+ this.statusText = "准备生成…";
452
+ }
453
+ if (bindingId) {
454
+ this.actionBindingId = bindingId;
455
+ }
456
+ this.actionTitle = bindingTitle || this.actionTitle || "";
457
+ this.actionInvocationId = id || this.actionInvocationId || "";
458
+
459
+ const last = kit.state && kit.state.lastInvocation ? kit.state.lastInvocation : null;
460
+ const matchingLast = last && id && safeText(last.invocationId) === id ? last : null;
461
+
462
+ if (matchingLast) {
463
+ debugLog(`[tone-rewrite] openIntentMatchedLast id=${id}`);
464
+ await this.handleInvocation(matchingLast);
465
+ return;
466
+ }
467
+
468
+ const tone = toneFromBindingId(bindingId) || this.tone;
469
+ if (tone !== this.tone) {
470
+ this.tone = tone;
471
+ }
472
+
473
+ const view = viewFromBindingId(bindingId);
474
+ this.presentation = view === "panel" ? "panel" : "preview";
475
+
476
+ if (id) {
477
+ this.scheduleOpenInvocationFallback({ invocationId: id, tone: this.tone });
478
+ return;
479
+ }
480
+ if (!this.sourceText.trim()) {
481
+ await this.useCurrentInput();
482
+ }
483
+ if (this.sourceText.trim()) {
484
+ await this.runTone(this.tone);
485
+ }
486
+ };
487
+
488
+ kit.runtime.onIntent(async ({ intent }) => {
489
+ this.handleOpenInvocationIntent(intent);
490
+ });
491
+
492
+ const handleHashIntent = async () => {
493
+ const intent = parseOpenInvocationHash();
494
+ if (intent) {
495
+ await this.handleOpenInvocationIntent(intent);
496
+ }
497
+ };
498
+ window.addEventListener("hashchange", () => {
499
+ handleHashIntent();
500
+ });
501
+
502
+ await this.ensureConnected();
503
+
504
+ await handleHashIntent();
505
+
506
+ const bootInvocation = kit.state && kit.state.lastInvocation ? kit.state.lastInvocation : null;
507
+ const bootInvocationId = safeText(bootInvocation && bootInvocation.invocationId);
508
+ if (
509
+ bootInvocation &&
510
+ bootInvocationId !== lastHandledInvocationId &&
511
+ (!expectedInvocationId || !bootInvocationId || bootInvocationId === expectedInvocationId)
512
+ ) {
513
+ debugLog(`[tone-rewrite] bootInvocation id=${bootInvocationId || "-"}`);
514
+ await this.handleInvocation(bootInvocation);
515
+ }
516
+ },
517
+
518
+ setTone(tone) {
519
+ if (this.busy) return;
520
+ if (!SUPPORTED_TONES.includes(tone)) return;
521
+ if (this.tone === tone) return;
522
+ this.tone = tone;
523
+ this.lastTone = null;
524
+ this.resultText = "";
525
+ this.status = "idle";
526
+ this.statusKind = "muted";
527
+ this.statusText = this.sourceText.trim() ? `已选择${toneMeta(tone).label}:点生成` : "请输入或读取原文";
528
+ },
529
+
530
+ generate() {
531
+ this.runTone(this.tone);
532
+ },
533
+
534
+ async useCurrentInput() {
535
+ if (this.busy) return;
536
+ const ready = await this.ensureConnected();
537
+ if (!ready) {
538
+ this.status = "error";
539
+ this.statusKind = "error";
540
+ this.statusText = "读取失败";
541
+ return;
542
+ }
543
+ try {
544
+ const ctx = await kit.context.refresh();
545
+ const before = safeText(ctx && ctx.beforeCursor);
546
+ const after = safeText(ctx && ctx.afterCursor);
547
+ const selected = safeText(ctx && ctx.selectedText);
548
+ const preedit = safeText(ctx && ctx.preeditText);
549
+ const selectedTrimmed = selected.trim();
550
+ const combinedTrimmed = `${before}${after}`.trim();
551
+ const text = selectedTrimmed || preedit.trim() || combinedTrimmed;
552
+ if (!text.trim()) {
553
+ this.status = "error";
554
+ this.statusKind = "error";
555
+ this.statusText = "当前输入为空";
556
+ return;
557
+ }
558
+ this.sourceText = text;
559
+ this.status = "idle";
560
+ this.statusKind = "muted";
561
+ this.statusText = "已读取当前输入";
562
+ } catch (_error) {
563
+ this.status = "error";
564
+ this.statusKind = "error";
565
+ this.statusText = "读取失败";
566
+ }
567
+ },
568
+
569
+ clearAll() {
570
+ if (this.busy) return;
571
+ this.sourceText = "";
572
+ this.resultText = "";
573
+ this.status = "idle";
574
+ this.statusKind = "muted";
575
+ this.statusText = "选效果 → 点生成";
576
+ },
577
+
578
+ async runTone(tone) {
579
+ this.tone = tone;
580
+ const text = this.sourceText.trim();
581
+ debugLog(`[tone-rewrite] runTone tone=${tone} actionId=${this.actionInvocationId || "-"} sourceLen=${text.length}`);
582
+ if (!text) {
583
+ this.status = "error";
584
+ this.statusKind = "error";
585
+ this.statusText = "请输入或读取原文";
586
+ return;
587
+ }
588
+ const ready = await this.ensureConnected();
589
+ if (!ready) {
590
+ this.status = "error";
591
+ this.statusKind = "error";
592
+ this.statusText = "宿主连接失败";
593
+ return;
594
+ }
595
+
596
+ const seq = ++runSeq;
597
+ this.busy = true;
598
+ this.status = "loading";
599
+ this.statusKind = "muted";
600
+ this.statusText = "生成中…";
601
+ this.resultText = "";
602
+
603
+ try {
604
+ const { systemPrompt, userPrompt, taskTitle, temperature, maxTokens } = buildPrompt(tone, text);
605
+ const resp = await kit.ai.request({
606
+ task: { title: taskTitle },
607
+ systemPrompt,
608
+ messages: [{ role: "user", content: userPrompt }],
609
+ temperature,
610
+ maxTokens
611
+ });
612
+
613
+ if (seq !== runSeq) return;
614
+ const out = cleanAiText(resp && resp.output && resp.output.text);
615
+ if (!out) {
616
+ this.status = "error";
617
+ this.statusKind = "error";
618
+ this.statusText = "未生成结果";
619
+ return;
620
+ }
621
+ this.resultText = out;
622
+ this.lastTone = tone;
623
+ this.status = "ready";
624
+ this.statusKind = "success";
625
+ this.statusText = `已生成${toneMeta(tone).label}版本:点一下替换`;
626
+ } catch (error) {
627
+ if (seq !== runSeq) return;
628
+ this.status = "error";
629
+ this.statusKind = "error";
630
+ this.statusText = userFacingErrorMessage(error);
631
+ } finally {
632
+ if (seq === runSeq) {
633
+ this.busy = false;
634
+ }
635
+ }
636
+ },
637
+
638
+ applyReplace() {
639
+ if (!this.canApply) return;
640
+ kit.input.replace(this.resultText.trim());
641
+ this.status = "done";
642
+ this.statusKind = "success";
643
+ this.statusText = "已替换到输入框";
644
+ },
645
+
646
+ async copyResult() {
647
+ if (!this.canCopy) return;
648
+ try {
649
+ await navigator.clipboard.writeText(this.resultText.trim());
650
+ this.statusKind = "success";
651
+ this.statusText = "已复制";
652
+ } catch (_error) {
653
+ this.statusKind = "error";
654
+ this.statusText = "复制失败";
655
+ }
656
+ },
657
+
658
+ rerun() {
659
+ const tone = this.lastTone || this.tone;
660
+ this.runTone(tone);
661
+ }
662
+ };
663
+ return state;
664
+ }
665
+
666
+ const appState = window.PetiteVue.reactive(App());
667
+ window.PetiteVue.createApp(appState).mount("#app");
668
+ appState.init();