lume-dsh-plugin 0.4.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.
package/lib/client.js ADDED
@@ -0,0 +1,2395 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "lume-dsh-plugin",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let react = require("react");
10
+ let react_jsx_runtime = require("react/jsx-runtime");
11
+
12
+ //#region src/core/dialogue-mining.ts
13
+ /** 聊天记录时间戳行:2026年08月31日 00:40(也兼容 2026-08-31 00:40)。 */
14
+ const CHAT_TS_RE = /^\d{4}[-年]\d{1,2}[-月]\d{1,2}日?\s+\d{1,2}:\d{2}/;
15
+ /** 非文本消息占位:[语音] 3" / [图片] 微信图片_xxx.jpg / [动画表情]。 */
16
+ const CHAT_PLACEHOLDER_RE = /^\[(语音|图片|视频|动画表情|表情|文件|链接|转账|红包|位置|名片|小程序|引用|音乐|语音通话|视频通话|接龙|笔记|收藏)/;
17
+ /** 纯方括号短占位(QQ 表情名如 [无语]、[捂脸])。 */
18
+ const CHAT_EMOJI_RE = /^\[[^\]\s]{1,8}\]$/;
19
+ /** 对象替换符(微信复制时图片/表情的残留)。 */
20
+ const OBJ_REPLACEMENT = "";
21
+ /**
22
+ * 解析聊天记录导出文本。识别不出聊天结构(时间戳锚点不足 / 说话人单一)
23
+ * 返回 null——调用方回退到小说/剧本挖掘。
24
+ *
25
+ * 结构锚点:说话人独占一行,紧跟时间戳行。解析用前瞻——若某行的下一行是
26
+ * 时间戳,则该行是说话人,内容从再下一行起,直到下一个说话人行。
27
+ */
28
+ function parseChatLog(text) {
29
+ const lines = text.split("\n");
30
+ const messages = [];
31
+ const counts = /* @__PURE__ */ new Map();
32
+ let i = 0;
33
+ while (i < lines.length - 1) {
34
+ if (!CHAT_TS_RE.test(lines[i + 1].trim())) {
35
+ i++;
36
+ continue;
37
+ }
38
+ const speaker = lines[i].trim();
39
+ if (!speaker) {
40
+ i++;
41
+ continue;
42
+ }
43
+ const timestamp = lines[i + 1].trim();
44
+ const content = [];
45
+ let j = i + 2;
46
+ while (j < lines.length) {
47
+ if (j + 1 < lines.length && lines[j].trim() && CHAT_TS_RE.test(lines[j + 1].trim())) break;
48
+ content.push(lines[j]);
49
+ j++;
50
+ }
51
+ const cleaned = cleanChatContent(content);
52
+ if (cleaned) {
53
+ messages.push({
54
+ speaker,
55
+ text: cleaned,
56
+ timestamp
57
+ });
58
+ counts.set(speaker, (counts.get(speaker) ?? 0) + 1);
59
+ }
60
+ i = j;
61
+ }
62
+ if (messages.length < 4 || counts.size < 2) return null;
63
+ return {
64
+ messages,
65
+ speakers: [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([name]) => name)
66
+ };
67
+ }
68
+ /** 清洗一条消息的原始行:剔除占位符、对象替换符、空行,合并多行。 */
69
+ function cleanChatContent(lines) {
70
+ const kept = [];
71
+ for (const raw of lines) {
72
+ const line = raw.replaceAll(OBJ_REPLACEMENT, "").trim();
73
+ if (!line) continue;
74
+ if (CHAT_PLACEHOLDER_RE.test(line)) continue;
75
+ if (CHAT_EMOJI_RE.test(line)) continue;
76
+ kept.push(line);
77
+ }
78
+ return kept.join(" ").trim();
79
+ }
80
+ /** 探测文本是否为聊天记录导出;是则返回说话人列表(供 UI 点选),否则 null。 */
81
+ function detectChatLog(text) {
82
+ const chat = parseChatLog(text);
83
+ return chat ? chat.speakers : null;
84
+ }
85
+
86
+ //#endregion
87
+ //#region src/client/form-styles.ts
88
+ /** 共享表单内联样式:原生 DOM 控件,视觉对齐官方原语(仓库既有模式)。 */
89
+ const inputStyle = {
90
+ width: "100%",
91
+ boxSizing: "border-box",
92
+ background: "none",
93
+ border: "1px solid var(--color-border, #333)",
94
+ borderRadius: 6,
95
+ padding: "6px 8px",
96
+ fontSize: 13,
97
+ color: "var(--color-text, #ddd)"
98
+ };
99
+ const labelStyle = {
100
+ display: "block",
101
+ fontSize: 12,
102
+ opacity: .7,
103
+ margin: "10px 0 4px"
104
+ };
105
+
106
+ //#endregion
107
+ //#region src/client/distill.tsx
108
+ /**
109
+ * 蒸馏弹窗:粘贴/导入素材 → RPC 投递任务 → 轮询 → 预览可编辑 → 保存。
110
+ *
111
+ * Modal 用官方原语(portal + Escape/遮罩关闭),表单控件用原生 DOM + 内联样式
112
+ * (仓库既有模式,primitives 没有 Textarea)。轮询 2s 一次,任务制兜住 10~90s
113
+ * 的不可控蒸馏耗时;宿主重启导致任务丢失时提示重蒸。
114
+ */
115
+ const STAGE_ORDER = [
116
+ "mining",
117
+ "contract",
118
+ "corpus"
119
+ ];
120
+ const TEXT_CAP = 2e4;
121
+ /** 聊天记录素材的宽容上限:原始文本含双人对话+时间戳,噪音过半。 */
122
+ const CHAT_TEXT_CAP = 2e5;
123
+ const miniBtn = {
124
+ background: "none",
125
+ border: "none",
126
+ cursor: "pointer",
127
+ fontSize: 11,
128
+ color: "var(--color-text-secondary, #999)",
129
+ padding: "2px 4px"
130
+ };
131
+ function DistillModal({ open, onClose, onSaved, t, callRpc }) {
132
+ const [phase, setPhase] = (0, react.useState)("input");
133
+ const [text, setText] = (0, react.useState)("");
134
+ const [hint, setHint] = (0, react.useState)("");
135
+ /** 检测到的聊天记录说话人(按消息数降序);null = 非聊天记录素材。 */
136
+ const [chatSpeakers, setChatSpeakers] = (0, react.useState)(null);
137
+ /** 用户补充的对方信息(性别/年龄段),可选;蒸馏时作为事实锚点传给宿主。 */
138
+ const [jobId, setJobId] = (0, react.useState)(null);
139
+ const [error, setError] = (0, react.useState)(null);
140
+ const [card, setCard] = (0, react.useState)({
141
+ key: "",
142
+ displayName: "",
143
+ description: "",
144
+ promptText: "",
145
+ corpus: [],
146
+ memory: void 0
147
+ });
148
+ const [savedName, setSavedName] = (0, react.useState)("");
149
+ const [stage, setStage] = (0, react.useState)(null);
150
+ const [showComplete, setShowComplete] = (0, react.useState)(false);
151
+ /** 运行中点击 ✕ 时的确认条;确认后取消任务并关闭。 */
152
+ const [confirmClose, setConfirmClose] = (0, react.useState)(false);
153
+ /** 预览阶段正在内联编辑的记忆条目下标;-1 = 无编辑。DSH webview 不支持
154
+ * window.prompt(调用直接抛异常,会把整个插件 UI 崩掉),编辑走内联 textarea。 */
155
+ const [editingIdx, setEditingIdx] = (0, react.useState)(-1);
156
+ const [editingText, setEditingText] = (0, react.useState)("");
157
+ const fileRef = (0, react.useRef)(null);
158
+ const cap = chatSpeakers ? CHAT_TEXT_CAP : TEXT_CAP;
159
+ (0, react.useEffect)(() => {
160
+ if (!open) {
161
+ setPhase("input");
162
+ setJobId(null);
163
+ setError(null);
164
+ setStage(null);
165
+ setShowComplete(false);
166
+ setConfirmClose(false);
167
+ setChatSpeakers(null);
168
+ setEditingIdx(-1);
169
+ setCard({
170
+ key: "",
171
+ displayName: "",
172
+ description: "",
173
+ promptText: "",
174
+ corpus: [],
175
+ memory: void 0
176
+ });
177
+ }
178
+ }, [open]);
179
+ /** 运行中关闭:先确认,确认后取消宿主任务再关。 */
180
+ const cancelRunning = async () => {
181
+ if (jobId) try {
182
+ await callRpc("distillCancel", { jobId });
183
+ } catch {}
184
+ onClose();
185
+ };
186
+ (0, react.useEffect)(() => {
187
+ if (phase !== "running" || !jobId) return;
188
+ let cancelled = false;
189
+ const timer = setInterval(async () => {
190
+ try {
191
+ const res = await callRpc("distillStatus", { jobId });
192
+ if (cancelled) return;
193
+ if (!res?.ok) return;
194
+ const job = res.value;
195
+ if (job === null || job === void 0) {
196
+ setError(t("distill.lost"));
197
+ setPhase("input");
198
+ return;
199
+ }
200
+ if (job.status === "running") {
201
+ if (job.stage) setStage(job.stage);
202
+ return;
203
+ }
204
+ if (job.status === "done" && job.card) {
205
+ setCard({
206
+ ...job.card,
207
+ memory: job.card.memory ?? void 0
208
+ });
209
+ setStage("corpus");
210
+ setPhase("preview");
211
+ setShowComplete(true);
212
+ } else if (job.status === "error") {
213
+ setError(t("distill.failed", { message: job.error ?? "unknown" }));
214
+ setPhase("input");
215
+ }
216
+ } catch {}
217
+ }, 2e3);
218
+ return () => {
219
+ cancelled = true;
220
+ clearInterval(timer);
221
+ };
222
+ }, [
223
+ phase,
224
+ jobId,
225
+ callRpc,
226
+ t
227
+ ]);
228
+ (0, react.useEffect)(() => {
229
+ if (!showComplete) return;
230
+ const timer = setTimeout(() => setShowComplete(false), 3e3);
231
+ return () => clearTimeout(timer);
232
+ }, [showComplete]);
233
+ const start = async () => {
234
+ setError(null);
235
+ setStage(null);
236
+ setShowComplete(false);
237
+ if (!text.trim()) return;
238
+ if (text.length > cap) {
239
+ setError(t("distill.too.long", { cap }));
240
+ return;
241
+ }
242
+ try {
243
+ const res = await callRpc("distillStart", {
244
+ text,
245
+ hint: hint.trim() || void 0
246
+ });
247
+ if (res?.ok && typeof res.value?.jobId === "string") {
248
+ setJobId(res.value.jobId);
249
+ setPhase("running");
250
+ } else setError(t("distill.failed", { message: "rejected" }));
251
+ } catch (err) {
252
+ setError(t("distill.failed", { message: String(err) }));
253
+ }
254
+ };
255
+ /** 文本变更统一入口:粘贴与文件导入共用,聊天记录检测在此触发。 */
256
+ const applyText = (value) => {
257
+ setText(value);
258
+ const speakers = detectChatLog(value);
259
+ setChatSpeakers(speakers);
260
+ if (!speakers) setHint("");
261
+ };
262
+ const importFile = async (file) => {
263
+ if (!file) return;
264
+ const content = await file.text();
265
+ if (content.length > CHAT_TEXT_CAP) {
266
+ setError(t("distill.too.long", { cap }));
267
+ return;
268
+ }
269
+ setError(null);
270
+ applyText(content);
271
+ };
272
+ const save = async () => {
273
+ setError(null);
274
+ try {
275
+ if ((await callRpc("saveCustomPersona", {
276
+ name: card.key,
277
+ displayName: card.displayName,
278
+ description: card.description,
279
+ promptText: card.promptText,
280
+ corpus: card.corpus,
281
+ distillVersion: card.distillVersion,
282
+ distillSource: text,
283
+ distillHint: hint || void 0,
284
+ ...card.memory?.length ? { memory: card.memory } : {}
285
+ }))?.ok) {
286
+ setSavedName(card.displayName);
287
+ setPhase("saved");
288
+ onSaved();
289
+ } else setError(t("distill.failed", { message: "rejected" }));
290
+ } catch (err) {
291
+ setError(t("distill.failed", { message: String(err) }));
292
+ }
293
+ };
294
+ const title = phase === "preview" ? t("distill.preview.title") : t("distill.title");
295
+ const running = phase === "running";
296
+ const locked = running || phase === "preview";
297
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
298
+ open,
299
+ onClose: locked ? () => {} : onClose,
300
+ headless: locked,
301
+ title,
302
+ description: phase === "input" ? t("distill.description") : void 0,
303
+ footer: phase === "input" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
304
+ variant: "ghost",
305
+ onClick: onClose,
306
+ children: t("distill.cancel")
307
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
308
+ variant: "primary",
309
+ disabled: !text.trim() || text.length > cap,
310
+ onClick: () => void start(),
311
+ children: t("distill.start")
312
+ })] }) : phase === "saved" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
313
+ variant: "primary",
314
+ onClick: onClose,
315
+ children: "OK"
316
+ }) : void 0,
317
+ children: [locked ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
319
+ style: {
320
+ display: "flex",
321
+ alignItems: "center",
322
+ gap: 10,
323
+ paddingBottom: 12,
324
+ borderBottom: "1px solid var(--color-border, #333)"
325
+ },
326
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
327
+ style: {
328
+ flex: 1,
329
+ fontSize: 15,
330
+ fontWeight: 600
331
+ },
332
+ children: title
333
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
334
+ type: "button",
335
+ "aria-label": t("distill.close.aria"),
336
+ onClick: () => setConfirmClose(true),
337
+ style: {
338
+ background: "none",
339
+ border: "none",
340
+ cursor: "pointer",
341
+ fontSize: 18,
342
+ color: "var(--color-text-secondary, #999)",
343
+ padding: "2px 6px"
344
+ },
345
+ children: "✕"
346
+ })]
347
+ }),
348
+ confirmClose ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
349
+ style: {
350
+ margin: "14px 0",
351
+ padding: "10px 12px",
352
+ borderRadius: 8,
353
+ border: "1px solid var(--color-warning, #e6a23c)",
354
+ background: "rgba(230,162,60,0.08)"
355
+ },
356
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
357
+ style: {
358
+ fontSize: 12.5,
359
+ marginBottom: 10,
360
+ color: "var(--color-text, #ddd)"
361
+ },
362
+ children: running ? t("distill.close.confirm") : t("distill.close.confirm.preview")
363
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
364
+ style: {
365
+ display: "flex",
366
+ gap: 8,
367
+ justifyContent: "center"
368
+ },
369
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
370
+ size: "sm",
371
+ variant: "ghost",
372
+ onClick: () => setConfirmClose(false),
373
+ children: t("distill.close.keep")
374
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
375
+ size: "sm",
376
+ variant: "primary",
377
+ onClick: () => running ? void cancelRunning() : onClose(),
378
+ children: running ? t("distill.close.stop") : t("distill.close.discard")
379
+ })]
380
+ })]
381
+ }) : null,
382
+ running ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
383
+ style: {
384
+ padding: "20px 0 24px",
385
+ textAlign: "center"
386
+ },
387
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
388
+ style: {
389
+ display: "flex",
390
+ justifyContent: "center",
391
+ alignItems: "center",
392
+ gap: 0,
393
+ marginBottom: 12
394
+ },
395
+ children: STAGE_ORDER.map((s, i) => {
396
+ const idx = stage ? STAGE_ORDER.indexOf(stage) : -1;
397
+ const done = STAGE_ORDER.indexOf(s) < idx;
398
+ const active = STAGE_ORDER.indexOf(s) === idx;
399
+ const dotColor = done ? "var(--color-success, #4caf50)" : active ? "var(--color-accent, #7c8cf8)" : "var(--color-border, #444)";
400
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
401
+ style: {
402
+ display: "flex",
403
+ alignItems: "center",
404
+ gap: 0
405
+ },
406
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
407
+ style: {
408
+ width: 12,
409
+ height: 12,
410
+ borderRadius: "50%",
411
+ background: done ? dotColor : active ? dotColor : "transparent",
412
+ border: `2px solid ${done ? dotColor : active ? dotColor : "var(--color-border, #444)"}`,
413
+ display: "flex",
414
+ alignItems: "center",
415
+ justifyContent: "center",
416
+ transition: "all 0.3s ease"
417
+ },
418
+ children: done ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
419
+ style: {
420
+ color: "#fff",
421
+ fontSize: 8,
422
+ lineHeight: 1
423
+ },
424
+ children: "✓"
425
+ }) : active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
426
+ style: {
427
+ color: "#fff",
428
+ fontSize: 8,
429
+ lineHeight: 1
430
+ },
431
+ children: "●"
432
+ }) : null
433
+ }), i < STAGE_ORDER.length - 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
434
+ width: 40,
435
+ height: 2,
436
+ background: done ? "var(--color-success, #4caf50)" : "var(--color-border, #444)",
437
+ transition: "background 0.3s ease"
438
+ } })]
439
+ }, s);
440
+ })
441
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
442
+ style: {
443
+ fontSize: 13,
444
+ opacity: .8
445
+ },
446
+ children: stage ? t(`distill.stage.${stage}`) : t("distill.running")
447
+ })]
448
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
449
+ showComplete ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
450
+ style: {
451
+ padding: "8px 12px",
452
+ marginBottom: 12,
453
+ borderRadius: 6,
454
+ background: "var(--color-success-bg, rgba(76, 175, 80, 0.12))",
455
+ border: "1px solid var(--color-success, #4caf50)",
456
+ fontSize: 13,
457
+ color: "var(--color-success, #4caf50)",
458
+ textAlign: "center",
459
+ fontWeight: 500
460
+ },
461
+ children: t("distill.complete")
462
+ }) : null,
463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
464
+ style: labelStyle,
465
+ children: t("distill.display.label")
466
+ }),
467
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
468
+ value: card.displayName,
469
+ onChange: (e) => setCard((c) => ({
470
+ ...c,
471
+ displayName: e.target.value
472
+ }))
473
+ }),
474
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
475
+ style: labelStyle,
476
+ children: t("distill.key.label")
477
+ }),
478
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
479
+ value: card.key,
480
+ onChange: (e) => setCard((c) => ({
481
+ ...c,
482
+ key: e.target.value
483
+ }))
484
+ }),
485
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
486
+ style: labelStyle,
487
+ children: t("distill.desc.label")
488
+ }),
489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
490
+ value: card.description,
491
+ onChange: (e) => setCard((c) => ({
492
+ ...c,
493
+ description: e.target.value
494
+ }))
495
+ }),
496
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
497
+ style: labelStyle,
498
+ children: t("distill.prompt.label")
499
+ }),
500
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
501
+ value: card.promptText,
502
+ onChange: (e) => setCard((c) => ({
503
+ ...c,
504
+ promptText: e.target.value
505
+ })),
506
+ rows: 8,
507
+ style: {
508
+ ...inputStyle,
509
+ resize: "vertical"
510
+ }
511
+ }),
512
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
513
+ style: labelStyle,
514
+ children: t("distill.corpus.label", { count: card.corpus.length })
515
+ }),
516
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
517
+ style: {
518
+ maxHeight: 120,
519
+ overflow: "auto",
520
+ fontSize: 12,
521
+ opacity: .8,
522
+ display: "flex",
523
+ flexDirection: "column",
524
+ gap: 4
525
+ },
526
+ children: card.corpus.map((sample, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: `用户: ${sample.user || "…"}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: `回复: ${sample.assistant}` })] }, i))
527
+ }),
528
+ card.memory && card.memory.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
529
+ style: labelStyle,
530
+ children: t("distill.memory.label", { count: card.memory.length })
531
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
532
+ style: {
533
+ maxHeight: 180,
534
+ overflow: "auto",
535
+ fontSize: 12,
536
+ opacity: .9,
537
+ display: "flex",
538
+ flexDirection: "column",
539
+ gap: 6
540
+ },
541
+ children: card.memory.map((m, i) => editingIdx === i ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
542
+ style: {
543
+ padding: "6px 8px",
544
+ borderRadius: 6,
545
+ background: "rgba(124,140,248,0.1)",
546
+ border: "1px solid rgba(124,140,248,0.45)"
547
+ },
548
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
549
+ value: editingText,
550
+ onChange: (e) => setEditingText(e.target.value),
551
+ rows: 2,
552
+ style: {
553
+ ...inputStyle,
554
+ fontSize: 12,
555
+ resize: "vertical"
556
+ }
557
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
558
+ style: {
559
+ display: "flex",
560
+ justifyContent: "flex-end",
561
+ gap: 6,
562
+ marginTop: 4
563
+ },
564
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
565
+ type: "button",
566
+ onClick: () => setEditingIdx(-1),
567
+ style: miniBtn,
568
+ children: t("manage.cancel")
569
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
570
+ type: "button",
571
+ disabled: !editingText.trim(),
572
+ onClick: () => {
573
+ setCard((c) => ({
574
+ ...c,
575
+ memory: c.memory.map((mm, j) => j === i ? {
576
+ ...mm,
577
+ text: editingText.trim()
578
+ } : mm)
579
+ }));
580
+ setEditingIdx(-1);
581
+ },
582
+ style: {
583
+ ...miniBtn,
584
+ color: "var(--color-accent, #7c8cf8)"
585
+ },
586
+ children: t("memory.save")
587
+ })]
588
+ })]
589
+ }, i) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
590
+ style: {
591
+ display: "flex",
592
+ alignItems: "center",
593
+ gap: 6,
594
+ padding: "6px 8px",
595
+ borderRadius: 6,
596
+ background: "rgba(124,140,248,0.1)",
597
+ border: "1px solid rgba(124,140,248,0.25)"
598
+ },
599
+ children: [
600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
601
+ style: { flex: 1 },
602
+ children: ["🎞️ ", m.text]
603
+ }),
604
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
605
+ type: "button",
606
+ onClick: () => {
607
+ setEditingIdx(i);
608
+ setEditingText(m.text);
609
+ },
610
+ style: miniBtn,
611
+ children: t("manage.edit")
612
+ }),
613
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
614
+ type: "button",
615
+ onClick: () => setCard((c) => ({
616
+ ...c,
617
+ memory: c.memory.filter((_, j) => j !== i)
618
+ })),
619
+ style: {
620
+ ...miniBtn,
621
+ color: "#ff6b6b"
622
+ },
623
+ children: t("manage.delete")
624
+ })
625
+ ]
626
+ }, i))
627
+ })] }) : null,
628
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
629
+ style: {
630
+ display: "flex",
631
+ gap: 8,
632
+ marginTop: 16,
633
+ paddingTop: 12,
634
+ borderTop: "1px solid var(--color-border, #333)"
635
+ },
636
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
637
+ variant: "ghost",
638
+ onClick: () => setPhase("input"),
639
+ children: t("distill.redistill")
640
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
641
+ variant: "primary",
642
+ onClick: () => void save(),
643
+ children: t("distill.save")
644
+ })]
645
+ })
646
+ ] })
647
+ ] }) : phase === "input" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
648
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
649
+ style: labelStyle,
650
+ children: t("distill.text.label")
651
+ }),
652
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
653
+ value: text,
654
+ onChange: (e) => applyText(e.target.value),
655
+ placeholder: t("distill.text.placeholder"),
656
+ rows: 10,
657
+ style: {
658
+ ...inputStyle,
659
+ resize: "vertical"
660
+ }
661
+ }),
662
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
663
+ style: {
664
+ display: "flex",
665
+ justifyContent: "space-between",
666
+ alignItems: "center",
667
+ marginTop: 4
668
+ },
669
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
670
+ style: {
671
+ fontSize: 11,
672
+ opacity: .6
673
+ },
674
+ children: t("distill.counter", {
675
+ count: text.length,
676
+ cap
677
+ })
678
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
679
+ ref: fileRef,
680
+ type: "file",
681
+ accept: ".txt,.md,text/plain",
682
+ style: { display: "none" },
683
+ "aria-label": t("distill.file.aria"),
684
+ onChange: (e) => void importFile(e.target.files?.[0])
685
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
686
+ size: "sm",
687
+ variant: "outline",
688
+ onClick: () => fileRef.current?.click(),
689
+ children: t("distill.file")
690
+ })] })]
691
+ }),
692
+ chatSpeakers && chatSpeakers.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
693
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
694
+ style: labelStyle,
695
+ children: t("distill.chat.who")
696
+ }),
697
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
698
+ style: {
699
+ display: "flex",
700
+ flexWrap: "wrap",
701
+ gap: 6
702
+ },
703
+ children: chatSpeakers.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
704
+ onClick: () => setHint(hint === name ? "" : name),
705
+ style: {
706
+ fontSize: 12,
707
+ padding: "4px 12px",
708
+ borderRadius: 999,
709
+ cursor: "pointer",
710
+ border: `1px solid ${hint === name ? "var(--color-accent, #7c8cf8)" : "var(--color-border, #444)"}`,
711
+ background: hint === name ? "var(--color-accent-bg, rgba(124,140,248,0.15))" : "transparent",
712
+ color: hint === name ? "var(--color-accent, #7c8cf8)" : "var(--color-text, #ddd)"
713
+ },
714
+ children: name
715
+ }, name))
716
+ }),
717
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
718
+ style: {
719
+ fontSize: 11,
720
+ opacity: .55,
721
+ marginTop: 6
722
+ },
723
+ children: t("distill.chat.hint")
724
+ })
725
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
726
+ style: labelStyle,
727
+ children: t("distill.hint.label")
728
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
729
+ value: hint,
730
+ onChange: (e) => setHint(e.target.value),
731
+ placeholder: t("distill.hint.placeholder")
732
+ })] })
733
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
734
+ style: {
735
+ padding: "24px 0",
736
+ textAlign: "center",
737
+ fontSize: 13
738
+ },
739
+ children: t("distill.saved", { persona: savedName })
740
+ }), error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
741
+ style: {
742
+ marginTop: 10,
743
+ fontSize: 12,
744
+ color: "var(--color-danger, #e56)"
745
+ },
746
+ children: error
747
+ }) : null]
748
+ });
749
+ }
750
+
751
+ //#endregion
752
+ //#region src/client/memory.tsx
753
+ /**
754
+ * 记忆星图:某个角色的长期记忆,以力导向星空图可视化。
755
+ *
756
+ * 渲染为自定义宽幅遮罩层(不依赖 Modal 原语——Modal 的 dialog 宽度上限 380px,
757
+ * 对记忆星图这种需要横屏空间的内容过于局促,改为自绘遮罩 + 居中卡片)。
758
+ *
759
+ * - Canvas 渲染:星空背景 + 力导向布局 + 缓慢绕圈漂移 + 发光卡片 + 语义连线
760
+ * - 核心记忆(身份/称呼类,宿主 isCoreMemory 判定)紫色 + ★,普通记忆青色
761
+ * - 点击卡片 → 右侧详情面板(只读正文 + 关联记忆 + 编辑/删除)
762
+ * - 编辑:textarea → updateMemory;删除:deleteMemory
763
+ * - 顶部日期筛选:全部 / 最近 7 天 / 30 天 / 90 天
764
+ */
765
+ const CARD_W = 260;
766
+ const CARD_H = 72;
767
+ const CARD_R = 12;
768
+ const CORE_COLOR = "#a78bfa";
769
+ const NORMAL_COLOR = "#67e8f9";
770
+ const OVERLAY_W = 960;
771
+ const FILTER_MS = {
772
+ all: 0,
773
+ "7d": 6048e5,
774
+ "30d": 2592e6,
775
+ "90d": 7776e6
776
+ };
777
+ function tokenize(text) {
778
+ const tokens = [];
779
+ const lowered = text.toLowerCase();
780
+ for (const m of lowered.matchAll(/[a-z0-9]+/g)) tokens.push(m[0]);
781
+ for (const run of lowered.match(/[\u4e00-\u9fff\u3400-\u4dbf]+/g) ?? []) {
782
+ if (run.length === 1) {
783
+ tokens.push(run);
784
+ continue;
785
+ }
786
+ for (let i = 0; i < run.length - 1; i++) tokens.push(run.slice(i, i + 2));
787
+ }
788
+ return tokens;
789
+ }
790
+ function jaccard(a, b) {
791
+ const sa = new Set(tokenize(a)), sb = new Set(tokenize(b));
792
+ if (sa.size === 0 || sb.size === 0) return 0;
793
+ let hit = 0;
794
+ sa.forEach((t) => {
795
+ if (sb.has(t)) hit++;
796
+ });
797
+ return hit / (sa.size + sb.size - hit);
798
+ }
799
+ function relTime(ts) {
800
+ const sec = Math.floor((Date.now() - ts) / 1e3);
801
+ if (sec < 60) return "刚刚";
802
+ const min = Math.floor(sec / 60);
803
+ if (min < 60) return `${min} 分钟前`;
804
+ const hr = Math.floor(min / 60);
805
+ if (hr < 24) return `${hr} 小时前`;
806
+ return `${Math.floor(hr / 24)} 天前`;
807
+ }
808
+ function hexGlow(hex, alpha) {
809
+ return `rgba(${parseInt(hex.slice(1, 3), 16)},${parseInt(hex.slice(3, 5), 16)},${parseInt(hex.slice(5, 7), 16)},${alpha})`;
810
+ }
811
+ function rgba(hex, alpha) {
812
+ return `rgba(${parseInt(hex.slice(1, 3), 16)},${parseInt(hex.slice(3, 5), 16)},${parseInt(hex.slice(5, 7), 16)},${alpha})`;
813
+ }
814
+ function brighten(hex) {
815
+ return `rgb(${Math.min(255, parseInt(hex.slice(1, 3), 16) + 50)},${Math.min(255, parseInt(hex.slice(3, 5), 16) + 50)},${Math.min(255, parseInt(hex.slice(5, 7), 16) + 50)})`;
816
+ }
817
+ function wrapText(ctx, text, maxW) {
818
+ const chars = text.split("");
819
+ const lines = [];
820
+ let cur = "";
821
+ for (const ch of chars) {
822
+ const test = cur + ch;
823
+ if (ctx.measureText(test).width > maxW && cur.length > 0) {
824
+ lines.push(cur);
825
+ cur = ch;
826
+ } else cur = test;
827
+ }
828
+ if (cur) lines.push(cur);
829
+ return lines;
830
+ }
831
+ function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc }) {
832
+ const canvasRef = (0, react.useRef)(null);
833
+ const graphRef = (0, react.useRef)({
834
+ nodes: [],
835
+ edges: []
836
+ });
837
+ const [memories, setMemories] = (0, react.useState)([]);
838
+ const [selected, setSelected] = (0, react.useState)(null);
839
+ const [editing, setEditing] = (0, react.useState)(false);
840
+ const [editText, setEditText] = (0, react.useState)("");
841
+ const [loading, setLoading] = (0, react.useState)(true);
842
+ const [error, setError] = (0, react.useState)(null);
843
+ const [filter, setFilter] = (0, react.useState)("all");
844
+ const load = async () => {
845
+ setLoading(true);
846
+ setError(null);
847
+ try {
848
+ const res = await callRpc("getMemory", { personaName });
849
+ if (res?.ok && Array.isArray(res.value)) setMemories(res.value);
850
+ else setMemories([]);
851
+ } catch {
852
+ setMemories([]);
853
+ } finally {
854
+ setLoading(false);
855
+ }
856
+ };
857
+ (0, react.useEffect)(() => {
858
+ if (open) load();
859
+ }, [open, personaName]);
860
+ const filtered = memories.filter((m) => !FILTER_MS[filter] || Date.now() - m.at <= FILTER_MS[filter]);
861
+ (0, react.useEffect)(() => {
862
+ const canvas = canvasRef.current;
863
+ if (!canvas || !open) return;
864
+ const dpr = window.devicePixelRatio || 1;
865
+ const W = OVERLAY_W, H = 544;
866
+ canvas.width = W * dpr;
867
+ canvas.height = H * dpr;
868
+ canvas.style.width = "960px";
869
+ canvas.style.height = "544px";
870
+ const ctx = canvas.getContext("2d");
871
+ if (!ctx) return;
872
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
873
+ const nodes = filtered.map((m, id) => {
874
+ const x = W / 2 + (Math.random() - .5) * W * .22;
875
+ const y = H / 2 + (Math.random() - .5) * H * .22;
876
+ return {
877
+ id,
878
+ text: m.text,
879
+ at: m.at,
880
+ core: m.core,
881
+ x,
882
+ y,
883
+ ax: x,
884
+ ay: y,
885
+ orbitR: 3 + Math.random() * 5,
886
+ orbitPhase: Math.random() * Math.PI * 2,
887
+ orbitSpeed: .006 + Math.random() * .008,
888
+ vx: 0,
889
+ vy: 0,
890
+ pinned: false
891
+ };
892
+ });
893
+ const edges = [];
894
+ for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) {
895
+ const w = jaccard(nodes[i].text, nodes[j].text);
896
+ if (w >= .12) edges.push({
897
+ source: i,
898
+ target: j,
899
+ weight: w
900
+ });
901
+ else if (nodes[i].core) edges.push({
902
+ source: i,
903
+ target: j,
904
+ weight: .15
905
+ });
906
+ else if (nodes[j].core) edges.push({
907
+ source: i,
908
+ target: j,
909
+ weight: .15
910
+ });
911
+ }
912
+ const forceStep = () => {
913
+ const cx = W / 2, cy = H / 2;
914
+ for (let i = 0; i < nodes.length; i++) {
915
+ const a = nodes[i];
916
+ if (a.pinned) continue;
917
+ for (let j = i + 1; j < nodes.length; j++) {
918
+ const b = nodes[j];
919
+ if (b.pinned) continue;
920
+ let dx = a.x - b.x, dy = a.y - b.y, d2 = dx * dx + dy * dy;
921
+ if (d2 < 1) {
922
+ dx = Math.random() - .5;
923
+ dy = Math.random() - .5;
924
+ d2 = 1;
925
+ }
926
+ const d = Math.sqrt(d2), f = 12100 / d;
927
+ a.vx += dx / d * f;
928
+ a.vy += dy / d * f;
929
+ b.vx -= dx / d * f;
930
+ b.vy -= dy / d * f;
931
+ }
932
+ }
933
+ for (const e of edges) {
934
+ const a = nodes[e.source], b = nodes[e.target];
935
+ const dx = b.x - a.x, dy = b.y - a.y;
936
+ const d = Math.sqrt(dx * dx + dy * dy) || 1;
937
+ const f = (d - 160) * .025 * e.weight;
938
+ if (!a.pinned) {
939
+ a.vx += dx / d * f;
940
+ a.vy += dy / d * f;
941
+ }
942
+ if (!b.pinned) {
943
+ b.vx -= dx / d * f;
944
+ b.vy -= dy / d * f;
945
+ }
946
+ }
947
+ for (const n of nodes) {
948
+ if (n.pinned) continue;
949
+ n.vx += (cx - n.x) * .012;
950
+ n.vy += (cy - n.y) * .012;
951
+ n.x += n.vx;
952
+ n.y += n.vy;
953
+ n.vx *= .86;
954
+ n.vy *= .86;
955
+ if (n.x < 90) n.x = 90;
956
+ else if (n.x > 870) n.x = 870;
957
+ if (n.y < 90) n.y = 90;
958
+ else if (n.y > 454) n.y = 454;
959
+ }
960
+ };
961
+ for (let i = 0; i < 250; i++) forceStep();
962
+ nodes.forEach((n) => {
963
+ n.ax = n.x;
964
+ n.ay = n.y;
965
+ });
966
+ graphRef.current = {
967
+ nodes,
968
+ edges
969
+ };
970
+ const stars = Array.from({ length: 240 }, () => ({
971
+ x: Math.random() * W,
972
+ y: Math.random() * H,
973
+ r: Math.random() * 1.4 + .3,
974
+ a: Math.random() * .7 + .3,
975
+ phase: Math.random() * Math.PI * 2
976
+ }));
977
+ let hovered = null, dragging = null, dragOffX = 0, dragOffY = 0, dragMoved = false;
978
+ const hitTest = (mx, my) => {
979
+ for (let i = nodes.length - 1; i >= 0; i--) {
980
+ const n = nodes[i];
981
+ if (mx >= n.x - CARD_W / 2 && mx <= n.x + CARD_W / 2 && my >= n.y - CARD_H / 2 && my <= n.y + CARD_H / 2) return i;
982
+ }
983
+ return -1;
984
+ };
985
+ const onMove = (e) => {
986
+ const rect = canvas.getBoundingClientRect();
987
+ const mx = e.clientX - rect.left, my = e.clientY - rect.top;
988
+ if (dragging) {
989
+ dragging.x = mx - dragOffX;
990
+ dragging.y = my - dragOffY;
991
+ dragging.ax = dragging.x;
992
+ dragging.ay = dragging.y;
993
+ dragMoved = true;
994
+ return;
995
+ }
996
+ const idx = hitTest(mx, my);
997
+ hovered = idx >= 0 ? idx : null;
998
+ canvas.style.cursor = idx >= 0 ? "grab" : "default";
999
+ };
1000
+ const onDown = (e) => {
1001
+ const rect = canvas.getBoundingClientRect();
1002
+ const mx = e.clientX - rect.left, my = e.clientY - rect.top;
1003
+ dragMoved = false;
1004
+ const idx = hitTest(mx, my);
1005
+ if (idx >= 0) {
1006
+ dragging = nodes[idx];
1007
+ dragging.pinned = true;
1008
+ dragOffX = mx - dragging.x;
1009
+ dragOffY = my - dragging.y;
1010
+ canvas.style.cursor = "grabbing";
1011
+ e.preventDefault();
1012
+ }
1013
+ };
1014
+ const onUp = () => {
1015
+ if (dragging) {
1016
+ dragging.ax = dragging.x;
1017
+ dragging.ay = dragging.y;
1018
+ dragging.pinned = false;
1019
+ dragging = null;
1020
+ }
1021
+ };
1022
+ const onClick = (e) => {
1023
+ if (dragMoved) return;
1024
+ const rect = canvas.getBoundingClientRect();
1025
+ const idx = hitTest(e.clientX - rect.left, e.clientY - rect.top);
1026
+ setSelected(idx >= 0 ? idx : null);
1027
+ if (idx >= 0) setEditing(false);
1028
+ };
1029
+ canvas.addEventListener("mousemove", onMove);
1030
+ canvas.addEventListener("mousedown", onDown);
1031
+ canvas.addEventListener("mouseup", onUp);
1032
+ canvas.addEventListener("click", onClick);
1033
+ let frame = 0, raf = 0;
1034
+ const draw = () => {
1035
+ frame++;
1036
+ ctx.clearRect(0, 0, W, H);
1037
+ const bg = ctx.createRadialGradient(W / 2, H / 2, 0, W / 2, H / 2, Math.max(W, H) * .72);
1038
+ bg.addColorStop(0, "#0d0d24");
1039
+ bg.addColorStop(.5, "#06061a");
1040
+ bg.addColorStop(1, "#02020a");
1041
+ ctx.fillStyle = bg;
1042
+ ctx.fillRect(0, 0, W, H);
1043
+ for (const s of stars) {
1044
+ const flicker = .5 + .5 * Math.sin(frame * .018 + s.phase);
1045
+ ctx.fillStyle = `rgba(180,200,245,${s.a * (.55 + .45 * flicker)})`;
1046
+ ctx.beginPath();
1047
+ ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
1048
+ ctx.fill();
1049
+ }
1050
+ for (const n of nodes) {
1051
+ if (n.pinned) continue;
1052
+ n.orbitPhase += n.orbitSpeed;
1053
+ n.x = n.ax + Math.cos(n.orbitPhase) * n.orbitR;
1054
+ n.y = n.ay + Math.sin(n.orbitPhase) * n.orbitR;
1055
+ }
1056
+ const relEdges = hovered !== null ? edges.filter((e) => e.source === hovered || e.target === hovered) : [];
1057
+ const relSet = new Set(relEdges.map((e) => `${e.source}_${e.target}`));
1058
+ for (const e of edges) {
1059
+ if (relSet.has(`${e.source}_${e.target}`)) continue;
1060
+ const a = nodes[e.source], b = nodes[e.target];
1061
+ ctx.beginPath();
1062
+ ctx.moveTo(a.x, a.y);
1063
+ ctx.lineTo(b.x, b.y);
1064
+ ctx.strokeStyle = `rgba(90,170,255,${.1 + e.weight * .2})`;
1065
+ ctx.lineWidth = .8 + e.weight * 1.1;
1066
+ ctx.stroke();
1067
+ }
1068
+ for (const e of relEdges) {
1069
+ const a = nodes[e.source], b = nodes[e.target];
1070
+ ctx.save();
1071
+ ctx.shadowColor = "rgba(130,210,255,0.55)";
1072
+ ctx.shadowBlur = 8;
1073
+ ctx.beginPath();
1074
+ ctx.moveTo(a.x, a.y);
1075
+ ctx.lineTo(b.x, b.y);
1076
+ ctx.strokeStyle = `rgba(140,220,255,${.4 + e.weight * .4})`;
1077
+ ctx.lineWidth = 1.4 + e.weight * 2;
1078
+ ctx.stroke();
1079
+ ctx.restore();
1080
+ }
1081
+ for (const n of nodes) {
1082
+ const color = n.core ? CORE_COLOR : NORMAL_COLOR;
1083
+ const hover = hovered === n.id, sel = selected === n.id;
1084
+ const scale = hover ? 1.08 : 1;
1085
+ const w = CARD_W * scale, h = CARD_H * scale;
1086
+ const x = n.x - w / 2, y = n.y - h / 2;
1087
+ ctx.save();
1088
+ ctx.shadowColor = hexGlow(color, sel ? .7 : hover ? .5 : n.core ? .3 : .14);
1089
+ ctx.shadowBlur = sel ? 26 : hover ? 18 : 7;
1090
+ const bg2 = ctx.createLinearGradient(x, y, x, y + h);
1091
+ bg2.addColorStop(0, "rgba(20,28,56,0.95)");
1092
+ bg2.addColorStop(1, "rgba(10,14,32,0.96)");
1093
+ const r = CARD_R * scale;
1094
+ ctx.beginPath();
1095
+ ctx.moveTo(x + r, y);
1096
+ ctx.lineTo(x + w - r, y);
1097
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
1098
+ ctx.lineTo(x + w, y + h - r);
1099
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
1100
+ ctx.lineTo(x + r, y + h);
1101
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
1102
+ ctx.lineTo(x, y + r);
1103
+ ctx.quadraticCurveTo(x, y, x + r, y);
1104
+ ctx.closePath();
1105
+ ctx.fillStyle = bg2;
1106
+ ctx.fill();
1107
+ ctx.strokeStyle = sel || hover ? brighten(color) : rgba(color, .3);
1108
+ ctx.lineWidth = sel ? 1.4 : hover ? 1.1 : .8;
1109
+ ctx.stroke();
1110
+ ctx.restore();
1111
+ ctx.fillStyle = color;
1112
+ ctx.beginPath();
1113
+ ctx.arc(x + 12, y + 12, 3.5, 0, Math.PI * 2);
1114
+ ctx.fill();
1115
+ if (n.core) {
1116
+ ctx.fillStyle = "#c4b5fd";
1117
+ ctx.font = "10px 'PingFang SC',sans-serif";
1118
+ ctx.fillText("★", x + 20, y + 16);
1119
+ }
1120
+ ctx.fillStyle = sel ? "#e8f2ff" : hover ? "#d0e4fc" : "rgba(200,215,240,0.88)";
1121
+ ctx.font = "12px 'PingFang SC','Microsoft YaHei',sans-serif";
1122
+ const lines = wrapText(ctx, n.text, w - 28);
1123
+ for (let i = 0; i < Math.min(lines.length, 2); i++) ctx.fillText(lines[i], x + 14, y + 30 + i * 15);
1124
+ if (lines.length > 2) {
1125
+ ctx.fillStyle = "rgba(140,165,200,0.55)";
1126
+ ctx.fillText("…", x + 14, y + 30 + 30);
1127
+ }
1128
+ ctx.fillStyle = "rgba(130,160,195,0.5)";
1129
+ ctx.font = "9px 'SF Pro Display','PingFang SC',sans-serif";
1130
+ ctx.fillText(relTime(n.at), x + 14, y + h - 10);
1131
+ }
1132
+ raf = requestAnimationFrame(draw);
1133
+ };
1134
+ raf = requestAnimationFrame(draw);
1135
+ return () => {
1136
+ cancelAnimationFrame(raf);
1137
+ canvas.removeEventListener("mousemove", onMove);
1138
+ canvas.removeEventListener("mousedown", onDown);
1139
+ canvas.removeEventListener("mouseup", onUp);
1140
+ canvas.removeEventListener("click", onClick);
1141
+ };
1142
+ }, [
1143
+ filtered,
1144
+ open,
1145
+ selected,
1146
+ personaName
1147
+ ]);
1148
+ const selectedNode = selected !== null ? graphRef.current.nodes[selected] : void 0;
1149
+ const saveEdit = async () => {
1150
+ if (selected === null) return;
1151
+ const v = editText.trim();
1152
+ if (!v) return;
1153
+ try {
1154
+ if ((await callRpc("updateMemory", {
1155
+ personaName,
1156
+ index: selected,
1157
+ text: v
1158
+ }))?.ok) {
1159
+ setEditing(false);
1160
+ await load();
1161
+ }
1162
+ } catch {}
1163
+ };
1164
+ const doDelete = async () => {
1165
+ if (selected === null) return;
1166
+ try {
1167
+ if ((await callRpc("deleteMemory", {
1168
+ personaName,
1169
+ index: selected
1170
+ }))?.ok) {
1171
+ setSelected(null);
1172
+ await load();
1173
+ }
1174
+ } catch {}
1175
+ };
1176
+ const related = selectedNode ? graphRef.current.nodes.filter((n) => n.id !== selected).map((n) => ({
1177
+ node: n,
1178
+ sim: jaccard(selectedNode.text, n.text)
1179
+ })).filter((r) => r.sim >= .12).sort((a, b) => b.sim - a.sim) : [];
1180
+ if (!open) return null;
1181
+ const filters = [
1182
+ {
1183
+ key: "all",
1184
+ label: t("memory.filter.all")
1185
+ },
1186
+ {
1187
+ key: "7d",
1188
+ label: t("memory.filter.7d")
1189
+ },
1190
+ {
1191
+ key: "30d",
1192
+ label: t("memory.filter.30d")
1193
+ },
1194
+ {
1195
+ key: "90d",
1196
+ label: t("memory.filter.90d")
1197
+ }
1198
+ ];
1199
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1200
+ style: {
1201
+ position: "fixed",
1202
+ inset: 0,
1203
+ zIndex: 2e3,
1204
+ display: "flex",
1205
+ alignItems: "center",
1206
+ justifyContent: "center",
1207
+ padding: 24,
1208
+ background: "rgba(0,0,0,0.55)",
1209
+ backdropFilter: "blur(3px)"
1210
+ },
1211
+ onClick: (e) => {
1212
+ if (e.target === e.currentTarget) onClose();
1213
+ },
1214
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1215
+ style: {
1216
+ width: OVERLAY_W,
1217
+ maxWidth: "96vw",
1218
+ background: "linear-gradient(160deg, rgba(20,28,58,0.97), rgba(10,14,32,0.99))",
1219
+ border: "1px solid rgba(110,180,255,0.28)",
1220
+ borderRadius: 20,
1221
+ boxShadow: "0 0 80px rgba(0,0,0,0.8)",
1222
+ overflow: "hidden"
1223
+ },
1224
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1225
+ style: {
1226
+ display: "flex",
1227
+ alignItems: "center",
1228
+ gap: 12,
1229
+ padding: "16px 20px 12px",
1230
+ borderBottom: "1px solid rgba(110,180,255,0.14)"
1231
+ },
1232
+ children: [
1233
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1234
+ width: 8,
1235
+ height: 8,
1236
+ borderRadius: "50%",
1237
+ background: "#5af",
1238
+ boxShadow: "0 0 10px rgba(90,170,255,0.7)"
1239
+ } }),
1240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1241
+ style: {
1242
+ fontSize: 15,
1243
+ fontWeight: 500,
1244
+ color: "#d4e2f8",
1245
+ letterSpacing: 1
1246
+ },
1247
+ children: [
1248
+ personaLabel,
1249
+ " · ",
1250
+ t("memory.title")
1251
+ ]
1252
+ }),
1253
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { flex: 1 } }),
1254
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1255
+ style: {
1256
+ display: "flex",
1257
+ gap: 4
1258
+ },
1259
+ children: filters.map((f) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1260
+ onClick: () => {
1261
+ setFilter(f.key);
1262
+ setSelected(null);
1263
+ },
1264
+ style: {
1265
+ fontSize: 11,
1266
+ letterSpacing: 1,
1267
+ border: `1px solid ${filter === f.key ? "rgba(90,170,255,0.6)" : "rgba(110,180,255,0.18)"}`,
1268
+ borderRadius: 10,
1269
+ padding: "3px 10px",
1270
+ background: filter === f.key ? "rgba(90,170,255,0.15)" : "transparent",
1271
+ color: filter === f.key ? "#d4e2f8" : "rgba(170,190,220,0.55)",
1272
+ cursor: "pointer",
1273
+ transition: "all .2s"
1274
+ },
1275
+ children: f.label
1276
+ }, f.key))
1277
+ }),
1278
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1279
+ size: "sm",
1280
+ variant: "ghost",
1281
+ onClick: onClose,
1282
+ children: t("manage.close")
1283
+ })
1284
+ ]
1285
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1286
+ style: {
1287
+ position: "relative",
1288
+ width: "100%",
1289
+ height: 544,
1290
+ borderRadius: "0 0 20px 20px",
1291
+ overflow: "hidden",
1292
+ background: "#04040c"
1293
+ },
1294
+ children: [
1295
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
1296
+ ref: canvasRef,
1297
+ style: { display: "block" }
1298
+ }),
1299
+ loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1300
+ style: {
1301
+ position: "absolute",
1302
+ inset: 0,
1303
+ display: "flex",
1304
+ alignItems: "center",
1305
+ justifyContent: "center",
1306
+ color: "#64748b",
1307
+ fontSize: 13
1308
+ },
1309
+ children: t("status.loading")
1310
+ }) : null,
1311
+ !loading && filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1312
+ style: {
1313
+ position: "absolute",
1314
+ inset: 0,
1315
+ display: "flex",
1316
+ alignItems: "center",
1317
+ justifyContent: "center",
1318
+ color: "#64748b",
1319
+ fontSize: 13
1320
+ },
1321
+ children: t("memory.empty")
1322
+ }) : null,
1323
+ selectedNode ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1324
+ style: {
1325
+ position: "absolute",
1326
+ top: 12,
1327
+ right: 12,
1328
+ width: 320,
1329
+ maxHeight: "calc(100% - 24px)",
1330
+ overflow: "auto",
1331
+ background: "linear-gradient(160deg, rgba(20,28,58,0.96), rgba(10,14,32,0.98))",
1332
+ border: "1px solid rgba(110,180,255,0.28)",
1333
+ borderRadius: 14,
1334
+ color: "#d4e2f8",
1335
+ padding: 14,
1336
+ boxShadow: "0 0 60px rgba(0,0,0,0.7)"
1337
+ },
1338
+ children: [
1339
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1340
+ style: {
1341
+ display: "flex",
1342
+ alignItems: "center",
1343
+ gap: 8,
1344
+ marginBottom: 10
1345
+ },
1346
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1347
+ width: 8,
1348
+ height: 8,
1349
+ borderRadius: "50%",
1350
+ background: selectedNode.core ? CORE_COLOR : NORMAL_COLOR,
1351
+ boxShadow: `0 0 10px ${selectedNode.core ? CORE_COLOR : NORMAL_COLOR}`
1352
+ } }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1353
+ style: {
1354
+ fontSize: 11,
1355
+ color: "rgba(170,190,220,0.6)"
1356
+ },
1357
+ children: [
1358
+ selectedNode.core ? t("memory.core") : t("memory.plain"),
1359
+ " · ",
1360
+ relTime(selectedNode.at)
1361
+ ]
1362
+ })]
1363
+ }),
1364
+ editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1365
+ value: editText,
1366
+ onChange: (e) => setEditText(e.target.value),
1367
+ style: {
1368
+ ...inputStyle,
1369
+ minHeight: 90,
1370
+ resize: "vertical"
1371
+ }
1372
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1373
+ style: {
1374
+ fontSize: 13,
1375
+ lineHeight: 1.7,
1376
+ wordBreak: "break-word",
1377
+ whiteSpace: "pre-wrap"
1378
+ },
1379
+ children: selectedNode.text
1380
+ }),
1381
+ related.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1382
+ style: {
1383
+ marginTop: 12,
1384
+ marginBottom: 6,
1385
+ fontSize: 10,
1386
+ color: "rgba(170,190,220,0.55)",
1387
+ letterSpacing: 2,
1388
+ textTransform: "uppercase"
1389
+ },
1390
+ children: t("memory.related")
1391
+ }), related.slice(0, 6).map((r) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1392
+ onClick: () => setSelected(r.node.id),
1393
+ style: {
1394
+ display: "flex",
1395
+ gap: 8,
1396
+ alignItems: "flex-start",
1397
+ padding: "6px 8px",
1398
+ marginBottom: 5,
1399
+ border: "1px solid rgba(110,180,255,0.12)",
1400
+ borderRadius: 8,
1401
+ cursor: "pointer",
1402
+ fontSize: 11,
1403
+ lineHeight: 1.5,
1404
+ color: "rgba(200,216,240,0.85)"
1405
+ },
1406
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1407
+ style: {
1408
+ flexShrink: 0,
1409
+ fontSize: 9,
1410
+ color: "#5af",
1411
+ border: "1px solid rgba(90,170,255,0.3)",
1412
+ borderRadius: 8,
1413
+ padding: "0 6px",
1414
+ lineHeight: "15px"
1415
+ },
1416
+ children: [Math.round(r.sim * 100), "%"]
1417
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: r.node.text })]
1418
+ }, r.node.id))] }) : null,
1419
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1420
+ style: {
1421
+ display: "flex",
1422
+ gap: 8,
1423
+ marginTop: 12,
1424
+ paddingTop: 10,
1425
+ borderTop: "1px solid rgba(110,180,255,0.14)"
1426
+ },
1427
+ children: editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1428
+ size: "sm",
1429
+ variant: "ghost",
1430
+ onClick: () => setEditing(false),
1431
+ children: t("manage.cancel")
1432
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1433
+ size: "sm",
1434
+ variant: "primary",
1435
+ onClick: () => void saveEdit(),
1436
+ children: t("memory.save")
1437
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1438
+ size: "sm",
1439
+ variant: "outline",
1440
+ onClick: () => {
1441
+ setEditText(selectedNode.text);
1442
+ setEditing(true);
1443
+ },
1444
+ children: t("manage.edit")
1445
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1446
+ size: "sm",
1447
+ variant: "outline",
1448
+ onClick: () => void doDelete(),
1449
+ style: {
1450
+ color: "#ff6b6b",
1451
+ borderColor: "rgba(255,107,107,0.35)"
1452
+ },
1453
+ children: t("manage.delete")
1454
+ })] })
1455
+ })
1456
+ ]
1457
+ }) : null
1458
+ ]
1459
+ })]
1460
+ })
1461
+ });
1462
+ }
1463
+
1464
+ //#endregion
1465
+ //#region src/client/manage.tsx
1466
+ /**
1467
+ * 管理自定义人设:列出全部条目(内置的编辑/删除置灰),支持编辑契约、删除、导出与导入。
1468
+ *
1469
+ * - 删除走行内二次确认(删除会连带记忆/风格/档案,不可恢复);
1470
+ * - 编辑复用蒸馏预览的字段布局,键名是存储主键、创建后不可改;
1471
+ * - 保存复用 saveCustomPersona 的 upsert 语义(带原 createdAt);
1472
+ * - 导出任何人设(含内置)为自包含 JSON 卡片文件,可选是否包含记忆;
1473
+ * - 导入 JSON 卡片文件,同名覆盖需二次确认。
1474
+ */
1475
+ /** 浏览器下载 JSON 文件(DSH webview 内可用)。 */
1476
+ function downloadJson(filename, obj) {
1477
+ const blob = new Blob([JSON.stringify(obj, null, 2) + "\n"], { type: "application/json" });
1478
+ const a = document.createElement("a");
1479
+ a.href = URL.createObjectURL(blob);
1480
+ a.download = filename;
1481
+ a.click();
1482
+ URL.revokeObjectURL(a.href);
1483
+ }
1484
+ function ManageModal({ open, onClose, onSaved, t, callRpc, items }) {
1485
+ const [phase, setPhase] = (0, react.useState)("list");
1486
+ const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
1487
+ const [notice, setNotice] = (0, react.useState)(null);
1488
+ const [error, setError] = (0, react.useState)(null);
1489
+ const [editing, setEditing] = (0, react.useState)(null);
1490
+ const [exportTarget, setExportTarget] = (0, react.useState)(null);
1491
+ const [includeMemory, setIncludeMemory] = (0, react.useState)(false);
1492
+ const [memoryOpen, setMemoryOpen] = (0, react.useState)(false);
1493
+ const [memoryTarget, setMemoryTarget] = (0, react.useState)({
1494
+ name: "",
1495
+ label: ""
1496
+ });
1497
+ const fileRef = (0, react.useRef)(null);
1498
+ (0, react.useEffect)(() => {
1499
+ if (!open) {
1500
+ setPhase("list");
1501
+ setNotice(null);
1502
+ setError(null);
1503
+ setEditing(null);
1504
+ setExportTarget(null);
1505
+ setDeleteTarget(null);
1506
+ setIncludeMemory(false);
1507
+ }
1508
+ }, [open]);
1509
+ const startEdit = async (name) => {
1510
+ setError(null);
1511
+ setNotice(null);
1512
+ try {
1513
+ const res = await callRpc("getCustomPersona", { personaName: name });
1514
+ if (res?.ok && res.value) {
1515
+ setEditing({
1516
+ name,
1517
+ card: res.value
1518
+ });
1519
+ setPhase("edit");
1520
+ } else setError(t("distill.failed", { message: "not found" }));
1521
+ } catch (err) {
1522
+ setError(t("distill.failed", { message: String(err) }));
1523
+ }
1524
+ };
1525
+ const saveEdit = async () => {
1526
+ if (!editing) return;
1527
+ setError(null);
1528
+ try {
1529
+ if ((await callRpc("saveCustomPersona", {
1530
+ name: editing.name,
1531
+ displayName: editing.card.displayName,
1532
+ description: editing.card.description,
1533
+ promptText: editing.card.promptText,
1534
+ corpus: editing.card.corpus,
1535
+ createdAt: editing.card.createdAt
1536
+ }))?.ok) {
1537
+ setEditing(null);
1538
+ setPhase("list");
1539
+ setNotice(t("manage.saved"));
1540
+ onSaved();
1541
+ } else setError(t("distill.failed", { message: "rejected" }));
1542
+ } catch (err) {
1543
+ setError(t("distill.failed", { message: String(err) }));
1544
+ }
1545
+ };
1546
+ const doDelete = async (name) => {
1547
+ setError(null);
1548
+ try {
1549
+ if ((await callRpc("deleteCustomPersona", { personaName: name }))?.ok) {
1550
+ setDeleteTarget(null);
1551
+ setNotice(t("manage.deleted", { persona: name }));
1552
+ onSaved();
1553
+ } else setError(t("distill.failed", { message: "rejected" }));
1554
+ } catch (err) {
1555
+ setError(t("distill.failed", { message: String(err) }));
1556
+ }
1557
+ };
1558
+ const doExport = async (name) => {
1559
+ setError(null);
1560
+ try {
1561
+ const res = await callRpc("exportPersona", {
1562
+ personaName: name,
1563
+ includeMemory
1564
+ });
1565
+ if (res?.ok && res.value) {
1566
+ const bundle = res.value;
1567
+ downloadJson(`${name}.lume.json`, bundle);
1568
+ setExportTarget(null);
1569
+ setNotice(t("manage.exported", { persona: name }));
1570
+ } else setError(t("distill.failed", { message: "rejected" }));
1571
+ } catch (err) {
1572
+ setError(t("distill.failed", { message: String(err) }));
1573
+ }
1574
+ };
1575
+ const doImport = async (file) => {
1576
+ if (!file) return;
1577
+ setError(null);
1578
+ setNotice(null);
1579
+ let text;
1580
+ try {
1581
+ text = await file.text();
1582
+ } catch {
1583
+ setError(t("manage.import.read.failed"));
1584
+ return;
1585
+ }
1586
+ try {
1587
+ JSON.parse(text);
1588
+ } catch {
1589
+ setError(t("manage.import.parse.failed"));
1590
+ return;
1591
+ }
1592
+ try {
1593
+ const res = await callRpc("importPersona", { payload: text });
1594
+ if (res?.ok) {
1595
+ const v = res.value;
1596
+ setNotice(t("manage.imported", { persona: v?.displayName ?? "?" }));
1597
+ setPhase("list");
1598
+ onSaved();
1599
+ } else setError(res.error?.message ?? t("distill.failed", { message: "rejected" }));
1600
+ } catch (err) {
1601
+ setError(t("distill.failed", { message: String(err) }));
1602
+ }
1603
+ };
1604
+ const rowStyle = {
1605
+ display: "flex",
1606
+ alignItems: "center",
1607
+ gap: 8,
1608
+ padding: "8px 0",
1609
+ borderBottom: "1px solid var(--color-border, #222)"
1610
+ };
1611
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1612
+ open,
1613
+ onClose,
1614
+ title: t("manage.title"),
1615
+ footer: phase === "edit" && editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1616
+ variant: "ghost",
1617
+ onClick: () => setPhase("list"),
1618
+ children: t("manage.cancel")
1619
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1620
+ variant: "primary",
1621
+ disabled: !editing.card.displayName.trim() || !editing.card.promptText.trim(),
1622
+ onClick: () => void saveEdit(),
1623
+ children: t("manage.save")
1624
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1625
+ variant: "primary",
1626
+ onClick: onClose,
1627
+ children: t("manage.close")
1628
+ }),
1629
+ children: [
1630
+ phase === "list" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1631
+ exportTarget ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1632
+ style: {
1633
+ display: "flex",
1634
+ alignItems: "center",
1635
+ gap: 12,
1636
+ padding: "10px 12px",
1637
+ marginBottom: 12,
1638
+ borderRadius: 8,
1639
+ background: "var(--color-bg-2, #1a1b1e)",
1640
+ border: "1px solid var(--color-border, #333)"
1641
+ },
1642
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1643
+ style: {
1644
+ flex: 1,
1645
+ minWidth: 0
1646
+ },
1647
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1648
+ style: {
1649
+ fontSize: 13,
1650
+ fontWeight: 500
1651
+ },
1652
+ children: exportTarget.label
1653
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1654
+ style: {
1655
+ display: "flex",
1656
+ alignItems: "center",
1657
+ gap: 6,
1658
+ marginTop: 4,
1659
+ fontSize: 12,
1660
+ opacity: .8,
1661
+ cursor: "pointer"
1662
+ },
1663
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1664
+ type: "checkbox",
1665
+ checked: includeMemory,
1666
+ onChange: (e) => setIncludeMemory(e.target.checked)
1667
+ }), t("manage.export.memory")]
1668
+ })]
1669
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1670
+ style: {
1671
+ display: "flex",
1672
+ gap: 8
1673
+ },
1674
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1675
+ size: "sm",
1676
+ variant: "ghost",
1677
+ onClick: () => setExportTarget(null),
1678
+ children: t("manage.cancel")
1679
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1680
+ size: "sm",
1681
+ variant: "primary",
1682
+ onClick: () => void doExport(exportTarget.name),
1683
+ children: t("manage.export.confirm")
1684
+ })]
1685
+ })]
1686
+ }) : null,
1687
+ deleteTarget ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1688
+ style: {
1689
+ display: "flex",
1690
+ alignItems: "center",
1691
+ gap: 12,
1692
+ padding: "10px 12px",
1693
+ marginBottom: 12,
1694
+ borderRadius: 8,
1695
+ background: "var(--color-danger-bg, rgba(229, 85, 102, 0.10))",
1696
+ border: "1px solid var(--color-danger, #e56)"
1697
+ },
1698
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1699
+ style: {
1700
+ flex: 1,
1701
+ minWidth: 0
1702
+ },
1703
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1704
+ style: {
1705
+ fontSize: 13,
1706
+ fontWeight: 500
1707
+ },
1708
+ children: deleteTarget.label
1709
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1710
+ style: {
1711
+ fontSize: 11,
1712
+ opacity: .8,
1713
+ marginTop: 4
1714
+ },
1715
+ children: t("manage.delete.warning")
1716
+ })]
1717
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1718
+ style: {
1719
+ display: "flex",
1720
+ gap: 8
1721
+ },
1722
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1723
+ size: "sm",
1724
+ variant: "ghost",
1725
+ onClick: () => setDeleteTarget(null),
1726
+ children: t("manage.cancel")
1727
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1728
+ size: "sm",
1729
+ variant: "primary",
1730
+ onClick: () => void doDelete(deleteTarget.name),
1731
+ children: t("manage.confirm.delete")
1732
+ })]
1733
+ })]
1734
+ }) : null,
1735
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1736
+ style: {
1737
+ display: "flex",
1738
+ gap: 8,
1739
+ marginBottom: 10
1740
+ },
1741
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1742
+ ref: fileRef,
1743
+ type: "file",
1744
+ accept: ".json,application/json",
1745
+ style: { display: "none" },
1746
+ onChange: (e) => void doImport(e.target.files?.[0])
1747
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1748
+ size: "sm",
1749
+ variant: "outline",
1750
+ onClick: () => fileRef.current?.click(),
1751
+ children: t("manage.import")
1752
+ })]
1753
+ }),
1754
+ items.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1755
+ style: {
1756
+ padding: "16px 0",
1757
+ fontSize: 13,
1758
+ opacity: .7
1759
+ },
1760
+ children: t("manage.empty")
1761
+ }) : null,
1762
+ items.map((item) => {
1763
+ const label = item.profileName ?? item.displayName;
1764
+ const isCustom = item.custom === true;
1765
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1766
+ style: rowStyle,
1767
+ children: [
1768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1769
+ style: {
1770
+ flex: 1,
1771
+ minWidth: 0
1772
+ },
1773
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1774
+ style: { fontSize: 13 },
1775
+ children: [label, isCustom ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1776
+ style: {
1777
+ fontSize: 11,
1778
+ opacity: .55,
1779
+ marginLeft: 6
1780
+ },
1781
+ children: t("manage.builtin")
1782
+ })]
1783
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1784
+ style: {
1785
+ fontSize: 11,
1786
+ opacity: .6,
1787
+ overflow: "hidden",
1788
+ textOverflow: "ellipsis",
1789
+ whiteSpace: "nowrap"
1790
+ },
1791
+ children: item.description || item.name
1792
+ })]
1793
+ }),
1794
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1795
+ size: "sm",
1796
+ variant: "outline",
1797
+ onClick: () => {
1798
+ setExportTarget({
1799
+ name: item.name,
1800
+ label: item.profileName ?? item.displayName
1801
+ });
1802
+ setIncludeMemory(false);
1803
+ },
1804
+ children: t("manage.export")
1805
+ }),
1806
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1807
+ size: "sm",
1808
+ variant: "outline",
1809
+ onClick: () => {
1810
+ setMemoryTarget({
1811
+ name: item.name,
1812
+ label: item.profileName ?? item.displayName
1813
+ });
1814
+ setMemoryOpen(true);
1815
+ },
1816
+ children: t("memory.title")
1817
+ }),
1818
+ isCustom ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1819
+ size: "sm",
1820
+ variant: "outline",
1821
+ onClick: () => void startEdit(item.name),
1822
+ children: t("manage.edit")
1823
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1824
+ size: "sm",
1825
+ variant: "outline",
1826
+ onClick: () => setDeleteTarget({
1827
+ name: item.name,
1828
+ label: item.profileName ?? item.displayName
1829
+ }),
1830
+ children: t("manage.delete")
1831
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1832
+ size: "sm",
1833
+ variant: "outline",
1834
+ disabled: true,
1835
+ children: t("manage.edit")
1836
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1837
+ size: "sm",
1838
+ variant: "outline",
1839
+ disabled: true,
1840
+ children: t("manage.delete")
1841
+ })] })
1842
+ ]
1843
+ }, item.name);
1844
+ })
1845
+ ] }) : editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
1846
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1847
+ style: labelStyle,
1848
+ children: t("manage.display.label")
1849
+ }),
1850
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1851
+ value: editing.card.displayName,
1852
+ onChange: (e) => setEditing((s) => s ? {
1853
+ ...s,
1854
+ card: {
1855
+ ...s.card,
1856
+ displayName: e.target.value
1857
+ }
1858
+ } : s)
1859
+ }),
1860
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1861
+ style: labelStyle,
1862
+ children: t("manage.key.label")
1863
+ }),
1864
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1865
+ value: editing.name,
1866
+ disabled: true
1867
+ }),
1868
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1869
+ style: {
1870
+ fontSize: 11,
1871
+ opacity: .55,
1872
+ marginTop: 4
1873
+ },
1874
+ children: t("manage.key.hint")
1875
+ }),
1876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1877
+ style: labelStyle,
1878
+ children: t("manage.desc.label")
1879
+ }),
1880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
1881
+ value: editing.card.description,
1882
+ onChange: (e) => setEditing((s) => s ? {
1883
+ ...s,
1884
+ card: {
1885
+ ...s.card,
1886
+ description: e.target.value
1887
+ }
1888
+ } : s)
1889
+ }),
1890
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1891
+ style: labelStyle,
1892
+ children: t("manage.prompt.label")
1893
+ }),
1894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1895
+ value: editing.card.promptText,
1896
+ onChange: (e) => setEditing((s) => s ? {
1897
+ ...s,
1898
+ card: {
1899
+ ...s.card,
1900
+ promptText: e.target.value
1901
+ }
1902
+ } : s),
1903
+ rows: 8,
1904
+ style: {
1905
+ ...inputStyle,
1906
+ resize: "vertical"
1907
+ }
1908
+ }),
1909
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1910
+ style: labelStyle,
1911
+ children: t("manage.corpus.label", { count: editing.card.corpus.length })
1912
+ }),
1913
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1914
+ style: {
1915
+ maxHeight: 110,
1916
+ overflow: "auto",
1917
+ fontSize: 12,
1918
+ opacity: .8,
1919
+ display: "flex",
1920
+ flexDirection: "column",
1921
+ gap: 4
1922
+ },
1923
+ children: editing.card.corpus.map((sample, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: `用户: ${sample.user || "…"}` }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: `回复: ${sample.assistant}` })] }, i))
1924
+ })
1925
+ ] }) : null,
1926
+ notice ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1927
+ style: {
1928
+ marginTop: 10,
1929
+ fontSize: 12,
1930
+ opacity: .8
1931
+ },
1932
+ children: notice
1933
+ }) : null,
1934
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1935
+ style: {
1936
+ marginTop: 10,
1937
+ fontSize: 12,
1938
+ color: "var(--color-danger, #e56)"
1939
+ },
1940
+ children: error
1941
+ }) : null,
1942
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MemoryStarMap, {
1943
+ open: memoryOpen,
1944
+ onClose: () => setMemoryOpen(false),
1945
+ personaName: memoryTarget.name,
1946
+ personaLabel: memoryTarget.label,
1947
+ t,
1948
+ callRpc
1949
+ })
1950
+ ]
1951
+ });
1952
+ }
1953
+
1954
+ //#endregion
1955
+ //#region src/client/menu.ts
1956
+ /**
1957
+ * 人设菜单的纯展示逻辑:排序与标签去重。
1958
+ * 与 React 解耦,便于直接单测。
1959
+ */
1960
+ /**
1961
+ * 菜单排序:「不使用人设」(none)固定在最上面——它是最常用的"退出人设"入口,
1962
+ * 不该被埋在列表末尾;其余按宿主返回顺序(内置 manifest 序 + 自定义)。
1963
+ */
1964
+ function orderPersonaItems(items) {
1965
+ const none = items.filter((item) => item.name === "none");
1966
+ const rest = items.filter((item) => item.name !== "none");
1967
+ return [...none, ...rest];
1968
+ }
1969
+ /**
1970
+ * 标签去重:自定义人设的生效名与任何其他条目撞名时(典型:蒸馏出与内置同名的卡),
1971
+ * 给自定义条目追加本地化后缀,内置名保持原样。
1972
+ * @returns name → 最终展示标签
1973
+ */
1974
+ function resolveLabels(items, labelOf, customSuffix) {
1975
+ const labels = new Map(items.map((item) => [item.name, labelOf(item)]));
1976
+ const seen = /* @__PURE__ */ new Map();
1977
+ for (const item of items) seen.set(labels.get(item.name), (seen.get(labels.get(item.name)) ?? 0) + 1);
1978
+ for (const item of items) {
1979
+ const label = labels.get(item.name);
1980
+ if (item.custom && (seen.get(label) ?? 0) > 1) labels.set(item.name, `${label}${customSuffix}`);
1981
+ }
1982
+ return labels;
1983
+ }
1984
+
1985
+ //#endregion
1986
+ //#region src/client/locales.ts
1987
+ /** lume 客户端词典(dsh-client-locale 命名空间注册)。 */
1988
+ const NS = "lume";
1989
+ const zh = {
1990
+ "trigger.fallback": "选择人设",
1991
+ "trigger.aria": "选择人设,当前 {persona}",
1992
+ "menu.aria": "人设选择",
1993
+ "status.loading": "正在加载人设…",
1994
+ "empty": "没有人设可用。",
1995
+ "menu.custom.suffix": "(自定义)",
1996
+ "distill.menu": "+ 蒸馏角色卡…",
1997
+ "distill.title": "蒸馏角色卡",
1998
+ "distill.description": "粘贴小说/剧本/设定文档,自动提炼成角色卡。素材只用于蒸馏,不会进入对话。",
1999
+ "distill.text.label": "素材文本",
2000
+ "distill.text.placeholder": "粘贴小说片段、剧本文本或人物设定…",
2001
+ "distill.counter": "{count} / {cap} 字",
2002
+ "distill.hint.label": "目标角色名(可选,素材中的称呼)",
2003
+ "distill.hint.placeholder": "如:晚晴",
2004
+ "distill.file": "导入 .txt/.md",
2005
+ "distill.file.aria": "导入文本文件",
2006
+ "distill.start": "开始蒸馏",
2007
+ "distill.cancel": "取消",
2008
+ "distill.running": "正在提炼…通常需要 10~60 秒,可离开此窗口稍后回来。",
2009
+ "distill.stage.mining": "正在分析对话角色…",
2010
+ "distill.stage.contract": "正在合成风格契约…",
2011
+ "distill.stage.corpus": "正在生成示例语料…",
2012
+ "distill.complete": "蒸馏完成!",
2013
+ "distill.preview.title": "确认角色卡",
2014
+ "distill.display.label": "显示名",
2015
+ "distill.key.label": "英文键名",
2016
+ "distill.desc.label": "简介",
2017
+ "distill.prompt.label": "风格契约",
2018
+ "distill.corpus.label": "示例对话({count} 条,保存后会在相处中继续进化)",
2019
+ "distill.memory.label": "真实记忆点({count} 条,来自你们的聊天记录——她会记得这些)",
2020
+ "distill.memory.edit": "编辑这条记忆",
2021
+ "distill.save": "保存角色卡",
2022
+ "distill.redistill": "重新蒸馏",
2023
+ "distill.saved": "已保存「{persona}」。重新打开人设菜单即可选择。",
2024
+ "distill.failed": "蒸馏失败:{message}",
2025
+ "distill.lost": "任务状态丢失(宿主可能重启过),请重新蒸馏。",
2026
+ "distill.too.long": "素材超过 {cap} 字上限,请截取片段。",
2027
+ "distill.chat.who": "检测到聊天记录,蒸馏谁?",
2028
+ "distill.chat.hint": "点选要蒸馏的人:TA 的台词将成为语气样本,另一人的对话直接剔除。",
2029
+ "distill.close.aria": "关闭蒸馏",
2030
+ "distill.close.confirm": "蒸馏仍在进行。关闭将中止本次蒸馏,已生成的内容不会保留。",
2031
+ "distill.close.keep": "继续蒸馏",
2032
+ "distill.close.stop": "停止并关闭",
2033
+ "distill.close.confirm.preview": "角色卡还未保存,关闭将丢弃本次蒸馏结果。",
2034
+ "distill.close.discard": "丢弃并关闭",
2035
+ "manage.menu": "管理自定义人设…",
2036
+ "manage.title": "管理自定义人设",
2037
+ "manage.empty": "还没有自定义人设。用「蒸馏角色卡」或在对话里创建一个吧。",
2038
+ "manage.builtin": "内置",
2039
+ "manage.edit": "编辑",
2040
+ "manage.delete": "删除",
2041
+ "manage.confirm.delete": "确认删除",
2042
+ "manage.delete.warning": "她的记忆、风格与档案会一起删除,不可恢复。",
2043
+ "manage.cancel": "取消",
2044
+ "manage.save": "保存修改",
2045
+ "manage.close": "关闭",
2046
+ "manage.display.label": "显示名",
2047
+ "manage.key.label": "英文键名",
2048
+ "manage.key.hint": "键名是她的唯一标识,创建后不可修改",
2049
+ "manage.desc.label": "简介",
2050
+ "manage.prompt.label": "风格契约",
2051
+ "manage.corpus.label": "示例对话({count} 条,只读;语气会随对话继续进化)",
2052
+ "manage.saved": "已保存修改。",
2053
+ "manage.deleted": "已删除「{persona}」。",
2054
+ "manage.export": "导出",
2055
+ "manage.export.confirm": "确认导出",
2056
+ "manage.export.memory": "包含记忆",
2057
+ "manage.exported": "已导出「{persona}」为 JSON 卡片文件。",
2058
+ "manage.import": "导入人设卡…",
2059
+ "manage.import.read.failed": "读取文件失败。",
2060
+ "manage.import.parse.failed": "文件不是合法的 JSON。",
2061
+ "manage.imported": "已导入「{persona}」。",
2062
+ "memory.title": "记忆",
2063
+ "memory.empty": "还没有记忆,相处中会慢慢沉淀。",
2064
+ "memory.core": "核心记忆",
2065
+ "memory.plain": "普通记忆",
2066
+ "memory.related": "关联记忆",
2067
+ "memory.save": "保存修改",
2068
+ "memory.filter.all": "全部",
2069
+ "memory.filter.7d": "最近 7 天",
2070
+ "memory.filter.30d": "最近 30 天",
2071
+ "memory.filter.90d": "最近 90 天"
2072
+ };
2073
+ const en = {
2074
+ "trigger.fallback": "Select persona",
2075
+ "trigger.aria": "Select persona, current {persona}",
2076
+ "menu.aria": "Persona selection",
2077
+ "status.loading": "Loading personas…",
2078
+ "empty": "No personas available.",
2079
+ "menu.custom.suffix": " (custom)",
2080
+ "distill.menu": "+ Distill a character card…",
2081
+ "distill.title": "Distill a character card",
2082
+ "distill.description": "Paste a novel/script/character sheet and distill it into a persona card. The material is only used for distillation, never sent into the conversation.",
2083
+ "distill.text.label": "Source text",
2084
+ "distill.text.placeholder": "Paste a novel excerpt, script or character sheet…",
2085
+ "distill.counter": "{count} / {cap} chars",
2086
+ "distill.hint.label": "Target character name (optional, as addressed in the text)",
2087
+ "distill.hint.placeholder": "e.g.晚晴",
2088
+ "distill.file": "Import .txt/.md",
2089
+ "distill.file.aria": "Import a text file",
2090
+ "distill.start": "Start distilling",
2091
+ "distill.cancel": "Cancel",
2092
+ "distill.running": "Distilling… usually 10–60 seconds. You may leave this window and come back.",
2093
+ "distill.stage.mining": "Analyzing character voice…",
2094
+ "distill.stage.contract": "Synthesizing style contract…",
2095
+ "distill.stage.corpus": "Generating sample dialogues…",
2096
+ "distill.complete": "Distillation complete!",
2097
+ "distill.preview.title": "Review the card",
2098
+ "distill.display.label": "Display name",
2099
+ "distill.key.label": "Key (english)",
2100
+ "distill.desc.label": "Description",
2101
+ "distill.prompt.label": "Style contract",
2102
+ "distill.corpus.label": "Sample dialogues ({count}; they keep evolving as you talk)",
2103
+ "distill.memory.label": "Real memory points ({count}; from your chat log — she will remember these)",
2104
+ "distill.memory.edit": "Edit this memory",
2105
+ "distill.save": "Save card",
2106
+ "distill.redistill": "Re-distill",
2107
+ "distill.saved": "Saved \"{persona}\". Reopen the persona menu to select it.",
2108
+ "distill.failed": "Distillation failed: {message}",
2109
+ "distill.lost": "Job state lost (the host may have restarted). Please distill again.",
2110
+ "distill.too.long": "Source exceeds the {cap}-character limit; please trim it.",
2111
+ "distill.chat.who": "Chat log detected. Who should be distilled?",
2112
+ "distill.chat.hint": "Pick the person: their lines become the tone samples, the other person's are dropped.",
2113
+ "distill.close.aria": "Close distillation",
2114
+ "distill.close.confirm": "Distillation is still running. Closing will abort it and discard the result.",
2115
+ "distill.close.keep": "Keep distilling",
2116
+ "distill.close.stop": "Stop and close",
2117
+ "distill.close.confirm.preview": "The card is not saved yet — closing discards the distillation.",
2118
+ "distill.close.discard": "Discard and close",
2119
+ "manage.menu": "Manage custom personas…",
2120
+ "manage.title": "Manage custom personas",
2121
+ "manage.empty": "No custom personas yet. Distill one or create it in conversation.",
2122
+ "manage.builtin": "built-in",
2123
+ "manage.edit": "Edit",
2124
+ "manage.delete": "Delete",
2125
+ "manage.confirm.delete": "Confirm delete",
2126
+ "manage.delete.warning": "Her memory, style and profile will be deleted too. This cannot be undone.",
2127
+ "manage.cancel": "Cancel",
2128
+ "manage.save": "Save changes",
2129
+ "manage.close": "Close",
2130
+ "manage.display.label": "Display name",
2131
+ "manage.key.label": "Key (english)",
2132
+ "manage.key.hint": "The key is her unique identifier and cannot be changed after creation",
2133
+ "manage.desc.label": "Description",
2134
+ "manage.prompt.label": "Style contract",
2135
+ "manage.corpus.label": "Sample dialogues ({count}; read-only; her tone keeps evolving in conversation)",
2136
+ "manage.saved": "Changes saved.",
2137
+ "manage.deleted": "Deleted \"{persona}\".",
2138
+ "manage.export": "Export",
2139
+ "manage.export.confirm": "Export",
2140
+ "manage.export.memory": "Include memory",
2141
+ "manage.exported": "Exported \"{persona}\" as a JSON card file.",
2142
+ "manage.import": "Import a card…",
2143
+ "manage.import.read.failed": "Failed to read the file.",
2144
+ "manage.import.parse.failed": "The file is not valid JSON.",
2145
+ "manage.imported": "Imported \"{persona}\".",
2146
+ "memory.title": "Memory",
2147
+ "memory.empty": "No memories yet; they accumulate as you talk.",
2148
+ "memory.core": "Core memory",
2149
+ "memory.plain": "Memory",
2150
+ "memory.related": "Related memories",
2151
+ "memory.save": "Save changes",
2152
+ "memory.filter.all": "All",
2153
+ "memory.filter.7d": "Last 7 days",
2154
+ "memory.filter.30d": "Last 30 days",
2155
+ "memory.filter.90d": "Last 90 days"
2156
+ };
2157
+
2158
+ //#endregion
2159
+ //#region src/client/index.tsx
2160
+ /**
2161
+ * lume-dsh-plugin 客户端半边:输入栏左侧人设选择。
2162
+ *
2163
+ * 下拉用官方原语 @deepseek-ai/dsh-client-ui-primitives 的 Menu:
2164
+ * - side="top":输入栏位于视口底端时菜单向上弹出(B 项修复的核心)
2165
+ * - portal:渲染进 document.body 并随滚动/缩放跟随重定位,不受祖先 overflow 裁剪
2166
+ * - selectedId / onSelect / onClose(外点 + Escape)全部内建,取代 v0.1.0 手写的
2167
+ * document mousedown 监听与绝对定位样式
2168
+ *
2169
+ * 打包契约:本文件由 tsdown 包成 __ModuleLoader__ 工厂 bundle(见 tsdown.config.ts),
2170
+ * react 系与 primitives 为外部 require,由模块图解析。
2171
+ */
2172
+ const DISTILL_ITEM_ID = "lume-distill";
2173
+ const MANAGE_ITEM_ID = "lume-manage";
2174
+ /**
2175
+ * 错误边界:任何子组件渲染异常只隐藏 Lume 的 UI 附件,不让 DSH 整个界面崩掉。
2176
+ * (曾发生:webview 不支持 window.prompt,调用抛异常把插件 UI 整体卸载——
2177
+ * 人设选择按钮与记忆卡一起消失。边界把爆炸范围圈在插件内。)
2178
+ */
2179
+ var LumeErrorBoundary = class extends react.Component {
2180
+ state = { failed: false };
2181
+ static getDerivedStateFromError() {
2182
+ return { failed: true };
2183
+ }
2184
+ render() {
2185
+ return this.state.failed ? null : this.props.children;
2186
+ }
2187
+ };
2188
+ function PersonaSelect({ available, load, select, callRpc, t }) {
2189
+ const [open, setOpen] = (0, react.useState)(false);
2190
+ const [loading, setLoading] = (0, react.useState)(false);
2191
+ const [items, setItems] = (0, react.useState)([]);
2192
+ const [current, setCurrent] = (0, react.useState)(null);
2193
+ const [distillOpen, setDistillOpen] = (0, react.useState)(false);
2194
+ const [manageOpen, setManageOpen] = (0, react.useState)(false);
2195
+ const [reloadToken, setReloadToken] = (0, react.useState)(0);
2196
+ const loadedTokenRef = (0, react.useRef)(-1);
2197
+ (0, react.useEffect)(() => {
2198
+ if (!available) return;
2199
+ if (loadedTokenRef.current === reloadToken) return;
2200
+ let cancelled = false;
2201
+ setLoading(true);
2202
+ load().then(({ list, current: curr }) => {
2203
+ if (cancelled) return;
2204
+ setItems(list);
2205
+ setCurrent(curr);
2206
+ setLoading(false);
2207
+ window.setTimeout(() => window.dispatchEvent(new Event("resize")), 50);
2208
+ }).catch(() => {
2209
+ if (!cancelled) setLoading(false);
2210
+ }).finally(() => {
2211
+ loadedTokenRef.current = reloadToken;
2212
+ });
2213
+ return () => {
2214
+ cancelled = true;
2215
+ };
2216
+ }, [
2217
+ available,
2218
+ reloadToken,
2219
+ load
2220
+ ]);
2221
+ if (!available) return null;
2222
+ const ordered = orderPersonaItems(items);
2223
+ const labels = resolveLabels(ordered, (it) => it.profileName ?? it.displayName, t("menu.custom.suffix"));
2224
+ const labelOf = (it) => it ? labels.get(it.name) ?? it.profileName ?? it.displayName : t("trigger.fallback");
2225
+ const currentLabel = labelOf(ordered.find((it) => it.name === current) ?? items.find((it) => it.name === current));
2226
+ const entries = loading ? [{
2227
+ type: "label",
2228
+ id: "lume-loading",
2229
+ text: t("status.loading")
2230
+ }] : ordered.length === 0 ? [{
2231
+ type: "label",
2232
+ id: "lume-empty",
2233
+ text: t("empty")
2234
+ }] : ordered.map((item) => ({
2235
+ id: item.name,
2236
+ label: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2237
+ style: {
2238
+ display: "flex",
2239
+ flexDirection: "column",
2240
+ gap: 1
2241
+ },
2242
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: labelOf(item) }), item.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2243
+ style: {
2244
+ fontSize: 10,
2245
+ opacity: .6
2246
+ },
2247
+ children: item.description
2248
+ }) : null]
2249
+ })
2250
+ }));
2251
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
2253
+ open,
2254
+ portal: true,
2255
+ side: "top",
2256
+ align: "start",
2257
+ items: entries,
2258
+ footer: [{
2259
+ id: MANAGE_ITEM_ID,
2260
+ label: t("manage.menu")
2261
+ }, {
2262
+ id: DISTILL_ITEM_ID,
2263
+ label: t("distill.menu")
2264
+ }],
2265
+ selectedId: current ?? void 0,
2266
+ onSelect: (id) => {
2267
+ if (id === DISTILL_ITEM_ID) {
2268
+ setOpen(false);
2269
+ setDistillOpen(true);
2270
+ return;
2271
+ }
2272
+ if (id === MANAGE_ITEM_ID) {
2273
+ setOpen(false);
2274
+ setManageOpen(true);
2275
+ return;
2276
+ }
2277
+ select(id).then((ok) => {
2278
+ if (ok) setCurrent(id);
2279
+ });
2280
+ setOpen(false);
2281
+ },
2282
+ onClose: () => setOpen(false),
2283
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2284
+ className: "lume-persona-trigger",
2285
+ onClick: () => setOpen((v) => !v),
2286
+ "aria-label": t("trigger.aria", { persona: currentLabel }),
2287
+ "aria-haspopup": "listbox",
2288
+ "aria-expanded": open,
2289
+ style: {
2290
+ background: "none",
2291
+ border: "1px solid var(--color-border, #333)",
2292
+ borderRadius: 6,
2293
+ padding: "2px 8px",
2294
+ fontSize: 12,
2295
+ color: "var(--color-text-secondary, #999)",
2296
+ cursor: "pointer",
2297
+ display: "flex",
2298
+ alignItems: "center",
2299
+ gap: 4
2300
+ },
2301
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: currentLabel }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2302
+ style: { fontSize: 10 },
2303
+ children: open ? "▴" : "▾"
2304
+ })]
2305
+ })
2306
+ }),
2307
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DistillModal, {
2308
+ open: distillOpen,
2309
+ onClose: () => setDistillOpen(false),
2310
+ onSaved: () => setReloadToken((v) => v + 1),
2311
+ t,
2312
+ callRpc
2313
+ }),
2314
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ManageModal, {
2315
+ open: manageOpen,
2316
+ onClose: () => setManageOpen(false),
2317
+ onSaved: () => setReloadToken((v) => v + 1),
2318
+ t,
2319
+ callRpc,
2320
+ items
2321
+ })
2322
+ ] });
2323
+ }
2324
+ /** 客户端插件依赖服务 */
2325
+ const inject = [
2326
+ "connection",
2327
+ "locale",
2328
+ "slots"
2329
+ ];
2330
+ /** 客户端插件入口:注册人设词典 + 输入栏人设插槽 */
2331
+ function apply(ctx) {
2332
+ ctx.effect(() => ctx.locale.register(NS, {
2333
+ zh,
2334
+ en
2335
+ }), "lume: dictionaries");
2336
+ ctx.inject(["slots", "connection"], (scope) => {
2337
+ const conn = scope.connection;
2338
+ scope.slots.inject("conversation.input.left", () => scope.slots.register({
2339
+ name: "conversation.input.left",
2340
+ id: "lume-persona",
2341
+ order: 10,
2342
+ locale: NS,
2343
+ inject: (sessionId) => {
2344
+ const available = sessionId != null;
2345
+ /** 加载人设列表 + 当前会话的显式人设选择 */
2346
+ async function load() {
2347
+ let list = [];
2348
+ let current = null;
2349
+ try {
2350
+ const result = await conn.rpc.call("/lume", "list", {}, void 0);
2351
+ if (result?.ok && Array.isArray(result.value)) list = result.value;
2352
+ } catch {}
2353
+ try {
2354
+ const r = await conn.rpc.call("/lume", "getSessionPersona", { sessionId }, void 0);
2355
+ if (r?.ok && (typeof r.value === "string" || r.value === null)) current = r.value;
2356
+ } catch {}
2357
+ return {
2358
+ list,
2359
+ current
2360
+ };
2361
+ }
2362
+ /** 选择人设 */
2363
+ async function select(personaName) {
2364
+ if (sessionId == null) return false;
2365
+ try {
2366
+ return (await conn.rpc.call("/lume", "select", {
2367
+ sessionId,
2368
+ personaName
2369
+ }, void 0))?.ok === true;
2370
+ } catch {
2371
+ return false;
2372
+ }
2373
+ }
2374
+ /** 通用 /lume RPC(蒸馏弹窗用) */
2375
+ function callRpc(endpoint, payload) {
2376
+ return conn.rpc.call("/lume", endpoint, payload, void 0);
2377
+ }
2378
+ return {
2379
+ available,
2380
+ load,
2381
+ select,
2382
+ callRpc
2383
+ };
2384
+ }
2385
+ }, (props) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LumeErrorBoundary, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PersonaSelect, { ...props }) })));
2386
+ });
2387
+ }
2388
+
2389
+ //#endregion
2390
+ exports.apply = apply;
2391
+ exports.inject = inject;
2392
+
2393
+ return module.exports;
2394
+ }
2395
+ });