dsh-prime-memory 0.11.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.
Files changed (151) hide show
  1. package/CHANGELOG.en.md +28 -0
  2. package/CHANGELOG.ja.md +30 -0
  3. package/CHANGELOG.ko.md +30 -0
  4. package/CHANGELOG.md +1220 -0
  5. package/ENGINEERING-NOTES.md +452 -0
  6. package/INSTALL.en.md +92 -0
  7. package/INSTALL.ja.md +92 -0
  8. package/INSTALL.ko.md +92 -0
  9. package/INSTALL.md +92 -0
  10. package/LICENSE +21 -0
  11. package/README.en.md +458 -0
  12. package/README.ja.md +306 -0
  13. package/README.ko.md +306 -0
  14. package/README.md +425 -0
  15. package/assets/changelog/0.8.10/01-write-only-pill.png +0 -0
  16. package/assets/changelog/0.8.9/01-panel.png +0 -0
  17. package/assets/changelog/0.8.9/02-halo.png +0 -0
  18. package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
  19. package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
  20. package/assets/img/EmbeddingSource.png +0 -0
  21. package/assets/img/Hero.png +0 -0
  22. package/assets/img/Layers.png +0 -0
  23. package/assets/img/MemoryTools.png +0 -0
  24. package/assets/img/Modes.png +0 -0
  25. package/assets/img/ToolTrajectory.png +0 -0
  26. package/assets/img/ui-dark.jpg +0 -0
  27. package/assets/img/ui-light.jpg +0 -0
  28. package/assets/readme/bench-dialog.svg +70 -0
  29. package/assets/readme/bench-workflow.svg +79 -0
  30. package/assets/readme/flow.svg +189 -0
  31. package/assets/readme/storage.svg +115 -0
  32. package/cordis.patch.yml +16 -0
  33. package/dist/bench-control.d.ts +34 -0
  34. package/dist/bench-control.js +16 -0
  35. package/dist/client.js +4293 -0
  36. package/dist/config.d.ts +683 -0
  37. package/dist/config.js +129 -0
  38. package/dist/contract.d.ts +820 -0
  39. package/dist/contract.js +1 -0
  40. package/dist/embedding-worker.cjs +176 -0
  41. package/dist/graph/apply.d.ts +37 -0
  42. package/dist/graph/apply.js +270 -0
  43. package/dist/graph/constraints.d.ts +47 -0
  44. package/dist/graph/constraints.js +38 -0
  45. package/dist/graph/search.d.ts +16 -0
  46. package/dist/graph/search.js +115 -0
  47. package/dist/graph/types.d.ts +142 -0
  48. package/dist/graph/types.js +14 -0
  49. package/dist/hooks/capture.d.ts +32 -0
  50. package/dist/hooks/capture.js +194 -0
  51. package/dist/hooks/recall.d.ts +63 -0
  52. package/dist/hooks/recall.js +429 -0
  53. package/dist/index.d.ts +534 -0
  54. package/dist/index.js +344 -0
  55. package/dist/llm-usage.d.ts +26 -0
  56. package/dist/llm-usage.js +37 -0
  57. package/dist/llm.d.ts +153 -0
  58. package/dist/llm.js +530 -0
  59. package/dist/pipeline/graph.d.ts +35 -0
  60. package/dist/pipeline/graph.js +104 -0
  61. package/dist/pipeline/l1.d.ts +19 -0
  62. package/dist/pipeline/l1.js +271 -0
  63. package/dist/pipeline/l2.d.ts +13 -0
  64. package/dist/pipeline/l2.js +83 -0
  65. package/dist/pipeline/l3.d.ts +15 -0
  66. package/dist/pipeline/l3.js +78 -0
  67. package/dist/pipeline/rebuild.d.ts +61 -0
  68. package/dist/pipeline/rebuild.js +307 -0
  69. package/dist/pipeline/ruminate.d.ts +89 -0
  70. package/dist/pipeline/ruminate.js +298 -0
  71. package/dist/pipeline/runner.d.ts +167 -0
  72. package/dist/pipeline/runner.js +638 -0
  73. package/dist/pipeline/trigger.d.ts +40 -0
  74. package/dist/pipeline/trigger.js +75 -0
  75. package/dist/prompts/graph-projection.d.ts +70 -0
  76. package/dist/prompts/graph-projection.js +167 -0
  77. package/dist/prompts/l1-dedup.d.ts +22 -0
  78. package/dist/prompts/l1-dedup.js +251 -0
  79. package/dist/prompts/l1-extraction.d.ts +22 -0
  80. package/dist/prompts/l1-extraction.js +457 -0
  81. package/dist/prompts/persona.d.ts +23 -0
  82. package/dist/prompts/persona.js +240 -0
  83. package/dist/prompts/scene.d.ts +32 -0
  84. package/dist/prompts/scene.js +414 -0
  85. package/dist/runtime-package-lock.json +982 -0
  86. package/dist/settings.d.ts +50 -0
  87. package/dist/settings.js +355 -0
  88. package/dist/stats.d.ts +109 -0
  89. package/dist/stats.js +929 -0
  90. package/dist/store/bm25.d.ts +19 -0
  91. package/dist/store/bm25.js +63 -0
  92. package/dist/store/cost-ledger.d.ts +75 -0
  93. package/dist/store/cost-ledger.js +171 -0
  94. package/dist/store/download-queue.d.ts +79 -0
  95. package/dist/store/download-queue.js +424 -0
  96. package/dist/store/embedding-source.d.ts +118 -0
  97. package/dist/store/embedding-source.js +443 -0
  98. package/dist/store/embedding.d.ts +90 -0
  99. package/dist/store/embedding.js +206 -0
  100. package/dist/store/graph-store.d.ts +94 -0
  101. package/dist/store/graph-store.js +641 -0
  102. package/dist/store/l0.d.ts +40 -0
  103. package/dist/store/l0.js +197 -0
  104. package/dist/store/l1.d.ts +93 -0
  105. package/dist/store/l1.js +297 -0
  106. package/dist/store/local-embedding.d.ts +89 -0
  107. package/dist/store/local-embedding.js +227 -0
  108. package/dist/store/model-catalog.d.ts +48 -0
  109. package/dist/store/model-catalog.js +81 -0
  110. package/dist/store/occupancy.d.ts +30 -0
  111. package/dist/store/occupancy.js +134 -0
  112. package/dist/store/pending.d.ts +36 -0
  113. package/dist/store/pending.js +103 -0
  114. package/dist/store/persona.d.ts +15 -0
  115. package/dist/store/persona.js +60 -0
  116. package/dist/store/recall-dedupe.d.ts +26 -0
  117. package/dist/store/recall-dedupe.js +138 -0
  118. package/dist/store/runtime-installer.d.ts +59 -0
  119. package/dist/store/runtime-installer.js +243 -0
  120. package/dist/store/scenes.d.ts +24 -0
  121. package/dist/store/scenes.js +160 -0
  122. package/dist/store/search-utils.d.ts +38 -0
  123. package/dist/store/search-utils.js +100 -0
  124. package/dist/store/session-modes.d.ts +35 -0
  125. package/dist/store/session-modes.js +144 -0
  126. package/dist/store/sqlite.d.ts +246 -0
  127. package/dist/store/sqlite.js +1491 -0
  128. package/dist/store/state.d.ts +41 -0
  129. package/dist/store/state.js +72 -0
  130. package/dist/token-cost.d.ts +23 -0
  131. package/dist/token-cost.js +185 -0
  132. package/dist/tools/index.d.ts +34 -0
  133. package/dist/tools/index.js +758 -0
  134. package/dist/types.d.ts +139 -0
  135. package/dist/types.js +38 -0
  136. package/dist/util/context-occupancy.d.ts +68 -0
  137. package/dist/util/context-occupancy.js +92 -0
  138. package/dist/util/filelog.d.ts +6 -0
  139. package/dist/util/filelog.js +108 -0
  140. package/dist/util/io.d.ts +18 -0
  141. package/dist/util/io.js +97 -0
  142. package/dist/util/recall-budget.d.ts +31 -0
  143. package/dist/util/recall-budget.js +84 -0
  144. package/dist/util/sanitize.d.ts +11 -0
  145. package/dist/util/sanitize.js +67 -0
  146. package/dist/util/text.d.ts +16 -0
  147. package/dist/util/text.js +61 -0
  148. package/dist/util/tokenizer.d.ts +9 -0
  149. package/dist/util/tokenizer.js +50 -0
  150. package/dsh.plugin.json +22 -0
  151. package/package.json +118 -0
package/dist/client.js ADDED
@@ -0,0 +1,4293 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-prime-memory",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+
25
+ // client/src/entry.tsx
26
+ var entry_exports = {};
27
+ __export(entry_exports, {
28
+ apply: () => apply,
29
+ inject: () => inject
30
+ });
31
+ module.exports = __toCommonJS(entry_exports);
32
+
33
+ // client/src/panel.tsx
34
+ var import_react16 = require("react");
35
+
36
+ // client/src/sidebar-icon.ts
37
+ var BOOK_ICON_SVG = '<svg data-mem-icon="1" viewBox="0 0 16 16" width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg" style="flex-shrink:0"><path d="M8 3.4C6.6 2.5 4.6 2.4 2.9 3.1v9.3c1.7-.7 3.7-.6 5.1.3 1.4-.9 3.4-1 5.1-.3V3.1C11.4 2.4 9.4 2.5 8 3.4Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/><path d="M8 3.4v9.3" stroke="currentColor" stroke-width="1.2"/></svg>';
38
+ function patchSidebarIcon() {
39
+ try {
40
+ const buttons = document.querySelectorAll("button");
41
+ for (let i = 0; i < buttons.length; i++) {
42
+ const b = buttons[i];
43
+ const span = b.querySelector("span");
44
+ const svg = b.querySelector("svg");
45
+ if (span && svg && span.textContent && span.textContent.trim() === "记忆" && svg.getAttribute("data-mem-icon") !== "1") {
46
+ svg.outerHTML = BOOK_ICON_SVG;
47
+ }
48
+ }
49
+ } catch {
50
+ }
51
+ }
52
+ var sidebarIconTimer = null;
53
+ function scheduleSidebarPatch() {
54
+ if (sidebarIconTimer !== null) return;
55
+ sidebarIconTimer = setTimeout(() => {
56
+ sidebarIconTimer = null;
57
+ patchSidebarIcon();
58
+ }, 250);
59
+ }
60
+ function watchSidebarIcon() {
61
+ patchSidebarIcon();
62
+ try {
63
+ if (document.body.getAttribute("data-mem-icon-bodywatch") === "1") return;
64
+ document.body.setAttribute("data-mem-icon-bodywatch", "1");
65
+ new MutationObserver(scheduleSidebarPatch).observe(document.body, { childList: true, subtree: true });
66
+ } catch {
67
+ }
68
+ }
69
+
70
+ // client/src/styles.ts
71
+ var S = {
72
+ section: { padding: "0 4px" },
73
+ heading: { fontSize: 16, fontWeight: 600, margin: "0 0 4px", color: "var(--dsh-mem-text-1)" },
74
+ intro: { fontSize: 13, color: "var(--dsh-mem-text-3)", margin: "0 0 12px" },
75
+ tabbar: { display: "flex", gap: 2, borderBottom: "1px solid var(--dsh-mem-border)", marginBottom: 14 },
76
+ error: {
77
+ marginTop: 10,
78
+ fontSize: 13,
79
+ color: "var(--dsh-mem-danger)",
80
+ whiteSpace: "pre-wrap"
81
+ },
82
+ hint: { marginTop: 12, fontSize: 12, color: "var(--dsh-mem-text-3)" },
83
+ toolbar: { display: "flex", gap: 8, marginBottom: 12, flexWrap: "wrap", alignItems: "center" },
84
+ card: {
85
+ padding: "10px 12px",
86
+ marginBottom: 8,
87
+ fontSize: 13
88
+ },
89
+ cardHead: { display: "flex", alignItems: "center", gap: 8, marginBottom: 4 },
90
+ muted: { color: "var(--dsh-mem-text-3)", fontSize: 12 },
91
+ content: { lineHeight: 1.5, wordBreak: "break-word", color: "var(--dsh-mem-text-1)" },
92
+ detail: {
93
+ marginTop: 8,
94
+ paddingTop: 8,
95
+ borderTop: "1px dashed var(--dsh-mem-border)",
96
+ fontSize: 12,
97
+ fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace",
98
+ color: "var(--dsh-mem-text-2)",
99
+ whiteSpace: "pre-wrap"
100
+ },
101
+ pre: {
102
+ margin: 0,
103
+ padding: "10px 12px",
104
+ background: "var(--dsh-mem-bg-inset)",
105
+ border: "1px solid var(--dsh-mem-border)",
106
+ borderRadius: 10,
107
+ fontSize: 12,
108
+ fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace",
109
+ whiteSpace: "pre-wrap",
110
+ wordBreak: "break-word",
111
+ lineHeight: 1.6,
112
+ maxHeight: 480,
113
+ overflow: "auto"
114
+ },
115
+ switchRow: { display: "flex", alignItems: "center", gap: 10, padding: "8px 0" },
116
+ switchLabel: { fontSize: 13, fontWeight: 600, minWidth: 72, color: "var(--dsh-mem-text-1)" },
117
+ switchDesc: { fontSize: 12, color: "var(--dsh-mem-text-3)" },
118
+ switch: {
119
+ width: 36,
120
+ height: 20,
121
+ borderRadius: 999,
122
+ position: "relative",
123
+ cursor: "pointer",
124
+ transition: "background .15s",
125
+ flexShrink: 0
126
+ },
127
+ switchOn: { background: "var(--dsh-mem-accent-fill)" },
128
+ switchOff: { background: "var(--dsh-mem-border-strong)" },
129
+ switchDisabled: { opacity: 0.4, cursor: "not-allowed" },
130
+ knob: {
131
+ position: "absolute",
132
+ top: 2,
133
+ width: 16,
134
+ height: 16,
135
+ borderRadius: "50%",
136
+ background: "var(--dsh-mem-thumb)",
137
+ transition: "left .15s",
138
+ boxShadow: "0 1px 2px rgba(0,0,0,.25)"
139
+ },
140
+ switchPanel: {
141
+ border: "1px solid var(--dsh-mem-border)",
142
+ borderRadius: 10,
143
+ background: "var(--dsh-mem-bg-card)",
144
+ boxShadow: "var(--dsh-mem-shadow-card)",
145
+ padding: "4px 14px",
146
+ marginBottom: 14
147
+ },
148
+ panelLabel: {
149
+ fontSize: 12,
150
+ fontWeight: 600,
151
+ color: "var(--dsh-mem-text-3)",
152
+ margin: "12px 0 2px"
153
+ },
154
+ statGrid: {
155
+ display: "grid",
156
+ gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))",
157
+ gap: 8,
158
+ marginBottom: 14
159
+ },
160
+ statTile: { padding: "10px 12px" },
161
+ statNum: { fontSize: 20, fontWeight: 650, lineHeight: "28px", color: "var(--dsh-mem-text-1)" },
162
+ statLabel: { fontSize: 12, color: "var(--dsh-mem-text-3)", marginTop: 2 },
163
+ infoRow: {
164
+ display: "flex",
165
+ alignItems: "baseline",
166
+ gap: 12,
167
+ padding: "5px 0",
168
+ borderBottom: "1px solid var(--dsh-mem-border)"
169
+ },
170
+ infoKey: { fontSize: 12.5, color: "var(--dsh-mem-text-3)", whiteSpace: "nowrap", minWidth: 96 },
171
+ infoVal: {
172
+ fontSize: 12.5,
173
+ color: "var(--dsh-mem-text-1)",
174
+ fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace",
175
+ wordBreak: "break-all",
176
+ textAlign: "right",
177
+ flex: 1
178
+ },
179
+ seg: {
180
+ display: "inline-flex",
181
+ border: "1px solid var(--dsh-mem-border)",
182
+ borderRadius: 8,
183
+ overflow: "hidden",
184
+ flexShrink: 0,
185
+ background: "var(--dsh-mem-bg-inset)"
186
+ },
187
+ segBtn: {
188
+ padding: "4px 12px",
189
+ fontSize: 12,
190
+ lineHeight: "16px",
191
+ cursor: "pointer",
192
+ background: "transparent",
193
+ color: "var(--dsh-mem-text-2)",
194
+ border: "none",
195
+ borderRight: "1px solid var(--dsh-mem-border)"
196
+ },
197
+ segBtnOn: {
198
+ background: "var(--dsh-mem-accent-fill)",
199
+ color: "#fff",
200
+ fontWeight: 600
201
+ },
202
+ flexRow: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
203
+ grow: { flex: 1 },
204
+ sceneHead: { display: "flex", alignItems: "baseline", gap: 10, marginBottom: 6, flexWrap: "wrap" },
205
+ sceneTitle: { fontSize: 13, fontWeight: 600, fontFamily: "ui-monospace, Consolas, monospace", color: "var(--dsh-mem-text-1)" }
206
+ };
207
+
208
+ // client/src/theme.ts
209
+ var THEME_STYLE_ID = "dsh-mem-theme-style";
210
+ function ensureThemeStyle() {
211
+ if (document.getElementById(THEME_STYLE_ID)) return;
212
+ const el = document.createElement("style");
213
+ el.id = THEME_STYLE_ID;
214
+ el.textContent = [
215
+ // conic 角度要 @property 注册才能插值动画;不支持 @property 的浏览器里
216
+ // var(--dsh-mem-angle) 无定义 → conic 层失效 → background 整条退化为无背景
217
+ //(光带边框与内底一起消失,只剩文字色)。2026 常青浏览器均已支持,仅作记录。
218
+ "@property --dsh-mem-angle { syntax: '<angle>'; initial-value: 0deg; inherits: false; }",
219
+ "@keyframes dshMemFlow { to { --dsh-mem-angle: 360deg; } }",
220
+ // ── 令牌(浅色)。中性色链选用【真实存在】的 dsw 令牌(design-platform.css 校对):
221
+ // bg-layer-2/3、bg-overlay、border-l1/l2/l3、border-inverted、label-*、
222
+ // interactive-bg-hover、state-error-primary、tooltip-bg、dsw-shadow-lv1/lv3 ──
223
+ ":root {",
224
+ " --dsh-mem-accent: #4d6bfe;",
225
+ " --dsh-mem-accent-text: #3d5be0;",
226
+ " --dsh-mem-accent-fill: #3d5be0;",
227
+ " --dsh-mem-accent-weak: rgba(77,107,254,0.10);",
228
+ " --dsh-mem-bg-card: var(--dsw-alias-bg-layer-2, #ffffff);",
229
+ " --dsh-mem-bg-inset: var(--dsw-alias-bg-overlay, #e9ecf2);",
230
+ " --dsh-mem-bg-hover: var(--dsw-alias-interactive-bg-hover, rgba(38,49,72,0.06));",
231
+ " --dsh-mem-bg-pop: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3, #ffffff));",
232
+ " --dsh-mem-border: var(--dsw-alias-border-l2, rgba(0,0,0,0.10));",
233
+ " --dsh-mem-border-strong: var(--dsw-alias-border-l3, rgba(0,0,0,0.12));",
234
+ " --dsh-mem-border-pop: var(--dsw-alias-border-inverted, rgba(0,0,0,0));",
235
+ " --dsh-mem-text-1: var(--dsw-alias-label-primary, #0f1115);",
236
+ " --dsh-mem-text-2: var(--dsw-alias-label-secondary, #61666b);",
237
+ " --dsh-mem-text-3: var(--dsw-alias-label-tertiary, #6e7781);",
238
+ " --dsh-mem-danger: var(--dsw-alias-state-error-primary, #d0403f);",
239
+ // 成本折线图系列色:8 档固定色;PALETTE 只引用 var(),1 档锚品牌蓝、8 档中性"其他"
240
+ " --dsh-mem-chart-1: #4d6bfe;",
241
+ " --dsh-mem-chart-2: #0e9c8f;",
242
+ " --dsh-mem-chart-3: #1f9d55;",
243
+ " --dsh-mem-chart-4: #a8821c;",
244
+ " --dsh-mem-chart-5: #d97a0d;",
245
+ " --dsh-mem-chart-6: #d64570;",
246
+ " --dsh-mem-chart-7: #7c5cff;",
247
+ " --dsh-mem-chart-8: #61666b;",
248
+ // 档位色 = 灰 → 品牌蓝的渐变阶(chat/work 过渡蓝、auto 品牌蓝);
249
+ // 文字对比度按 pill 实际底色(流光内底 = bg-card 97% + 档位色 3%)复算 AA 达标
250
+ " --dsh-mem-mode-chat: #5a69b0;",
251
+ " --dsh-mem-mode-work: #5263ca;",
252
+ " --dsh-mem-mode-auto: #3d5be0;",
253
+ // 滑轨填充渐变(左浅右深)
254
+ " --dsh-mem-fill-1: #7b93ff;",
255
+ " --dsh-mem-fill-2: #3d5be0;",
256
+ " --dsh-mem-thumb: #ffffff;",
257
+ " --dsh-mem-track: rgba(128,140,150,0.32);",
258
+ " --dsh-mem-dot: rgba(128,140,150,0.55);",
259
+ " --dsh-mem-shadow-card: var(--dsw-shadow-lv1, 0 2px 4px 0 rgba(0,0,0,0.05));",
260
+ " --dsh-mem-shadow-pop: var(--dsw-shadow-lv3, 0 0 1px 0 rgba(0,0,0,.2), 0 0 4px 0 rgba(0,0,0,.02), 0 12px 32px 0 rgba(0,0,0,0.08));",
261
+ "}",
262
+ // ── 令牌(暗色:body[data-ds-dark-theme] 是 dsh 前端的暗色开关) ──
263
+ "body[data-ds-dark-theme] {",
264
+ " --dsh-mem-accent: #6e85ff;",
265
+ " --dsh-mem-accent-text: #7b90ff;",
266
+ " --dsh-mem-accent-fill: #465ce8;",
267
+ " --dsh-mem-accent-weak: rgba(110,133,255,0.14);",
268
+ " --dsh-mem-bg-card: var(--dsw-alias-bg-layer-2, #2c2c2e);",
269
+ " --dsh-mem-bg-inset: var(--dsw-alias-bg-layer-1, #232324);",
270
+ " --dsh-mem-bg-hover: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,0.08));",
271
+ " --dsh-mem-bg-pop: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3, #353638));",
272
+ " --dsh-mem-border: var(--dsw-alias-border-l2, rgba(255,255,255,0.12));",
273
+ " --dsh-mem-border-strong: var(--dsw-alias-border-l3, rgba(255,255,255,0.16));",
274
+ " --dsh-mem-border-pop: var(--dsw-alias-border-inverted, rgba(255,255,255,0.06));",
275
+ " --dsh-mem-text-1: var(--dsw-alias-label-primary, #f9fafb);",
276
+ " --dsh-mem-text-2: var(--dsw-alias-label-secondary, #cfd3d6);",
277
+ " --dsh-mem-text-3: var(--dsw-alias-label-tertiary, #8892a6);",
278
+ " --dsh-mem-danger: var(--dsw-alias-state-error-primary, #f4707b);",
279
+ " --dsh-mem-chart-1: #6e85ff;",
280
+ " --dsh-mem-chart-2: #35c4b5;",
281
+ " --dsh-mem-chart-3: #52c98d;",
282
+ " --dsh-mem-chart-4: #d9b23e;",
283
+ " --dsh-mem-chart-5: #f59e5b;",
284
+ " --dsh-mem-chart-6: #f47ba2;",
285
+ " --dsh-mem-chart-7: #a78bfa;",
286
+ " --dsh-mem-chart-8: #8892a6;",
287
+ " --dsh-mem-mode-chat: #97a4ff;",
288
+ " --dsh-mem-mode-work: #8295ff;",
289
+ " --dsh-mem-mode-auto: #7b90ff;",
290
+ " --dsh-mem-fill-1: #8fa0ff;",
291
+ " --dsh-mem-fill-2: #465ce8;",
292
+ " --dsh-mem-thumb: #e8ebf5;",
293
+ " --dsh-mem-track: rgba(148,160,180,0.30);",
294
+ " --dsh-mem-dot: rgba(148,160,180,0.5);",
295
+ " --dsh-mem-shadow-card: var(--dsw-shadow-lv1, 0 2px 4px 0 rgba(0,0,0,0.3));",
296
+ " --dsh-mem-shadow-pop: var(--dsw-shadow-lv3, 0 0 1px 0 rgba(0,0,0,.2), 0 0 4px 0 rgba(0,0,0,.02), 0 12px 32px 0 rgba(0,0,0,0.08));",
297
+ "}",
298
+ // ── 主题切换过渡:只过渡颜色/阴影(不碰 transform),明暗翻转不生硬 ──
299
+ ".dsh-mem-root, .dsh-mem-root * { transition: background-color .18s ease, border-color .18s ease, color .18s ease, box-shadow .18s ease; }",
300
+ // ── 控件:按钮(次级)── 圆角体系:控件 8 / 卡片 10 / 浮层 12 / 胶囊 999
301
+ ".dsh-mem-btn {",
302
+ " padding: 5px 14px; font-size: 13px; line-height: 20px; border-radius: 8px; cursor: pointer;",
303
+ " border: 1px solid var(--dsh-mem-border); background: var(--dsh-mem-bg-card); color: var(--dsh-mem-text-1);",
304
+ " transition: background-color .15s ease, border-color .15s ease, transform .08s ease;",
305
+ "}",
306
+ ".dsh-mem-btn:hover:not(:disabled) { border-color: var(--dsh-mem-border-strong); background: var(--dsh-mem-bg-hover); }",
307
+ ".dsh-mem-btn:active:not(:disabled) { transform: scale(0.98); }",
308
+ ".dsh-mem-btn:focus-visible { outline: 2px solid var(--dsh-mem-accent); outline-offset: 1px; }",
309
+ ".dsh-mem-btn:disabled { opacity: 0.45; cursor: not-allowed; }",
310
+ // ── 控件:输入框 / 下拉 ──
311
+ ".dsh-mem-input {",
312
+ " padding: 5px 10px; font-size: 13px; border-radius: 8px; color: var(--dsh-mem-text-1);",
313
+ " border: 1px solid var(--dsh-mem-border); background: var(--dsh-mem-bg-card);",
314
+ " transition: border-color .15s ease, box-shadow .15s ease;",
315
+ "}",
316
+ ".dsh-mem-input:focus {",
317
+ " outline: none; border-color: var(--dsh-mem-accent);",
318
+ " box-shadow: 0 0 0 3px var(--dsh-mem-accent-weak);",
319
+ "}",
320
+ // ── 下拉触发钮(NSel):观感与输入框一致(8px 圆角/同令牌边框底色),
321
+ // 文字 ellipsis + CSS 描边 chevron(展开旋转 180°);弹出面板见 .dsh-mem-pop ──
322
+ ".dsh-mem-select {",
323
+ " display: inline-flex; align-items: center; justify-content: space-between; gap: 8px;",
324
+ " width: 100%; min-width: 0; padding: 5px 10px; font: inherit; font-size: 13px; line-height: 20px;",
325
+ " text-align: left; border-radius: 8px; cursor: pointer; color: var(--dsh-mem-text-1);",
326
+ " border: 1px solid var(--dsh-mem-border); background: var(--dsh-mem-bg-card);",
327
+ " transition: border-color .15s ease, box-shadow .15s ease;",
328
+ "}",
329
+ ".dsh-mem-select:focus-visible {",
330
+ " outline: none; border-color: var(--dsh-mem-accent);",
331
+ " box-shadow: 0 0 0 3px var(--dsh-mem-accent-weak);",
332
+ "}",
333
+ ".dsh-mem-select:disabled { opacity: 0.45; cursor: not-allowed; }",
334
+ ".dsh-mem-select-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
335
+ // 下拉弹出面板:dsh MenuDropdown 同款——浮层 12 圆角 + menu 底 + lv3 投影;
336
+ // 选项行 10 圆角 + hover 底色 + 选中打勾(active 由 data-active 标记)
337
+ ".dsh-mem-sel { position: relative; display: inline-flex; min-width: 0; vertical-align: top; }",
338
+ ".dsh-mem-sel-chev {",
339
+ " width: 8px; height: 8px; flex: none; margin-right: 2px;",
340
+ " border-right: 1.5px solid var(--dsh-mem-text-3); border-bottom: 1.5px solid var(--dsh-mem-text-3);",
341
+ " transform: rotate(45deg); transition: transform .12s ease;",
342
+ "}",
343
+ ".dsh-mem-sel-chev-open { transform: rotate(225deg); }",
344
+ ".dsh-mem-pop {",
345
+ " position: absolute; top: calc(100% + 6px); left: 0; z-index: 30;",
346
+ " min-width: 100%; width: max-content; max-width: 340px; max-height: 264px;",
347
+ " overflow-y: auto; overscroll-behavior: contain; padding: 4px;",
348
+ " background: var(--dsh-mem-bg-pop); border: 1px solid var(--dsh-mem-border-pop);",
349
+ " border-radius: 12px; box-shadow: var(--dsh-mem-shadow-pop);",
350
+ "}",
351
+ ".dsh-mem-pop-opt {",
352
+ " display: flex; align-items: center; gap: 8px; width: 100%; box-sizing: border-box;",
353
+ " min-height: 32px; padding: 5px 10px; font: inherit; font-size: 13px; line-height: 20px;",
354
+ " text-align: left; color: var(--dsh-mem-text-1); cursor: pointer;",
355
+ " background: none; border: none; outline: none; border-radius: 10px;",
356
+ "}",
357
+ '.dsh-mem-pop-opt:hover, .dsh-mem-pop-opt[data-active="1"] { background: var(--dsh-mem-bg-hover); }',
358
+ ".dsh-mem-pop-opt-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
359
+ ".dsh-mem-pop-check { flex: none; font-size: 12px; color: var(--dsh-mem-text-1); }",
360
+ ".dsh-mem-pop-empty { padding: 10px; font-size: 13px; color: var(--dsh-mem-text-3); }",
361
+ // ── Tab(下划线式):active 品牌蓝下划线 + 主文字色 ──
362
+ ".dsh-mem-tab {",
363
+ " padding: 6px 12px; font-size: 13px; cursor: pointer; background: none; border: none;",
364
+ " color: var(--dsh-mem-text-2); border-bottom: 2px solid transparent; margin-bottom: -1px;",
365
+ "}",
366
+ ".dsh-mem-tab:hover { color: var(--dsh-mem-text-1); }",
367
+ ".dsh-mem-tab-on { font-weight: 600; color: var(--dsh-mem-text-1); border-bottom-color: var(--dsh-mem-accent); }",
368
+ ".dsh-mem-tab:focus-visible { outline: 2px solid var(--dsh-mem-accent); outline-offset: -2px; }",
369
+ // ── 卡片:hover 微抬(边框加深),只给可交互卡片用 ──
370
+ ".dsh-mem-card {",
371
+ " border: 1px solid var(--dsh-mem-border); border-radius: 10px; background: var(--dsh-mem-bg-card);",
372
+ " box-shadow: var(--dsh-mem-shadow-card);",
373
+ "}",
374
+ ".dsh-mem-card-hover:hover { border-color: var(--dsh-mem-border-strong); }",
375
+ // ── 场景卡折叠箭头:展开态旋转 90°(进 reduced-motion 压制名单) ──
376
+ ".dsh-mem-scene-chev { display: inline-block; transition: transform .15s ease; color: var(--dsh-mem-text-3); }",
377
+ // ── 记忆类型标签:tint 风格(彩底淡色 + 彩字),--dsh-mem-tag-c 由类型类给定 ──
378
+ ".dsh-mem-tag {",
379
+ " display: inline-block; padding: 1px 8px; border-radius: 999px;",
380
+ " font-size: 11px; font-weight: 600; line-height: 18px; white-space: nowrap;",
381
+ " color: var(--dsh-mem-tag-c, var(--dsh-mem-text-2));",
382
+ " background: color-mix(in srgb, var(--dsh-mem-tag-c, #8a93a1) 12%, transparent);",
383
+ " border: 1px solid color-mix(in srgb, var(--dsh-mem-tag-c, #8a93a1) 28%, transparent);",
384
+ "}",
385
+ ".dsh-mem-tag-persona { --dsh-mem-tag-c: #6f42c1; }",
386
+ ".dsh-mem-tag-episodic { --dsh-mem-tag-c: #0757b4; }",
387
+ ".dsh-mem-tag-instruction, .dsh-mem-tag-work-artifact { --dsh-mem-tag-c: #8a5a00; }",
388
+ ".dsh-mem-tag-work-fact { --dsh-mem-tag-c: #0757b4; }",
389
+ ".dsh-mem-tag-work-task { --dsh-mem-tag-c: #116629; }",
390
+ ".dsh-mem-tag-work-method { --dsh-mem-tag-c: #6f42c1; }",
391
+ "body[data-ds-dark-theme] .dsh-mem-tag-persona { --dsh-mem-tag-c: #c297ff; }",
392
+ "body[data-ds-dark-theme] .dsh-mem-tag-episodic { --dsh-mem-tag-c: #6cb2ff; }",
393
+ "body[data-ds-dark-theme] .dsh-mem-tag-instruction, body[data-ds-dark-theme] .dsh-mem-tag-work-artifact { --dsh-mem-tag-c: #e3b341; }",
394
+ "body[data-ds-dark-theme] .dsh-mem-tag-work-fact { --dsh-mem-tag-c: #6cb2ff; }",
395
+ "body[data-ds-dark-theme] .dsh-mem-tag-work-task { --dsh-mem-tag-c: #6fca74; }",
396
+ "body[data-ds-dark-theme] .dsh-mem-tag-work-method { --dsh-mem-tag-c: #c297ff; }",
397
+ // ── pill 边缘流光(品牌蓝族,chat/work/auto 三档共用;off 无):border 区画旋转
398
+ // conic 光带;内部必须用【不透明】底色盖住光带——半透明内层会让 conic 透进按钮
399
+ // 内部干扰文字(实测事故)。不透明底 = 主题底混 3% 档位色(--dsh-mem-pill-tint
400
+ // 由 pill inline 给定;3% 保证档位色文字 AA,暗色智能档余量 4.63:1)
401
+ ".dsh-mem-flow {",
402
+ " border: 1px solid transparent;",
403
+ " background:",
404
+ " linear-gradient(",
405
+ " color-mix(in srgb, var(--dsh-mem-bg-card, #ffffff) 97%, var(--dsh-mem-pill-tint, #4d6bfe)),",
406
+ " color-mix(in srgb, var(--dsh-mem-bg-card, #ffffff) 97%, var(--dsh-mem-pill-tint, #4d6bfe))",
407
+ " ) padding-box,",
408
+ " conic-gradient(from var(--dsh-mem-angle),",
409
+ " rgba(61,91,224,0.9), rgba(77,107,254,0.95), rgba(147,168,255,1),",
410
+ " rgba(110,133,255,0.9), rgba(61,91,224,0.9)) border-box;",
411
+ " animation: dshMemFlow 3s linear infinite;",
412
+ "}",
413
+ // ── off 档 pill:dsh 透明按钮——无底无边框只留文字,hover 才出 interactive 淡底
414
+ //(裸 button 会露 UA 默认灰底+描边,必须显式压掉) ──
415
+ ".dsh-mem-pill-off { border: none; background: transparent; }",
416
+ ".dsh-mem-pill-off:hover { background: var(--dsh-mem-bg-hover); }",
417
+ ".dsh-mem-pill-off:focus-visible { outline: 2px solid var(--dsh-mem-accent); outline-offset: 1px; }",
418
+ // 流光态焦点环(同一物理按钮的两态焦点反馈对称,配方同 .dsh-mem-btn)
419
+ ".dsh-mem-flow:focus-visible { outline: 2px solid var(--dsh-mem-accent); outline-offset: 1px; }",
420
+ // ── 触屏隐形热区(手机端适配):触点目标补足 44px 标准(iOS HIG)。
421
+ // 伪元素不参与布局(浮层/输入栏几何零变化),指针事件落在宿主元素上。
422
+ // 全端统一不做 pointer:coarse 分端(桌面点中目标变大是纯收益)。
423
+ // pill:视觉高 24px,::after 上下各外扩 10px;只上下不左右——左右是宿主输入栏
424
+ // 邻位控件(模式选择器),外扩会制造误触重叠带。
425
+ // 滑轨:视觉轨 22px,::before 上下各外扩 11px;touch-action:none 已随轨声明,
426
+ // 伪元素区的触摸同样命中轨元素。两处 px 值改动须与注释口径同步 ──
427
+ ".dsh-mem-pill-hit::after {",
428
+ " content: ''; position: absolute; left: 0; right: 0; top: -10px; bottom: -10px;",
429
+ "}",
430
+ ".dsh-mem-hitband::before {",
431
+ " content: ''; position: absolute; left: 0; right: 0; top: -11px; bottom: -11px;",
432
+ "}",
433
+ // ── 浮层(dsh 原生菜单同配方:不透明实底 + inverted 描边(浅色不可见)+ lv3 阴影) ──
434
+ ".dsh-mem-popover {",
435
+ " border-radius: 12px;",
436
+ " border: 1px solid var(--dsh-mem-border-pop);",
437
+ " background: var(--dsh-mem-bg-pop);",
438
+ " box-shadow: var(--dsh-mem-shadow-pop);",
439
+ " color: var(--dsh-mem-text-1);",
440
+ "}",
441
+ // ── 拖动气泡:拖拽时显示当前档位名,随圆球移动,倒三角尖角贴近圆球 ──
442
+ // 底色走浮层同材质令牌(浅色白底深字 / 暗色深底浅字,随主题翻转;
443
+ // tooltip-bg 令牌在浅色下仍是深色、不随材质走,已弃用)。
444
+ // 悬停 8px(尖角尖端距圆球顶约 5px);气泡 zIndex 4 高于浮层(同层叠上下文内
445
+ // 数值比较),跨过浮层上缘时盖在其上;描边 + 投影避免同材质融合
446
+ ".dsh-mem-bubble {",
447
+ " position: absolute; bottom: calc(100% + 8px); transform: translateX(-50%);",
448
+ " padding: 3px 10px; border-radius: 8px; font-size: 12px; font-weight: 600; line-height: 18px;",
449
+ " border: 1px solid var(--dsh-mem-border);",
450
+ " background: var(--dsh-mem-bg-pop); color: var(--dsh-mem-text-1); white-space: nowrap;",
451
+ " box-shadow: 0 2px 8px rgba(0,0,0,0.18);",
452
+ "}",
453
+ // 尖角:clip-path 倒三角(旋转方块会露出上半截成菱形,实测视觉缺陷)。
454
+ // 双三角叠画:外层描边色大一圈、内层填充色,压在浮层上缘也有轮廓可读
455
+ ".dsh-mem-bubble::before {",
456
+ " content: ''; position: absolute; top: 100%; left: 50%; margin-left: -6px;",
457
+ " width: 12px; height: 7px;",
458
+ " clip-path: polygon(0 0, 100% 0, 50% 100%);",
459
+ " background: var(--dsh-mem-border);",
460
+ "}",
461
+ ".dsh-mem-bubble::after {",
462
+ " content: ''; position: absolute; top: 100%; left: 50%; margin-left: -5px;",
463
+ " width: 10px; height: 6px;",
464
+ " clip-path: polygon(0 0, 100% 0, 50% 100%);",
465
+ " background: var(--dsh-mem-bg-pop);",
466
+ "}",
467
+ // ── 粒子层(点阵场):浅色 multiply 混合——深蓝点乘在浅蓝填充上沉显对比 ──
468
+ "body:not([data-ds-dark-theme]) .dsh-mem-particles { mix-blend-mode: multiply; opacity: 0.82; }",
469
+ // ── 重建面板 ──(模态本体走 NModal:原生 Modal 优先,回退 rb-overlay/rb-modal)
470
+ ".dsh-mem-rb-card {",
471
+ " border: 1px solid var(--dsh-mem-border); border-radius: 10px; background: var(--dsh-mem-bg-card);",
472
+ " box-shadow: var(--dsh-mem-shadow-card); padding: 12px 14px; margin-bottom: 14px; font-size: 13px;",
473
+ "}",
474
+ ".dsh-mem-rb-bar { height: 8px; border-radius: 4px; overflow: hidden; flex: 1; background: var(--dsh-mem-track); }",
475
+ ".dsh-mem-rb-fill { height: 100%; border-radius: 4px; background: var(--dsh-mem-accent-fill); transition: width .4s ease; }",
476
+ ".dsh-mem-rb-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.35);",
477
+ " display: flex; align-items: center; justify-content: center; z-index: 2000; }",
478
+ ".dsh-mem-rb-modal { width: 440px; max-width: calc(100vw - 48px); border-radius: 12px;",
479
+ " padding: 18px 20px; font-size: 13px; line-height: 1.6;",
480
+ " border: 1px solid var(--dsh-mem-border); background: var(--dsh-mem-bg-card); color: var(--dsh-mem-text-1);",
481
+ " box-shadow: 0 16px 48px rgba(0,0,0,0.24); }",
482
+ "body[data-ds-dark-theme] .dsh-mem-rb-modal { box-shadow: 0 16px 48px rgba(0,0,0,0.6); }",
483
+ ".dsh-mem-rb-muted { font-size: 12px; color: var(--dsh-mem-text-3); }",
484
+ // ── 会话信息区(悬浮卡下半部):分隔线 + 2×2 指标 + 状态行;纯静态 DOM,
485
+ // 不进粒子层 rAF 循环,轮询数据到达才触发本组件小树 re-render ──
486
+ ".dsh-mem-sinfo { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--dsh-mem-border); }",
487
+ ".dsh-mem-sinfo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px; }",
488
+ ".dsh-mem-sinfo-val { font-size: 13px; font-weight: 600; color: var(--dsh-mem-text-1); line-height: 18px; font-variant-numeric: tabular-nums; }",
489
+ ".dsh-mem-sinfo-label { font-size: 11px; color: var(--dsh-mem-text-3); line-height: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
490
+ ".dsh-mem-sinfo-warn { font-size: 11px; color: var(--dsh-mem-danger); line-height: 16px; margin-bottom: 6px; }",
491
+ ".dsh-mem-sinfo-note { font-size: 11px; color: var(--dsh-mem-text-3); line-height: 16px; margin-top: 8px; }",
492
+ ".dsh-mem-sinfo-sum { font-size: 11px; color: var(--dsh-mem-text-3); line-height: 16px; margin-top: 8px; }",
493
+ // reduced-motion 兜底放样式表末尾:同特异性下后置声明才能压过上面的组件类
494
+ "@media (prefers-reduced-motion: reduce) {",
495
+ " .dsh-mem-root, .dsh-mem-root *, .dsh-mem-btn, .dsh-mem-input, .dsh-mem-select, .dsh-mem-rb-fill, .dsh-mem-scene-chev, .dsh-mem-sel-chev { transition: none; }",
496
+ " .dsh-mem-flow { animation: none; }",
497
+ "}"
498
+ ].join("\n");
499
+ document.head.appendChild(el);
500
+ }
501
+
502
+ // client/src/tabs/CostTab.tsx
503
+ var import_react2 = require("react");
504
+
505
+ // client/src/ui/controls.tsx
506
+ var import_jsx_runtime = require("react/jsx-runtime");
507
+ function Switch(props) {
508
+ const on = !!props.checked;
509
+ const disabled = !!props.disabled;
510
+ const base = { ...S.switch, ...on ? S.switchOn : S.switchOff, ...disabled ? S.switchDisabled : null };
511
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
512
+ "div",
513
+ {
514
+ style: base,
515
+ onClick: () => {
516
+ if (!disabled && props.onChange) props.onChange(!on);
517
+ },
518
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { ...S.knob, left: on ? 18 : 2 } })
519
+ }
520
+ );
521
+ }
522
+ function SwitchRow(props) {
523
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: S.switchRow, children: [
524
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, { checked: props.checked, disabled: props.disabled, onChange: props.onChange }),
525
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
526
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.switchLabel, children: props.label }),
527
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: S.switchDesc, children: props.desc || "" })
528
+ ] })
529
+ ] });
530
+ }
531
+ function Segmented(props) {
532
+ const value = props.value;
533
+ const disabled = !!props.disabled;
534
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...S.seg, ...disabled ? S.switchDisabled : null }, children: props.options.map((opt, i) => {
535
+ const on = opt.key === value;
536
+ const optDisabled = disabled || !!opt.disabled;
537
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
538
+ "span",
539
+ {
540
+ title: opt.disabledTitle || opt.title || "",
541
+ style: {
542
+ ...S.segBtn,
543
+ ...on ? S.segBtnOn : null,
544
+ ...i === props.options.length - 1 ? { borderRight: "none" } : null,
545
+ ...optDisabled ? { cursor: "not-allowed", opacity: 0.45 } : null
546
+ },
547
+ onClick: () => {
548
+ if (!optDisabled && !on && props.onChange) props.onChange(opt.key);
549
+ },
550
+ children: opt.label
551
+ },
552
+ opt.key
553
+ );
554
+ }) });
555
+ }
556
+
557
+ // client/src/ui/primitives.tsx
558
+ var import_react = require("react");
559
+
560
+ // client/src/env.ts
561
+ function hostRequire(id) {
562
+ return require(id);
563
+ }
564
+
565
+ // client/src/ui/primitives.tsx
566
+ var P = null;
567
+ try {
568
+ P = hostRequire("@deepseek-ai/dsh-client-ui-primitives");
569
+ } catch {
570
+ P = null;
571
+ }
572
+ function NButton(props) {
573
+ if (P && P.Button) return (0, import_react.createElement)(P.Button, { size: "sm", ...props });
574
+ const rest = { ...props };
575
+ delete rest.variant;
576
+ delete rest.icon;
577
+ rest.className = "dsh-mem-btn" + (rest.className ? " " + rest.className : "");
578
+ return (0, import_react.createElement)("button", rest);
579
+ }
580
+ function NInput(props) {
581
+ if (P && P.Input) {
582
+ const inner = { ...props };
583
+ const layoutStyle = inner.style;
584
+ delete inner.style;
585
+ return (0, import_react.createElement)("span", { style: layoutStyle }, (0, import_react.createElement)(P.Input, inner));
586
+ }
587
+ const rest = { ...props };
588
+ rest.className = "dsh-mem-input" + (rest.className ? " " + rest.className : "");
589
+ return (0, import_react.createElement)("input", rest);
590
+ }
591
+ function NModal(props) {
592
+ if (props.open === false) return null;
593
+ if (P && P.Modal) return (0, import_react.createElement)(P.Modal, { closeLabel: "关闭", ...props });
594
+ return (0, import_react.createElement)(
595
+ "div",
596
+ {
597
+ className: "dsh-mem-rb-overlay",
598
+ onClick: (e) => {
599
+ if (e.target === e.currentTarget && props.onClose) props.onClose();
600
+ }
601
+ },
602
+ (0, import_react.createElement)(
603
+ "div",
604
+ { className: "dsh-mem-rb-modal" },
605
+ props.title ? (0, import_react.createElement)("div", { style: { fontSize: 15, fontWeight: 600, marginBottom: 10 } }, props.title) : null,
606
+ props.children,
607
+ props.footer ? (0, import_react.createElement)(
608
+ "div",
609
+ { style: { display: "flex", justifyContent: "flex-end", gap: 8, marginTop: 16 } },
610
+ props.footer
611
+ ) : null
612
+ )
613
+ );
614
+ }
615
+
616
+ // client/src/tabs/CostTab.tsx
617
+ var import_jsx_runtime2 = require("react/jsx-runtime");
618
+ function renderCostChart(buckets, models, maxY, fmtDate, fmtInt, palette) {
619
+ const W = 600;
620
+ const H = 200;
621
+ const L = 46;
622
+ const R = 10;
623
+ const T = 10;
624
+ const B = 26;
625
+ const iw = W - L - R;
626
+ const ih = H - T - B;
627
+ const n = buckets.length;
628
+ const x = (i) => L + (n <= 1 ? iw / 2 : i / (n - 1) * iw);
629
+ const y = (v) => T + ih - v / maxY * ih;
630
+ const yTicks = [0, maxY / 2, maxY];
631
+ const xIdx = n > 2 ? [0, Math.floor((n - 1) / 2), n - 1] : n === 2 ? [0, 1] : [0];
632
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { viewBox: "0 0 " + W + " " + H, style: { width: "100%", height: "auto", display: "block" }, children: [
633
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: L, y1: y(0), x2: W - R, y2: y(0), stroke: "var(--dsh-mem-border)", strokeWidth: 1 }),
634
+ yTicks.map((v) => {
635
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: L - 6, y: y(v) + 4, textAnchor: "end", fontSize: 10, fill: "var(--dsh-mem-text-3)", children: fmtInt(v) }, "yt" + v);
636
+ }),
637
+ xIdx.map((i) => {
638
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: x(i), y: H - 8, textAnchor: "middle", fontSize: 10, fill: "var(--dsh-mem-text-3)", children: fmtDate(buckets[i] ? buckets[i].ts : 0) }, "xt" + i);
639
+ }),
640
+ models.map((m, mi) => {
641
+ const pts = buckets.map((b, i) => x(i) + "," + y(b.byModel[m] || 0)).join(" ");
642
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
643
+ "polyline",
644
+ {
645
+ points: pts,
646
+ fill: "none",
647
+ stroke: palette[mi % palette.length],
648
+ strokeWidth: 2,
649
+ strokeLinejoin: "round",
650
+ strokeLinecap: "round"
651
+ },
652
+ "pl" + m
653
+ );
654
+ })
655
+ ] });
656
+ }
657
+ var RANGE_LABELS = { day: "今日", week: "本周", month: "本月", all: "累计" };
658
+ var LAYER_OPTS = [
659
+ { key: "", label: "全部" },
660
+ { key: "l1", label: "L1" },
661
+ { key: "l2", label: "L2" },
662
+ { key: "l3", label: "L3" }
663
+ ];
664
+ var GRAN_OPTS = [
665
+ { key: "day", label: "日" },
666
+ { key: "week", label: "周" },
667
+ { key: "month", label: "月" }
668
+ ];
669
+ var PALETTE = [
670
+ "var(--dsh-mem-chart-1)",
671
+ "var(--dsh-mem-chart-2)",
672
+ "var(--dsh-mem-chart-3)",
673
+ "var(--dsh-mem-chart-4)",
674
+ "var(--dsh-mem-chart-5)",
675
+ "var(--dsh-mem-chart-6)",
676
+ "var(--dsh-mem-chart-7)",
677
+ "var(--dsh-mem-chart-8)"
678
+ ];
679
+ var RANGES = ["day", "week", "month", "all"];
680
+ var thFirst = { fontSize: 12, fontWeight: 600, color: "var(--dsh-mem-text-3)", textAlign: "left", padding: "4px 10px", borderBottom: "1px solid var(--dsh-mem-border)" };
681
+ var thStyle = { fontSize: 12, fontWeight: 600, color: "var(--dsh-mem-text-3)", textAlign: "right", padding: "4px 10px", borderBottom: "1px solid var(--dsh-mem-border)" };
682
+ var tdFirst = { fontSize: 12.5, fontWeight: 600, color: "var(--dsh-mem-text-1)", textAlign: "left", padding: "4px 10px" };
683
+ var tdStyle = { fontSize: 12.5, color: "var(--dsh-mem-text-1)", textAlign: "right", padding: "4px 10px", fontFamily: "ui-monospace, Consolas, monospace" };
684
+ function CostTab(props) {
685
+ const rpc = props.rpc;
686
+ const [data, setData] = (0, import_react2.useState)(null);
687
+ const [error, setError] = (0, import_react2.useState)(null);
688
+ const [granularity, setGranularity] = (0, import_react2.useState)("day");
689
+ const [layer, setLayer] = (0, import_react2.useState)("");
690
+ const [rangeDays, setRangeDays] = (0, import_react2.useState)(0);
691
+ const [rangeOpen, setRangeOpen] = (0, import_react2.useState)(false);
692
+ const load = (0, import_react2.useCallback)(() => {
693
+ setError(null);
694
+ rpc("dsh-memory/token-cost", {
695
+ granularity,
696
+ rangeDays
697
+ }).then((r) => {
698
+ if (r && r.ok) setData(r.value);
699
+ else setError(r && r.error ? r.error.message : "RPC error");
700
+ }).catch((e) => {
701
+ setError(String(e && e.message || e));
702
+ });
703
+ }, [rpc, granularity, rangeDays]);
704
+ (0, import_react2.useEffect)(() => {
705
+ load();
706
+ const timer = setInterval(load, 5e3);
707
+ return () => {
708
+ clearInterval(timer);
709
+ };
710
+ }, [load]);
711
+ const fmtInt = (n) => String(Math.round(n)).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
712
+ const fmtModel = (p, m) => p ? p + "/" + m : m;
713
+ const fmtDate = (ts) => {
714
+ try {
715
+ const d = new Date(ts);
716
+ const g = data && data.trend ? data.trend.granularity : granularity;
717
+ if (g === "month") return d.getMonth() + 1 + "月";
718
+ return d.getMonth() + 1 + "/" + d.getDate();
719
+ } catch {
720
+ return "";
721
+ }
722
+ };
723
+ const windows = data && data.windows || [];
724
+ const byModel = data && data.byModel || [];
725
+ const byLayer = data && data.byLayer || [];
726
+ const trend = data && data.trend ? data.trend : null;
727
+ let buckets = [];
728
+ if (trend && trend.byLayer) {
729
+ if (layer === "l1" || layer === "l2" || layer === "l3") {
730
+ buckets = trend.byLayer[layer] || [];
731
+ } else {
732
+ const seqs = [trend.byLayer.l1 || [], trend.byLayer.l2 || [], trend.byLayer.l3 || []];
733
+ const n = seqs[0].length;
734
+ for (let i = 0; i < n; i++) {
735
+ const merged = { ts: 0, total: 0, byModel: {} };
736
+ for (let s = 0; s < seqs.length; s++) {
737
+ const seq = seqs[s];
738
+ if (seq && seq[i]) {
739
+ if (merged.ts === 0) merged.ts = seq[i].ts;
740
+ merged.total += seq[i].total;
741
+ Object.keys(seq[i].byModel).forEach((m) => {
742
+ merged.byModel[m] = (merged.byModel[m] || 0) + seq[i].byModel[m];
743
+ });
744
+ }
745
+ }
746
+ buckets.push(merged);
747
+ }
748
+ }
749
+ }
750
+ const models = [];
751
+ const seen = {};
752
+ buckets.forEach((b) => {
753
+ Object.keys(b.byModel).forEach((m) => {
754
+ if (!seen[m]) {
755
+ seen[m] = true;
756
+ models.push(m);
757
+ }
758
+ });
759
+ });
760
+ models.sort();
761
+ let maxY = 1;
762
+ buckets.forEach((b) => {
763
+ if (b.total > maxY) maxY = b.total;
764
+ models.forEach((m) => {
765
+ if ((b.byModel[m] || 0) > maxY) maxY = b.byModel[m];
766
+ });
767
+ });
768
+ const layerTable = byLayer.map((lc) => {
769
+ const win = {};
770
+ lc.windows.forEach((w) => {
771
+ win[w.range] = w;
772
+ });
773
+ return { layer: lc.layer, win };
774
+ });
775
+ const cell = (lc, r, pick) => {
776
+ const w = lc.win[r];
777
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: tdStyle, children: w ? fmtInt(pick(w)) : "0" }, r);
778
+ };
779
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
780
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
781
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Segmented, { value: layer, options: LAYER_OPTS, onChange: setLayer }),
782
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Segmented, { value: granularity, options: GRAN_OPTS, onChange: setGranularity }),
783
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
784
+ NButton,
785
+ {
786
+ onClick: () => {
787
+ setRangeOpen(!rangeOpen);
788
+ },
789
+ children: rangeDays > 0 ? "近 " + rangeDays + " 天" : "近N天"
790
+ }
791
+ ),
792
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.grow }),
793
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(NButton, { onClick: load, children: "刷新" })
794
+ ] }),
795
+ rangeOpen ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
796
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: S.muted, children: "展示近 N 天(正整数,清空=默认窗口;超出保留期后端自动回退)" }),
797
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
798
+ NInput,
799
+ {
800
+ value: rangeDays === 0 ? "" : String(rangeDays),
801
+ placeholder: "如 30",
802
+ style: { width: 90 },
803
+ onChange: (e) => {
804
+ const v = String(e.target.value || "").trim();
805
+ if (v === "") {
806
+ setRangeDays(0);
807
+ return;
808
+ }
809
+ const n = Number(v);
810
+ if (Number.isInteger(n) && n > 0 && n <= 3650) setRangeDays(n);
811
+ }
812
+ }
813
+ )
814
+ ] }) : null,
815
+ error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.error, children: "成本读取失败:" + error }) : null,
816
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.panelLabel, children: "成本趋势(按模型)" }),
817
+ buckets.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
818
+ renderCostChart(buckets, models, maxY, fmtDate, fmtInt, PALETTE),
819
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", flexWrap: "wrap", gap: "4px 12px", margin: "6px 0 14px" }, children: models.map((m, mi) => {
820
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
821
+ "span",
822
+ {
823
+ style: { display: "inline-flex", alignItems: "center", gap: 4, fontSize: 12, color: "var(--dsh-mem-text-2)" },
824
+ children: [
825
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: { width: 10, height: 10, borderRadius: 4, background: PALETTE[mi % PALETTE.length], display: "inline-block" } }),
826
+ m
827
+ ]
828
+ },
829
+ "lg" + m
830
+ );
831
+ }) })
832
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: S.muted, children: data ? "暂无成本数据(触发一次蒸馏后这里会出现趋势)。" : "加载中…" }),
833
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.panelLabel, children: "层级成本(输出 token)" }),
834
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: { width: "100%", borderCollapse: "collapse", marginBottom: 14 }, children: [
835
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
836
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: thFirst, children: "层级" }),
837
+ RANGES.map((r) => {
838
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: thStyle, children: RANGE_LABELS[r] }, r);
839
+ })
840
+ ] }) }),
841
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: layerTable.map((lc) => {
842
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
843
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: tdFirst, children: lc.layer.toUpperCase() }),
844
+ RANGES.map((r) => cell(lc, r, (w) => w.outputTokens))
845
+ ] }, lc.layer);
846
+ }) })
847
+ ] }),
848
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.panelLabel, children: "层级成本(单次 avg)" }),
849
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: { width: "100%", borderCollapse: "collapse", marginBottom: 14 }, children: [
850
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
851
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: thFirst, children: "层级" }),
852
+ RANGES.map((r) => {
853
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: thStyle, children: RANGE_LABELS[r] + "-avg" }, r);
854
+ })
855
+ ] }) }),
856
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: layerTable.map((lc) => {
857
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
858
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: tdFirst, children: lc.layer.toUpperCase() }),
859
+ RANGES.map((r) => cell(lc, r, (w) => w.avgOutputTokens))
860
+ ] }, lc.layer);
861
+ }) })
862
+ ] }),
863
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.panelLabel, children: "层级成本(单次 median)" }),
864
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { style: { width: "100%", borderCollapse: "collapse", marginBottom: 14 }, children: [
865
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
866
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: thFirst, children: "层级" }),
867
+ RANGES.map((r) => {
868
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("th", { style: thStyle, children: RANGE_LABELS[r] + "-median" }, r);
869
+ })
870
+ ] }) }),
871
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: layerTable.map((lc) => {
872
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
873
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("td", { style: tdFirst, children: lc.layer.toUpperCase() }),
874
+ RANGES.map((r) => cell(lc, r, (w) => w.medianOutputTokens))
875
+ ] }, lc.layer);
876
+ }) })
877
+ ] }),
878
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.panelLabel, children: "时间窗口总览" }),
879
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.statGrid, children: windows.map((w) => {
880
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-mem-card", style: S.statTile, children: [
881
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.statNum, children: fmtInt(w.outputTokens + w.reasoningTokens) }),
882
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.statLabel, children: RANGE_LABELS[w.range] + " · 总输出 token" }),
883
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.muted, children: "文字 " + fmtInt(w.outputTokens) + " · 思考 " + fmtInt(w.reasoningTokens) + " · " + w.calls + " 次调用" })
884
+ ] }, w.range);
885
+ }) }),
886
+ byModel.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
887
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: S.panelLabel, children: "按模型(累计)" }),
888
+ byModel.map((m) => {
889
+ const label = fmtModel(m.provider, m.model);
890
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: S.infoRow, children: [
891
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: S.infoKey, children: label }),
892
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: S.infoVal, children: m.calls + " 次 · 输出 " + fmtInt(m.outputTokens) + " · 思考 " + fmtInt(m.reasoningTokens) })
893
+ ] }, "m-" + label);
894
+ })
895
+ ] }) : null,
896
+ data ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: S.hint, children: "输入按字符、输出/思考按 token 计;趋势图 Y 轴为输出 token,上方可切换层级与颗粒度。" }) : null
897
+ ] });
898
+ }
899
+
900
+ // client/src/tabs/LogTab.tsx
901
+ var import_react3 = require("react");
902
+ var import_jsx_runtime3 = require("react/jsx-runtime");
903
+ function LogTab(props) {
904
+ const rpc = props.rpc;
905
+ const [lines, setLines] = (0, import_react3.useState)(null);
906
+ const [error, setError] = (0, import_react3.useState)(null);
907
+ const preRef = (0, import_react3.useRef)(null);
908
+ const load = (0, import_react3.useCallback)(() => {
909
+ setError(null);
910
+ rpc("dsh-memory/log-tail", { lines: 200 }).then((r) => {
911
+ if (r && r.ok) setLines(r.value.lines);
912
+ else setError(r && r.error ? r.error.message : "RPC error");
913
+ }).catch((e) => {
914
+ setError(String(e && e.message || e));
915
+ });
916
+ }, [rpc]);
917
+ (0, import_react3.useEffect)(() => {
918
+ load();
919
+ }, [load]);
920
+ (0, import_react3.useEffect)(() => {
921
+ if (lines && preRef.current) preRef.current.scrollTop = preRef.current.scrollHeight;
922
+ }, [lines]);
923
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
924
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
925
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: S.muted, children: error ? "加载失败" : lines === null ? "加载中…" : "最近 " + lines.length + " 行(memory.log)" }),
926
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: S.grow }),
927
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(NButton, { onClick: load, children: "刷新" })
928
+ ] }),
929
+ error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { ...S.error, marginBottom: 10 }, children: "日志读取失败:" + error + "(点右上“刷新”重试)" }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("pre", { style: S.pre, ref: preRef, children: (lines || []).join("\n") || "(暂无日志)" })
930
+ ] });
931
+ }
932
+
933
+ // client/src/tabs/OverviewTab.tsx
934
+ var import_react12 = require("react");
935
+
936
+ // client/src/format.ts
937
+ var TYPE_LABELS = {
938
+ persona: "画像偏好",
939
+ episodic: "客观事件",
940
+ instruction: "全局指令",
941
+ work_fact: "工作事实",
942
+ work_task: "工作任务",
943
+ work_method: "工作方法",
944
+ work_artifact: "工作资产"
945
+ };
946
+ function fmtTime(iso) {
947
+ if (!iso) return "-";
948
+ try {
949
+ return new Date(iso).toLocaleString();
950
+ } catch {
951
+ return String(iso);
952
+ }
953
+ }
954
+ function fmtAgo(iso) {
955
+ if (!iso) return null;
956
+ try {
957
+ const t = new Date(iso).getTime();
958
+ if (!t) return null;
959
+ let s = Math.floor((Date.now() - t) / 1e3);
960
+ if (s < 0) s = 0;
961
+ if (s < 45) return "刚刚";
962
+ if (s < 3600) return Math.floor(s / 60) + " 分钟前";
963
+ if (s < 86400) return Math.floor(s / 3600) + " 小时前";
964
+ return Math.floor(s / 86400) + " 天前";
965
+ } catch {
966
+ return null;
967
+ }
968
+ }
969
+ function fmtMB(bytes) {
970
+ if (!bytes || bytes <= 0) return "0MB";
971
+ if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + "KB";
972
+ return (bytes / (1024 * 1024)).toFixed(bytes < 100 * 1024 * 1024 ? 1 : 0) + "MB";
973
+ }
974
+
975
+ // client/src/pill/modes.ts
976
+ var MODES = [
977
+ { key: "off", label: "关闭", color: "var(--dsh-mem-text-2)" },
978
+ { key: "chat", label: "日常", color: "var(--dsh-mem-mode-chat)" },
979
+ { key: "work", label: "工作", color: "var(--dsh-mem-mode-work)" },
980
+ { key: "auto", label: "智能", color: "var(--dsh-mem-mode-auto)" }
981
+ ];
982
+ var TRACK_W = 200;
983
+ var THUMB = 16;
984
+ var RAIL_H = 22;
985
+ var INNER_W = TRACK_W - THUMB;
986
+ var FIELD_TIERS = [
987
+ { density: 0, alpha: 0, wave: 0, tempo: 1 },
988
+ { density: 0.34, alpha: 0.5, wave: 0, tempo: 1 },
989
+ // 日常:稀疏微光
990
+ { density: 0.55, alpha: 0.78, wave: 1, tempo: 1.15 },
991
+ // 工作:中强 + 水波纹
992
+ { density: 0.72, alpha: 1, wave: 1, tempo: 1.3 }
993
+ // 智能:满场最活跃
994
+ ];
995
+ function smStep(a, b, x) {
996
+ const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
997
+ return t * t * (3 - 2 * t);
998
+ }
999
+ function modeInfo(key) {
1000
+ for (let i = 0; i < MODES.length; i++) if (MODES[i].key === key) return MODES[i];
1001
+ return MODES[3];
1002
+ }
1003
+ function modeLabel(key) {
1004
+ if (key === "auto") return "智能(双族)";
1005
+ if (key === "chat") return "日常(个人)";
1006
+ if (key === "work") return "工作(团队)";
1007
+ return "关闭";
1008
+ }
1009
+ function modeIndex(key) {
1010
+ for (let i = 0; i < MODES.length; i++) if (MODES[i].key === key) return i;
1011
+ return 3;
1012
+ }
1013
+
1014
+ // client/src/tabs/DistillSettings.tsx
1015
+ var import_react8 = require("react");
1016
+
1017
+ // client/src/tabs/BudgetInputs.tsx
1018
+ var import_react4 = require("react");
1019
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1020
+ var LAYERS = [
1021
+ ["extract", "抽取"],
1022
+ ["dedup", "去重"],
1023
+ ["l2", "L2 场景"],
1024
+ ["l3", "L3 画像"],
1025
+ ["graph", "图谱投影"]
1026
+ ];
1027
+ var SCOPE_KEYS = {
1028
+ l1: ["extract", "dedup"],
1029
+ l2: ["l2"],
1030
+ l3: ["l3"]
1031
+ };
1032
+ function BudgetInputs(props) {
1033
+ const rpc = props.rpc;
1034
+ const disabled = !!props.disabled;
1035
+ const data = props.data;
1036
+ const setData = props.setData;
1037
+ const onError = props.onError;
1038
+ const scope = props.scope ?? "all";
1039
+ const layers = scope === "all" || scope === "input" ? LAYERS : LAYERS.filter((l) => SCOPE_KEYS[scope].includes(l[0]));
1040
+ const [draft, setDraft] = (0, import_react4.useState)(null);
1041
+ if (!data || !data.budgets) return null;
1042
+ const cur = data.budgets.current || {};
1043
+ const def = data.budgets.defaults || {};
1044
+ const eff = data.budgets.effective || {};
1045
+ const ib = data.inputBudget || { current: 0, fallback: 0, effective: 0 };
1046
+ const curIn = ib.current || 0;
1047
+ const effIn = ib.effective || ib.fallback || 0;
1048
+ const shown = (key) => {
1049
+ if (draft && draft[key] !== void 0) return draft[key];
1050
+ const c = key === "input" ? curIn : cur[key] || 0;
1051
+ return c > 0 ? String(c) : "";
1052
+ };
1053
+ const commitPart = (keys, buildPayload, applyView) => {
1054
+ if (!draft) return;
1055
+ const values = {};
1056
+ for (let i = 0; i < keys.length; i++) {
1057
+ const key = keys[i];
1058
+ const raw = (draft[key] !== void 0 ? draft[key] : shown(key)).trim();
1059
+ const n = raw === "" ? 0 : Number(raw);
1060
+ const max = key === "input" ? 1e6 : 1e6;
1061
+ const min = key === "input" ? raw === "" ? 0 : 1e3 : 0;
1062
+ if (!Number.isInteger(n) || n < min || n > max) {
1063
+ onError(
1064
+ key === "input" ? "输入预算须为 0 或 1000~1000000 的整数(留空或 0 = 跟随配置)" : "输出预算须为 0~1000000 的整数(留空或 0 = 跟随默认)"
1065
+ );
1066
+ setDraft(null);
1067
+ return;
1068
+ }
1069
+ values[key] = n;
1070
+ }
1071
+ setDraft(null);
1072
+ const prev = data;
1073
+ setData(applyView(prev, values));
1074
+ rpc("dsh-memory/settings-set", buildPayload(values)).then((r) => {
1075
+ if (!r || !r.ok) {
1076
+ setData(prev);
1077
+ onError(r && r.error ? "预算写入失败:" + r.error.message : "预算写入失败");
1078
+ } else {
1079
+ onError(null);
1080
+ }
1081
+ }).catch((e) => {
1082
+ setData(prev);
1083
+ onError("预算写入失败:" + String(e && e.message || e));
1084
+ });
1085
+ };
1086
+ const commitOutputs = () => {
1087
+ commitPart(
1088
+ ["extract", "dedup", "l2", "l3"],
1089
+ (v) => ({ distillBudgets: v }),
1090
+ (prev, v) => {
1091
+ const effNext = {};
1092
+ for (let j = 0; j < layers.length; j++) {
1093
+ const k = layers[j][0];
1094
+ effNext[k] = v[k] > 0 ? v[k] : def[k] || 0;
1095
+ }
1096
+ return {
1097
+ ...prev,
1098
+ settings: { ...prev.settings, distillBudgets: v },
1099
+ budgets: {
1100
+ ...prev.budgets,
1101
+ current: v,
1102
+ effective: effNext
1103
+ }
1104
+ };
1105
+ }
1106
+ );
1107
+ };
1108
+ const commitInput = () => {
1109
+ commitPart(
1110
+ ["input"],
1111
+ (v) => ({ distillMaxInputChars: v.input }),
1112
+ (prev, v) => {
1113
+ return {
1114
+ ...prev,
1115
+ settings: { ...prev.settings, distillMaxInputChars: v.input },
1116
+ inputBudget: {
1117
+ ...prev.inputBudget || { current: 0, fallback: 0, effective: 0 },
1118
+ current: v.input,
1119
+ effective: v.input > 0 ? v.input : ib.fallback || 0
1120
+ }
1121
+ };
1122
+ }
1123
+ );
1124
+ };
1125
+ const dirty = (keys) => {
1126
+ if (!draft) return false;
1127
+ for (let i = 0; i < keys.length; i++) {
1128
+ const key = keys[i];
1129
+ const want = (draft[key] !== void 0 ? draft[key] : "").trim();
1130
+ const was = key === "input" ? curIn > 0 ? String(curIn) : "" : (cur[key] || 0) > 0 ? String(cur[key]) : "";
1131
+ if (want !== was) return true;
1132
+ }
1133
+ return false;
1134
+ };
1135
+ const inputBox = (key, _label, title, width, placeholder, onCommit) => {
1136
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1137
+ NInput,
1138
+ {
1139
+ type: "number",
1140
+ min: 0,
1141
+ max: 1e6,
1142
+ style: { width },
1143
+ title,
1144
+ placeholder: String(placeholder || ""),
1145
+ value: shown(key),
1146
+ disabled,
1147
+ onChange: (e) => {
1148
+ const v = e.target.value;
1149
+ const d = { ...draft || {} };
1150
+ d[key] = v;
1151
+ setDraft(d);
1152
+ },
1153
+ onBlur: () => {
1154
+ if (dirty([key])) onCommit();
1155
+ },
1156
+ onKeyDown: (e) => {
1157
+ if (e.key === "Enter" && dirty([key])) onCommit();
1158
+ }
1159
+ },
1160
+ key
1161
+ );
1162
+ };
1163
+ const showOutputs = scope !== "input";
1164
+ const showInput = scope === "all" || scope === "input";
1165
+ const effNote = layers.map((l) => eff[l[0]] || "?").join(" / ");
1166
+ const rowLabel = (key) => key === "extract" ? "抽取输出" : key === "dedup" ? "去重输出" : key === "l2" ? "L2 输出" : "L3 输出";
1167
+ const rowStyle = { display: "flex", alignItems: "center", gap: 8 };
1168
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { children: [
1169
+ showOutputs ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginTop: 12 }, children: [
1170
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1171
+ "div",
1172
+ {
1173
+ style: S.switchLabel,
1174
+ title: (scope === "l1" ? "L1 的抽取与去重是两次独立调用,输出限额各自设置(路由共用同一条链)。" : "") + "留空或 0 = 跟随默认(当前生效 " + effNote + ");思考档 high/xhigh/max 时实际限额自动 ×4",
1175
+ children: "输出预算"
1176
+ }
1177
+ ),
1178
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 4 }, children: layers.map((l) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: rowStyle, children: [
1179
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { width: 68, flexShrink: 0, fontSize: 12, color: "var(--dsh-mem-text-2)" }, children: rowLabel(l[0]) }),
1180
+ inputBox(
1181
+ l[0],
1182
+ l[1],
1183
+ l[1] + " 输出预算(token,留空 = 默认 " + (def[l[0]] || "?") + ")",
1184
+ 110,
1185
+ def[l[0]],
1186
+ commitOutputs
1187
+ ),
1188
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: "token" })
1189
+ ] }, l[0])) })
1190
+ ] }) : null,
1191
+ showInput ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { marginTop: 12 }, children: [
1192
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1193
+ "div",
1194
+ {
1195
+ style: S.switchLabel,
1196
+ title: "单次蒸馏调用的输入字符上限(≈token):L1 抽取按此分块、L2/L3 超限截断;留空或 0 = 跟随配置(当前生效 " + (effIn || "?") + ",来自 llm.maxInputChars)",
1197
+ children: "输入预算"
1198
+ }
1199
+ ),
1200
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 4 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: rowStyle, children: [
1201
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { width: 68, flexShrink: 0, fontSize: 12, color: "var(--dsh-mem-text-2)" }, children: "单次输入" }),
1202
+ inputBox(
1203
+ "input",
1204
+ "输入",
1205
+ "单次蒸馏输入字符上限(≈token,中文 1 字≈1 token;留空 = 跟随配置 " + (ib.fallback || "?") + ")",
1206
+ 110,
1207
+ ib.fallback,
1208
+ commitInput
1209
+ ),
1210
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: "字符" })
1211
+ ] }) })
1212
+ ] }) : null
1213
+ ] });
1214
+ }
1215
+
1216
+ // client/src/tabs/ChannelEditor.tsx
1217
+ var import_react5 = require("react");
1218
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1219
+ var STY = {
1220
+ block: { marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--dsh-mem-border)" },
1221
+ head: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginBottom: 6 },
1222
+ title: { fontSize: 13, fontWeight: 650, color: "var(--dsh-mem-text-1)" },
1223
+ chip: {
1224
+ display: "inline-flex",
1225
+ alignItems: "center",
1226
+ borderRadius: 999,
1227
+ padding: "1px 8px",
1228
+ fontSize: 11,
1229
+ fontWeight: 600,
1230
+ lineHeight: "18px",
1231
+ whiteSpace: "nowrap"
1232
+ },
1233
+ chipAccent: { background: "var(--dsh-mem-accent-weak)", color: "var(--dsh-mem-accent-text)" },
1234
+ chipMuted: { background: "var(--dsh-mem-bg-inset)", color: "var(--dsh-mem-text-2)" },
1235
+ desc: { fontSize: 11, color: "var(--dsh-mem-text-3)", margin: "0 0 8px", lineHeight: 1.5 },
1236
+ fieldRow: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginTop: 8 },
1237
+ fieldLabel: { fontSize: 11, color: "var(--dsh-mem-text-3)", flexShrink: 0, width: 64 },
1238
+ input: { flex: "1 1 220px", minWidth: 160 },
1239
+ warn: { fontSize: 11, color: "var(--dsh-mem-danger)", marginTop: 6 },
1240
+ preview: { fontSize: 12, color: "var(--dsh-mem-text-2)", marginTop: 8, wordBreak: "break-all", fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace" }
1241
+ };
1242
+ var MODE_OPTIONS = [
1243
+ { key: "", label: "跟随部署配置", title: "没有运行时覆盖,用 cordis.patch.yml 的 llm.mode/baseURL(默认 host)" },
1244
+ { key: "host", label: "复用宿主", title: "复用宿主 ctx.llm(与付费供应商同路)" },
1245
+ { key: "direct", label: "直连端点", title: "插件原生 HTTP 直连 OpenAI 兼容端点,与付费 API 解耦" }
1246
+ ];
1247
+ function ChannelEditor(props) {
1248
+ const { channel, rpc, disabled } = props;
1249
+ const [baseText, setBaseText] = (0, import_react5.useState)(channel.runtimeBaseURL);
1250
+ const [keyText, setKeyText] = (0, import_react5.useState)("");
1251
+ const baseTimer = (0, import_react5.useRef)(void 0);
1252
+ const dirty = (0, import_react5.useRef)(false);
1253
+ (0, import_react5.useEffect)(() => {
1254
+ if (!dirty.current) setBaseText(channel.runtimeBaseURL);
1255
+ }, [channel.runtimeBaseURL]);
1256
+ (0, import_react5.useEffect)(() => () => clearTimeout(baseTimer.current), []);
1257
+ const commit = (patch) => {
1258
+ rpc("dsh-memory/settings-set", patch).catch(() => {
1259
+ });
1260
+ };
1261
+ const onBaseChange = (v) => {
1262
+ dirty.current = true;
1263
+ setBaseText(v);
1264
+ clearTimeout(baseTimer.current);
1265
+ baseTimer.current = setTimeout(() => {
1266
+ dirty.current = false;
1267
+ commit({ directBaseURL: v.trim() });
1268
+ }, 600);
1269
+ };
1270
+ const commitKey = () => {
1271
+ const v = keyText.trim();
1272
+ if (v) {
1273
+ dirty.current = true;
1274
+ commit({ directApiKey: v });
1275
+ dirty.current = false;
1276
+ }
1277
+ setKeyText("");
1278
+ };
1279
+ const runtime = channel.runtime;
1280
+ const editing = runtime === "direct";
1281
+ const isDirect = channel.effective === "direct";
1282
+ const deployedDirect = channel.deployed === "direct";
1283
+ const apiKeySet = channel.runtimeApiKeySet || channel.deployedApiKeySet;
1284
+ const showWarn = isDirect && !channel.directReady;
1285
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: STY.block, children: [
1286
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: STY.head, children: [
1287
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: STY.title, children: "蒸馏通道" }),
1288
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: isDirect ? STY.chipAccent : STY.chipMuted, children: isDirect ? "直连端点" : "复用宿主" }),
1289
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { title: "direct = 插件原生直连 OpenAI 兼容端点,与付费 API 解耦;失败自动回退宿主路由链。", style: { marginLeft: "auto", fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: [
1290
+ "生效:",
1291
+ runtime ? runtime === "direct" ? "运行时直连" : "运行时宿主" : "跟随部署配置"
1292
+ ] })
1293
+ ] }),
1294
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: STY.desc, children: "direct 通道走插件原生 HTTP,不依赖宿主 provider 注册表;direct 失败自动回退宿主路由链作兜底安全网。" }),
1295
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1296
+ Segmented,
1297
+ {
1298
+ value: runtime,
1299
+ disabled,
1300
+ options: MODE_OPTIONS.map((m) => ({ key: m.key, label: m.label, title: m.title })),
1301
+ onChange: (k) => commit({ distillMode: k })
1302
+ }
1303
+ ),
1304
+ editing ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
1305
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: STY.fieldRow, children: [
1306
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: STY.fieldLabel, children: "端点 URL" }),
1307
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1308
+ NInput,
1309
+ {
1310
+ style: STY.input,
1311
+ placeholder: "如 http://127.0.0.1:11434/v1 或 https://api.xxx/v1",
1312
+ value: baseText,
1313
+ disabled,
1314
+ onChange: (e) => onBaseChange(e.target.value),
1315
+ onKeyDown: (e) => {
1316
+ if (e.key === "Enter") {
1317
+ clearTimeout(baseTimer.current);
1318
+ dirty.current = false;
1319
+ commit({ directBaseURL: baseText.trim() });
1320
+ }
1321
+ }
1322
+ }
1323
+ ),
1324
+ deployedDirect && !channel.runtimeBaseURL ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: [
1325
+ "部署基线:",
1326
+ channel.deployedBaseURL || "(空)"
1327
+ ] }) : null
1328
+ ] }),
1329
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: STY.fieldRow, children: [
1330
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: STY.fieldLabel, children: "API Key" }),
1331
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1332
+ NInput,
1333
+ {
1334
+ style: STY.input,
1335
+ type: "password",
1336
+ placeholder: apiKeySet ? "••••••••(已配置;留空提交可覆盖)" : "本地免 key 可留空",
1337
+ value: keyText,
1338
+ disabled,
1339
+ autoComplete: "new-password",
1340
+ onChange: (e) => setKeyText(e.target.value),
1341
+ onKeyDown: (e) => {
1342
+ if (e.key === "Enter") commitKey();
1343
+ },
1344
+ onBlur: commitKey
1345
+ }
1346
+ ),
1347
+ apiKeySet ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: STY.chipAccent, children: "已配置" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: STY.chipMuted, children: "未配置" }),
1348
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1349
+ NButton,
1350
+ {
1351
+ disabled,
1352
+ title: "写入空串清除运行时密钥(部署 .yml 的密钥不受影响)",
1353
+ onClick: () => commit({ directApiKey: "" }),
1354
+ children: "清除"
1355
+ }
1356
+ )
1357
+ ] }),
1358
+ showWarn ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: STY.warn, children: "⚠ direct 未配置完整:需同时有端点 URL 与模型(llm.model / 全局链主路由),否则会回退宿主路由。" }) : null
1359
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: STY.preview, children: isDirect ? "当前将直连端点:" + (channel.runtimeBaseURL || channel.deployedBaseURL || "(未配置端点)") + (apiKeySet ? " · 密钥已配置" : " · 未配置密钥") : "当前复用宿主 ctx.llm(" + (runtime === "host" ? "运行时锁定" : "跟随部署配置") + ")" })
1360
+ ] });
1361
+ }
1362
+
1363
+ // client/src/tabs/RouteChainEditor.tsx
1364
+ var import_react7 = require("react");
1365
+
1366
+ // client/src/ui/NSel.tsx
1367
+ var import_react6 = require("react");
1368
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1369
+ function NSel(props) {
1370
+ const options = props.options || [];
1371
+ const value = props.value || "";
1372
+ const disabled = !!props.disabled;
1373
+ const [open, setOpen] = (0, import_react6.useState)(false);
1374
+ const [idx, setIdx] = (0, import_react6.useState)(-1);
1375
+ const wrapRef = (0, import_react6.useRef)(null);
1376
+ const listRef = (0, import_react6.useRef)(null);
1377
+ let selectedLabel = "";
1378
+ for (let si = 0; si < options.length; si++) {
1379
+ if (options[si].id === value) selectedLabel = options[si].label;
1380
+ }
1381
+ (0, import_react6.useEffect)(() => {
1382
+ if (!open) return void 0;
1383
+ const onDown = (e) => {
1384
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
1385
+ };
1386
+ document.addEventListener("mousedown", onDown);
1387
+ return () => {
1388
+ document.removeEventListener("mousedown", onDown);
1389
+ };
1390
+ }, [open]);
1391
+ (0, import_react6.useEffect)(() => {
1392
+ if (!open || !listRef.current) return;
1393
+ const el = listRef.current.querySelector('[data-active="1"]');
1394
+ if (el && el.scrollIntoView) el.scrollIntoView({ block: "nearest" });
1395
+ }, [open, idx]);
1396
+ const indexOfValue = () => {
1397
+ for (let i = 0; i < options.length; i++) if (options[i].id === value) return i;
1398
+ return -1;
1399
+ };
1400
+ const closeMenu = (refocus) => {
1401
+ setOpen(false);
1402
+ setIdx(-1);
1403
+ if (refocus && wrapRef.current) {
1404
+ const btn = wrapRef.current.querySelector("button");
1405
+ if (btn && btn.focus) btn.focus();
1406
+ }
1407
+ };
1408
+ const pick = (id) => {
1409
+ closeMenu(true);
1410
+ if (id !== value && props.onChange) props.onChange(id);
1411
+ };
1412
+ const moveActive = (delta) => {
1413
+ const n = options.length;
1414
+ if (n === 0) return;
1415
+ let cur = idx >= 0 ? idx : indexOfValue();
1416
+ if (cur < 0) cur = delta > 0 ? -1 : 0;
1417
+ setIdx(delta > 0 ? ((cur + 1) % n + n) % n : ((cur - 1) % n + n) % n);
1418
+ };
1419
+ const onKey = (e) => {
1420
+ if (disabled) return;
1421
+ if (e.key === "Escape") {
1422
+ if (open) {
1423
+ e.preventDefault();
1424
+ closeMenu(true);
1425
+ }
1426
+ return;
1427
+ }
1428
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
1429
+ e.preventDefault();
1430
+ if (!open) {
1431
+ setOpen(true);
1432
+ setIdx(indexOfValue());
1433
+ } else moveActive(e.key === "ArrowDown" ? 1 : -1);
1434
+ return;
1435
+ }
1436
+ if (!open && (e.key === "Enter" || e.key === " ")) {
1437
+ e.preventDefault();
1438
+ setOpen(true);
1439
+ setIdx(indexOfValue());
1440
+ return;
1441
+ }
1442
+ if (open && e.key === "Enter") {
1443
+ e.preventDefault();
1444
+ let t = idx >= 0 ? idx : indexOfValue();
1445
+ if (t < 0) t = 0;
1446
+ if (options[t]) pick(options[t].id);
1447
+ }
1448
+ };
1449
+ const onBlur = (e) => {
1450
+ if (!open) return;
1451
+ const to = e.relatedTarget;
1452
+ if (!to || wrapRef.current && !wrapRef.current.contains(to)) setOpen(false);
1453
+ };
1454
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dsh-mem-sel", style: props.style, ref: wrapRef, onKeyDown: onKey, onBlur, children: [
1455
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1456
+ "button",
1457
+ {
1458
+ type: "button",
1459
+ className: "dsh-mem-select" + (open ? " dsh-mem-select-open" : ""),
1460
+ disabled,
1461
+ "aria-haspopup": "listbox",
1462
+ "aria-expanded": open,
1463
+ onClick: () => {
1464
+ if (open) closeMenu(false);
1465
+ else {
1466
+ setOpen(true);
1467
+ setIdx(-1);
1468
+ }
1469
+ },
1470
+ children: [
1471
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-mem-select-label", children: selectedLabel || props.placeholder || "(请选择)" }),
1472
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-mem-sel-chev" + (open ? " dsh-mem-sel-chev-open" : ""), "aria-hidden": true })
1473
+ ]
1474
+ }
1475
+ ),
1476
+ open && !disabled ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dsh-mem-pop", ref: listRef, role: "listbox", children: options.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dsh-mem-pop-empty", children: "无选项" }) : options.map((o, i) => {
1477
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1478
+ "button",
1479
+ {
1480
+ type: "button",
1481
+ className: "dsh-mem-pop-opt" + (o.id === value ? " dsh-mem-pop-opt-on" : ""),
1482
+ role: "option",
1483
+ "aria-selected": o.id === value,
1484
+ "data-active": i === idx ? "1" : "0",
1485
+ onMouseDown: (e) => {
1486
+ if (e.preventDefault) e.preventDefault();
1487
+ },
1488
+ onClick: () => {
1489
+ pick(o.id);
1490
+ },
1491
+ onMouseEnter: () => {
1492
+ if (idx !== i) setIdx(i);
1493
+ },
1494
+ children: [
1495
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-mem-pop-opt-label", children: o.label }),
1496
+ o.id === value ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dsh-mem-pop-check", children: "✓" }) : null
1497
+ ]
1498
+ },
1499
+ o.id
1500
+ );
1501
+ }) }) : null
1502
+ ] });
1503
+ }
1504
+
1505
+ // client/src/tabs/RouteChainEditor.tsx
1506
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1507
+ var modelsCache = {};
1508
+ var EFFORT_VOCAB = ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"];
1509
+ var STY2 = {
1510
+ wrap: { padding: "8px 0" },
1511
+ row: { background: "var(--dsh-mem-bg-inset)", borderRadius: 8, padding: 8, marginBottom: 8, border: "1px solid transparent" },
1512
+ rowErr: { border: "1px solid var(--dsh-mem-danger)" },
1513
+ badge: { flexShrink: 0, width: 20, height: 18, borderRadius: 999, background: "var(--dsh-mem-accent-weak)", color: "var(--dsh-mem-accent-text)", fontSize: 11, fontWeight: 600, display: "flex", alignItems: "center", justifyContent: "center" },
1514
+ // 控件行:供应商/模型/档位同行,flexWrap 兜底窄面板(放不下时档位折行)
1515
+ line: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" },
1516
+ // 序调整/删除按钮独立成行,右下角对齐
1517
+ actions: { display: "flex", alignItems: "center", justifyContent: "flex-end", gap: 4, marginTop: 6 },
1518
+ ico: { padding: 0, width: 26, height: 26, minWidth: 26, fontSize: 13, lineHeight: "20px" },
1519
+ add: { width: "100%", padding: "7px 0", fontSize: 12.5, color: "var(--dsh-mem-text-3)", background: "transparent", border: "1px dashed var(--dsh-mem-border-strong)", borderRadius: 8 },
1520
+ ghost: { border: "none", background: "transparent", color: "var(--dsh-mem-text-3)" },
1521
+ note: { fontSize: 11, color: "var(--dsh-mem-text-3)", marginTop: 6 },
1522
+ warn: { fontSize: 11, color: "var(--dsh-mem-danger)", marginTop: 6 },
1523
+ mono: { fontSize: 12.5, color: "var(--dsh-mem-text-1)", fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
1524
+ roRow: { display: "flex", alignItems: "center", gap: 8, background: "var(--dsh-mem-bg-inset)", borderRadius: 8, padding: "7px 10px", marginBottom: 6 }
1525
+ };
1526
+ var EMPTY_ROW = { provider: "", model: "", reasoningEffort: "" };
1527
+ function copyRow(e) {
1528
+ return { provider: e.provider || "", model: e.model || "", reasoningEffort: e.reasoningEffort || "" };
1529
+ }
1530
+ function RouteChainEditor(props) {
1531
+ const rpc = props.rpc;
1532
+ const disabled = !!props.disabled;
1533
+ const scope = props.scope ?? "global";
1534
+ const isLayer = scope !== "global";
1535
+ const [info, setInfo] = (0, import_react7.useState)(null);
1536
+ const [rows, setRows] = (0, import_react7.useState)(null);
1537
+ const [rowErrs, setRowErrs] = (0, import_react7.useState)({});
1538
+ const [err, setErr] = (0, import_react7.useState)(null);
1539
+ const [manual, setManual] = (0, import_react7.useState)({ idx: -1, text: "" });
1540
+ const pendingWrites = (0, import_react7.useRef)(0);
1541
+ function refreshInfo() {
1542
+ rpc("dsh-memory/llm-providers", {}).then((r) => {
1543
+ if (r && r.ok && pendingWrites.current === 0) setInfo(r.value);
1544
+ }).catch(() => {
1545
+ });
1546
+ }
1547
+ (0, import_react7.useEffect)(() => {
1548
+ refreshInfo();
1549
+ const timer = setInterval(refreshInfo, 5e3);
1550
+ return () => {
1551
+ clearInterval(timer);
1552
+ };
1553
+ }, [rpc]);
1554
+ (0, import_react7.useEffect)(() => {
1555
+ if (!info || !info.providers) return;
1556
+ info.providers.forEach((p) => {
1557
+ if (!p.id || modelsCache[p.id]) return;
1558
+ rpc("dsh-memory/llm-models", { provider: p.id }).then((r) => {
1559
+ if (r && r.ok && r.value) modelsCache[p.id] = r.value.models || [];
1560
+ }).catch(() => {
1561
+ });
1562
+ });
1563
+ }, [rpc, info]);
1564
+ const layerView = isLayer && info && info.layerChains ? info.layerChains[scope] : null;
1565
+ const savedRows = isLayer ? layerView?.runtime ?? [] : info && info.chain ? info.chain.current : [];
1566
+ (0, import_react7.useEffect)(() => {
1567
+ if (rows === null && info && savedRows.length) {
1568
+ setRows(savedRows.map(copyRow));
1569
+ }
1570
+ }, [info, rows, savedRows]);
1571
+ function updateRow(i, patch) {
1572
+ setRows(rows.map((r, j) => j === i ? { ...r, ...patch } : r));
1573
+ setRowErrs({});
1574
+ }
1575
+ function moveRow(i, dir) {
1576
+ let next = rows.map(copyRow);
1577
+ if (dir < 0 && i === 1) {
1578
+ if (!next[0].provider) next = next.slice(1);
1579
+ else {
1580
+ const t = next[0];
1581
+ next[0] = next[1];
1582
+ next[1] = t;
1583
+ }
1584
+ } else if (dir < 0 && i > 1) {
1585
+ const t2 = next[i - 1];
1586
+ next[i - 1] = next[i];
1587
+ next[i] = t2;
1588
+ } else if (dir > 0 && i < next.length - 1 && !(i === 0 && !next[0].provider)) {
1589
+ const t3 = next[i + 1];
1590
+ next[i + 1] = next[i];
1591
+ next[i] = t3;
1592
+ }
1593
+ setRows(next);
1594
+ setRowErrs({});
1595
+ }
1596
+ function removeRow(i) {
1597
+ if (i === 0) {
1598
+ if (isLayer) setRows(rows.length > 1 ? rows.slice(1) : rows);
1599
+ else setRows([EMPTY_ROW].concat(rows.slice(1)));
1600
+ } else {
1601
+ setRows(rows.slice(0, i).concat(rows.slice(i + 1)));
1602
+ }
1603
+ setRowErrs({});
1604
+ }
1605
+ function addRow() {
1606
+ let defProv = rows[0] && rows[0].provider || "";
1607
+ if (!defProv && info.providers && info.providers[0]) defProv = info.providers[0].id;
1608
+ setRows(rows.concat([{ provider: defProv, model: "", reasoningEffort: "" }]));
1609
+ setRowErrs({});
1610
+ }
1611
+ function forkStatic() {
1612
+ if (isLayer) {
1613
+ const st2 = layerView && layerView.static || [];
1614
+ if (st2.length) setRows(st2.slice(0, 8).map(copyRow));
1615
+ else {
1616
+ const first = info.providers && info.providers[0] || { id: "" };
1617
+ setRows([{ provider: first.id, model: "", reasoningEffort: "" }]);
1618
+ }
1619
+ setRowErrs({});
1620
+ return;
1621
+ }
1622
+ const st = info.chain && info.chain.static || [];
1623
+ setRows([EMPTY_ROW].concat(st.slice(0, 7).map(copyRow)));
1624
+ setRowErrs({});
1625
+ }
1626
+ function save() {
1627
+ const errs = {};
1628
+ const seen = {};
1629
+ if (isLayer && (!rows[0].provider || !rows[0].model)) {
1630
+ errs[0] = "主路由行必须显式选择供应商与模型(层链不支持跟随默认模型)";
1631
+ } else if (rows[0].provider && !rows[0].model || !rows[0].provider && rows[0].model) {
1632
+ errs[0] = "主路由行供应商与模型须成对(双空 = 跟随默认模型)";
1633
+ }
1634
+ if (rows[0].provider && rows[0].model) seen[rows[0].provider + "::" + rows[0].model] = 0;
1635
+ for (let i = 1; i < rows.length; i++) {
1636
+ if (!rows[i].provider || !rows[i].model) {
1637
+ errs[i] = "回退路由必须显式选择供应商与模型";
1638
+ continue;
1639
+ }
1640
+ const key = rows[i].provider + "::" + rows[i].model;
1641
+ if (seen[key] !== void 0) {
1642
+ errs[i] = seen[key] === 0 ? "与主路由完全相同(运行时会跳过,请去重)" : "与第 " + (seen[key] + 1) + " 行重复";
1643
+ } else {
1644
+ seen[key] = i;
1645
+ }
1646
+ }
1647
+ setRowErrs(errs);
1648
+ if (rows.length > 8) {
1649
+ setErr("路由链最多 8 行(含主路由行),请删除多余行");
1650
+ return;
1651
+ }
1652
+ if (Object.keys(errs).length) return;
1653
+ pendingWrites.current += 1;
1654
+ if (isLayer && info && info.layerChains) {
1655
+ setInfo({
1656
+ ...info,
1657
+ layerChains: {
1658
+ ...info.layerChains,
1659
+ [scope]: { ...info.layerChains[scope], runtime: rows.map(copyRow), source: "runtime" }
1660
+ }
1661
+ });
1662
+ } else {
1663
+ setInfo({
1664
+ ...info,
1665
+ chain: { ...info.chain, current: rows.map(copyRow), source: "runtime" }
1666
+ });
1667
+ }
1668
+ const payload = isLayer ? { distillLayerChains: { [scope]: rows } } : { distillChain: rows };
1669
+ rpc("dsh-memory/settings-set", payload).then((r) => {
1670
+ pendingWrites.current -= 1;
1671
+ setErr(!r || r.ok ? null : "路由链保存失败:" + (r && r.error && r.error.message || "未知错误"));
1672
+ refreshInfo();
1673
+ }).catch((e) => {
1674
+ pendingWrites.current -= 1;
1675
+ setErr("路由链保存失败:" + String(e && e.message || e));
1676
+ refreshInfo();
1677
+ });
1678
+ }
1679
+ function clearToFollow() {
1680
+ pendingWrites.current += 1;
1681
+ if (isLayer) {
1682
+ rpc("dsh-memory/settings-set", { distillLayerChains: { [scope]: [] } }).then((r) => {
1683
+ pendingWrites.current -= 1;
1684
+ setRows(null);
1685
+ setRowErrs({});
1686
+ setErr(!r || r.ok ? null : "清空失败,请重试");
1687
+ refreshInfo();
1688
+ }).catch((e) => {
1689
+ pendingWrites.current -= 1;
1690
+ setErr("清空失败:" + String(e && e.message || e));
1691
+ refreshInfo();
1692
+ });
1693
+ return;
1694
+ }
1695
+ rpc("dsh-memory/settings-set", { distillChain: [], distillProvider: "", distillModel: "", reasoningEffort: "" }).then((r) => {
1696
+ pendingWrites.current -= 1;
1697
+ setRows(null);
1698
+ setRowErrs({});
1699
+ setErr(!r || r.ok ? null : "清空失败,请重试");
1700
+ refreshInfo();
1701
+ }).catch((e) => {
1702
+ pendingWrites.current -= 1;
1703
+ setErr("清空失败:" + String(e && e.message || e));
1704
+ refreshInfo();
1705
+ });
1706
+ }
1707
+ if (!info) return null;
1708
+ const providers = info.providers || [];
1709
+ const providersById = {};
1710
+ providers.forEach((p) => {
1711
+ providersById[p.id] = p;
1712
+ });
1713
+ function roRow(e, i) {
1714
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: STY2.roRow, children: [
1715
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: STY2.badge, children: i === 0 ? "主" : String(i + 1) }),
1716
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { ...STY2.mono, color: "var(--dsh-mem-text-2)" }, children: e.provider + " / " + e.model }),
1717
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: { marginLeft: "auto", flexShrink: 0, fontSize: 11, color: "var(--dsh-mem-text-3)" }, children: e.effort ? "档位 " + e.effort : "跟随部署配置" })
1718
+ ] }, "ro" + i);
1719
+ }
1720
+ if (info.pinned) {
1721
+ const effPin = isLayer ? layerView?.effectiveChain ?? [] : info.chain && info.chain.effectiveChain || [];
1722
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: STY2.wrap, children: [
1723
+ effPin.map(roRow),
1724
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: STY2.note, children: "部署已锁定路由(pin),调整请修改 cordis.patch.yml 中 llm 的配置。" })
1725
+ ] });
1726
+ }
1727
+ if (rows === null) {
1728
+ if (isLayer) {
1729
+ const lv = layerView;
1730
+ const src = lv?.source ?? "global";
1731
+ if (src === "static" && lv) {
1732
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: STY2.wrap, children: [
1733
+ lv.static.map((e, i) => roRow({ provider: e.provider, model: e.model, effort: e.reasoningEffort || "" }, i)),
1734
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { marginTop: 6 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(NButton, { onClick: forkStatic, disabled, children: "自定义本层链" }) })
1735
+ ] });
1736
+ }
1737
+ const previewRows = lv?.effectiveChain ?? [];
1738
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: STY2.wrap, children: [
1739
+ previewRows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: S.switchDesc, children: "本层跟随全局链,暂无可用路由。" }) : previewRows.map(roRow),
1740
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { marginTop: 6 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(NButton, { onClick: forkStatic, disabled, children: "自定义本层链" }) })
1741
+ ] });
1742
+ }
1743
+ const effFollow = info.chain && info.chain.effectiveChain || [];
1744
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: STY2.wrap, children: [
1745
+ effFollow.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: S.switchDesc, children: "蒸馏跟随默认模型,未配置回退链。" }) : effFollow.map(roRow),
1746
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { marginTop: 6 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(NButton, { onClick: forkStatic, disabled, children: "编辑为运行时链" }) })
1747
+ ] });
1748
+ }
1749
+ const capped = rows.length >= 8;
1750
+ const dirty = JSON.stringify(rows) !== JSON.stringify(savedRows);
1751
+ const rowEls = rows.map((row2, i) => {
1752
+ const isPrimary = i === 0;
1753
+ const known = !row2.provider || !!providersById[row2.provider];
1754
+ const modelsLoaded = row2.provider ? Object.prototype.hasOwnProperty.call(modelsCache, row2.provider) : false;
1755
+ const modelList = modelsLoaded ? modelsCache[row2.provider] : [];
1756
+ const manualInput = modelsLoaded && modelList.length === 0;
1757
+ let curEfforts = [];
1758
+ for (let mi = 0; mi < modelList.length; mi++) {
1759
+ if (modelList[mi].id === row2.model) {
1760
+ curEfforts = modelList[mi].efforts || [];
1761
+ break;
1762
+ }
1763
+ }
1764
+ let providerOptions = providers.map((p) => {
1765
+ return { id: p.id, label: p.name !== p.id ? p.name + "(" + p.id + ")" : p.id };
1766
+ });
1767
+ if (isPrimary && !isLayer) {
1768
+ providerOptions = [
1769
+ {
1770
+ id: "",
1771
+ label: info.default ? "跟随默认模型(" + info.default.provider + " / " + info.default.model + ")" : "跟随默认模型"
1772
+ }
1773
+ ].concat(providerOptions);
1774
+ }
1775
+ if (row2.provider && !providersById[row2.provider]) {
1776
+ providerOptions.push({ id: row2.provider, label: row2.provider + "(已不在列表)" });
1777
+ }
1778
+ const modelOptions = modelList.map((m) => {
1779
+ return { id: m.id, label: m.name !== m.id ? m.name + "(" + m.id + ")" : m.id };
1780
+ });
1781
+ if (row2.model && !modelList.some((m) => m.id === row2.model)) {
1782
+ modelOptions.push({ id: row2.model, label: row2.model + "(已不在列表)" });
1783
+ }
1784
+ const effortOptions = [{ id: "", label: "跟随部署配置" }].concat(
1785
+ curEfforts.filter((k) => EFFORT_VOCAB.indexOf(k) >= 0).map((k) => ({ id: k, label: k }))
1786
+ );
1787
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { ...STY2.row, ...rowErrs[i] ? STY2.rowErr : null }, children: [
1788
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: STY2.line, children: [
1789
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { style: STY2.badge, children: isPrimary ? "主" : String(i + 1) }),
1790
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1791
+ NSel,
1792
+ {
1793
+ style: { flex: 1, minWidth: 150 },
1794
+ options: providerOptions,
1795
+ value: row2.provider,
1796
+ disabled,
1797
+ placeholder: isPrimary ? "跟随默认模型" : "供应商",
1798
+ onChange: (v) => {
1799
+ updateRow(i, { provider: v, model: "", reasoningEffort: "" });
1800
+ }
1801
+ }
1802
+ ),
1803
+ !row2.provider ? null : manualInput ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1804
+ NInput,
1805
+ {
1806
+ style: { flex: 1, minWidth: 150 },
1807
+ placeholder: "模型 id(该供应商未提供列表,输入后回车)…",
1808
+ value: manual.idx === i ? manual.text : row2.model,
1809
+ onChange: (e) => {
1810
+ setManual({ idx: i, text: e.target.value });
1811
+ },
1812
+ onKeyDown: (e) => {
1813
+ if (e.key === "Enter") {
1814
+ const v = (manual.idx === i ? manual.text : "").trim();
1815
+ if (v) {
1816
+ updateRow(i, { model: v });
1817
+ setManual({ idx: -1, text: "" });
1818
+ }
1819
+ }
1820
+ }
1821
+ }
1822
+ ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1823
+ NSel,
1824
+ {
1825
+ style: { flex: 1, minWidth: 150 },
1826
+ options: modelOptions,
1827
+ value: row2.model,
1828
+ disabled: disabled || !modelsLoaded,
1829
+ placeholder: modelsLoaded ? isPrimary ? "(选择模型,可留空跟随默认)" : "(选择模型)" : "加载模型列表…",
1830
+ onChange: (v) => {
1831
+ updateRow(i, { model: v });
1832
+ }
1833
+ }
1834
+ ),
1835
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1836
+ NSel,
1837
+ {
1838
+ style: { flexShrink: 0, width: 118 },
1839
+ options: effortOptions,
1840
+ value: row2.reasoningEffort,
1841
+ disabled: disabled || !(row2.provider && row2.model),
1842
+ placeholder: "跟随部署配置",
1843
+ onChange: (v) => {
1844
+ updateRow(i, { reasoningEffort: v });
1845
+ }
1846
+ }
1847
+ )
1848
+ ] }),
1849
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: STY2.actions, children: [
1850
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1851
+ NButton,
1852
+ {
1853
+ style: STY2.ico,
1854
+ disabled: disabled || i === 0,
1855
+ title: i === 1 ? "上移(与主路由互换/顶替为主路由)" : "上移",
1856
+ onClick: () => {
1857
+ moveRow(i, -1);
1858
+ },
1859
+ children: "↑"
1860
+ }
1861
+ ),
1862
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1863
+ NButton,
1864
+ {
1865
+ style: STY2.ico,
1866
+ disabled: disabled || i === rows.length - 1 || i === 0 && !row2.provider,
1867
+ title: "下移",
1868
+ onClick: () => {
1869
+ moveRow(i, 1);
1870
+ },
1871
+ children: "↓"
1872
+ }
1873
+ ),
1874
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1875
+ NButton,
1876
+ {
1877
+ style: STY2.ico,
1878
+ disabled,
1879
+ title: isPrimary ? "重置为跟随默认" : "删除",
1880
+ onClick: () => {
1881
+ removeRow(i);
1882
+ },
1883
+ children: "✕"
1884
+ }
1885
+ )
1886
+ ] }),
1887
+ isPrimary && !row2.provider ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: STY2.note, children: "跟随默认模型" + (info.default ? ":" + info.default.provider + " / " + info.default.model : "") + "(档位跟随部署配置,选定模型后可单独设置)" }) : null,
1888
+ !known ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: STY2.warn, children: "⚠ 供应商 " + row2.provider + " 已不在已注册路由中:该路由调用会失败并被链跳过(不阻止保存)。" }) : null,
1889
+ rowErrs[i] ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: STY2.warn, children: "✕ " + rowErrs[i] }) : null
1890
+ ] }, "row" + i);
1891
+ });
1892
+ const effChain = isLayer ? layerView?.effectiveChain ?? [] : info.chain && info.chain.effectiveChain || [];
1893
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: STY2.wrap, children: [
1894
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1895
+ "div",
1896
+ {
1897
+ style: { ...S.switchDesc, marginBottom: 8 },
1898
+ title: isLayer ? "本层链失败只在层内降级,绝不落到全局链;每行档位独立" : "第 1 行主路由;失败(报错/掐断/网络异常/空输出)按序降级;每行档位独立",
1899
+ children: isLayer ? "主路由失败,只降级到本层回退" : "主路由失败,按序降级"
1900
+ }
1901
+ ),
1902
+ rowEls,
1903
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(NButton, { style: STY2.add, disabled: disabled || capped, onClick: addRow, children: capped ? "已达上限(8 条)" : "+ 添加回退路由" }),
1904
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 10 }, children: [
1905
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1906
+ NButton,
1907
+ {
1908
+ style: isLayer ? { ...STY2.ghost, color: "var(--dsh-mem-danger)", border: "1px solid var(--dsh-mem-danger)" } : STY2.ghost,
1909
+ disabled,
1910
+ onClick: clearToFollow,
1911
+ children: isLayer ? "清除自定义 · 跟随全局" : "清空并跟随部署配置"
1912
+ }
1913
+ ),
1914
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: S.grow }),
1915
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(NButton, { variant: "primary", disabled, onClick: save, children: "保存" })
1916
+ ] }),
1917
+ err ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { ...STY2.warn, marginTop: 8 }, children: "✕ " + err }) : null,
1918
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--dsh-mem-border)" }, children: [
1919
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { fontSize: 11, color: "var(--dsh-mem-text-3)", marginBottom: 4 }, children: "实际链" + (dirty ? "(保存后更新;当前显示已保存值)" : "") }),
1920
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1921
+ "div",
1922
+ {
1923
+ style: { fontSize: 12, color: "var(--dsh-mem-text-2)", wordBreak: "break-all", fontFamily: "ui-monospace, SFMono-Regular, Consolas, monospace" },
1924
+ children: effChain.map((e) => e.provider + "/" + e.model + (e.effort ? "(" + e.effort + ")" : "")).join(" → ") || "(暂无可用路由)"
1925
+ }
1926
+ )
1927
+ ] })
1928
+ ] });
1929
+ }
1930
+
1931
+ // client/src/tabs/DistillSettings.tsx
1932
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1933
+ var STY3 = {
1934
+ hint: { fontSize: 11, color: "var(--dsh-mem-text-3)", margin: "0 0 8px" },
1935
+ panelHead: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginBottom: 8 },
1936
+ panelTitle: { fontSize: 13.5, fontWeight: 650, color: "var(--dsh-mem-text-1)" },
1937
+ chip: {
1938
+ display: "inline-flex",
1939
+ alignItems: "center",
1940
+ borderRadius: 999,
1941
+ padding: "1px 8px",
1942
+ fontSize: 11,
1943
+ fontWeight: 600,
1944
+ lineHeight: "18px",
1945
+ whiteSpace: "nowrap"
1946
+ },
1947
+ chipAccent: { background: "var(--dsh-mem-accent-weak)", color: "var(--dsh-mem-accent-text)" },
1948
+ chipMuted: { background: "var(--dsh-mem-bg-inset)", color: "var(--dsh-mem-text-2)" },
1949
+ inUse: { fontSize: 11, color: "var(--dsh-mem-text-3)", margin: "6px 0 0" }
1950
+ };
1951
+ function Dot(props) {
1952
+ const base = {
1953
+ display: "inline-block",
1954
+ width: 7,
1955
+ height: 7,
1956
+ borderRadius: "50%",
1957
+ marginRight: 4,
1958
+ verticalAlign: "middle",
1959
+ flexShrink: 0
1960
+ };
1961
+ if (props.kind === "runtime") return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { ...base, background: "var(--dsh-mem-accent)" } });
1962
+ if (props.kind === "static") return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { ...base, background: "transparent", border: "1.5px solid var(--dsh-mem-text-3)" } });
1963
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { ...base, background: "var(--dsh-mem-track)" } });
1964
+ }
1965
+ var LAYER_META = {
1966
+ l1: { seg: "L1", title: "L1 · 抽取 / 去重" },
1967
+ l2: { seg: "L2", title: "L2 · 场景摘要" },
1968
+ l3: { seg: "L3", title: "L3 · 画像蒸馏" }
1969
+ };
1970
+ function DistillSettings(props) {
1971
+ const rpc = props.rpc;
1972
+ const disabled = !!props.disabled;
1973
+ const [info, setInfo] = (0, import_react8.useState)(null);
1974
+ const [tab, setTab] = (0, import_react8.useState)("g");
1975
+ (0, import_react8.useEffect)(() => {
1976
+ let alive = true;
1977
+ const refresh = () => {
1978
+ rpc("dsh-memory/llm-providers", {}).then((r) => {
1979
+ if (alive && r && r.ok) setInfo(r.value);
1980
+ }).catch(() => {
1981
+ });
1982
+ };
1983
+ refresh();
1984
+ const timer = setInterval(refresh, 5e3);
1985
+ return () => {
1986
+ alive = false;
1987
+ clearInterval(timer);
1988
+ };
1989
+ }, [rpc]);
1990
+ const layers = info?.layerChains;
1991
+ const dotOf = (k) => layers?.[k]?.source ?? "global";
1992
+ const users = ["l1", "l2", "l3"].filter((k) => dotOf(k) === "global");
1993
+ const segOptions = [
1994
+ { key: "g", label: "全局默认", title: "未单独配置的层走这条链(当前在用:" + (users.length ? users.map((k) => LAYER_META[k].seg).join("、") : "无") + ")" },
1995
+ ...["l1", "l2", "l3"].map((k) => ({
1996
+ key: k,
1997
+ title: LAYER_META[k].title + " · " + (dotOf(k) === "runtime" ? "运行时自定义" : dotOf(k) === "static" ? "部署 YAML 层链(只读)" : "跟随全局"),
1998
+ label: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { children: [
1999
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Dot, { kind: dotOf(k) }),
2000
+ LAYER_META[k].seg
2001
+ ] })
2002
+ }))
2003
+ ];
2004
+ const chipTitle = (k) => dotOf(k) === "runtime" ? "本层走设置页自定义链" : dotOf(k) === "static" ? "本层走部署 YAML 层链(UI 只读,自定义可覆盖)" : "本层未单独配置,走全局默认链";
2005
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
2006
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Segmented, { value: tab, options: segOptions, onChange: (k) => setTab(k) }),
2007
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: STY3.hint, children: [
2008
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Dot, { kind: "runtime" }),
2009
+ " 自定义 · ",
2010
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Dot, { kind: "static" }),
2011
+ " 部署 YAML · ",
2012
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(Dot, { kind: "global" }),
2013
+ " 跟随全局",
2014
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { title: "每层实际链:运行时自定义 → 部署 YAML 层链 → 全局默认链,逐级兜底;部署 pin 时运行时编辑只读", children: "(层链优先于全局)" })
2015
+ ] }),
2016
+ tab === "g" ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
2017
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: STY3.panelHead, children: [
2018
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: STY3.panelTitle, children: "全局默认链" }),
2019
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { ...STY3.chip, ...STY3.chipAccent }, children: "运行时 · 可编辑" }),
2020
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { ...STY3.inUse, marginLeft: "auto" }, children: users.length ? "在用:" + users.map((k) => LAYER_META[k].seg).join("、") : "当前无层使用" })
2021
+ ] }),
2022
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(RouteChainEditor, { rpc, disabled }, "g"),
2023
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(BudgetInputs, { rpc, disabled, data: props.data, setData: props.setData, onError: props.onError, scope: "input" }, "g-budget"),
2024
+ info?.channel ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ChannelEditor, { channel: info.channel, rpc, disabled }) : null
2025
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
2026
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: STY3.panelHead, children: [
2027
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: STY3.panelTitle, children: LAYER_META[tab].title }),
2028
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { style: { ...STY3.chip, ...dotOf(tab) === "runtime" ? STY3.chipAccent : STY3.chipMuted }, title: chipTitle(tab), children: dotOf(tab) === "runtime" ? "运行时自定义" : dotOf(tab) === "static" ? "静态 · YAML" : "跟随全局" })
2029
+ ] }),
2030
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(RouteChainEditor, { rpc, disabled, scope: tab }, tab),
2031
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(BudgetInputs, { rpc, disabled, data: props.data, setData: props.setData, onError: props.onError, scope: tab }, tab + "-budget")
2032
+ ] })
2033
+ ] });
2034
+ }
2035
+
2036
+ // client/src/tabs/EmbeddingSection.tsx
2037
+ var import_react9 = require("react");
2038
+
2039
+ // client/src/rpc.ts
2040
+ function makeRpc(ctx) {
2041
+ return (endpoint, payload) => {
2042
+ if (!ctx.connection || !ctx.connection.rpc) return Promise.reject(new Error("connection 服务不可用"));
2043
+ return ctx.connection.rpc.call("/rpc", endpoint, payload ?? {});
2044
+ };
2045
+ }
2046
+ function asLoose(rpc) {
2047
+ return rpc;
2048
+ }
2049
+
2050
+ // client/src/tabs/EmbeddingSection.tsx
2051
+ var import_jsx_runtime9 = require("react/jsx-runtime");
2052
+ var RSTY = {
2053
+ block: { marginTop: 10, paddingTop: 10, borderTop: "1px solid var(--dsh-mem-border)" },
2054
+ title: { fontSize: 12, fontWeight: 600, color: "var(--dsh-mem-text-3)", margin: "0 0 2px" },
2055
+ row: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", marginTop: 8 },
2056
+ label: { fontSize: 11, color: "var(--dsh-mem-text-3)", flexShrink: 0, width: 64 },
2057
+ input: { flex: "1 1 200px", minWidth: 140 },
2058
+ chip: {
2059
+ display: "inline-flex",
2060
+ alignItems: "center",
2061
+ borderRadius: 999,
2062
+ padding: "1px 8px",
2063
+ fontSize: 11,
2064
+ fontWeight: 600,
2065
+ lineHeight: "18px",
2066
+ whiteSpace: "nowrap"
2067
+ },
2068
+ chipAccent: { background: "var(--dsh-mem-accent-weak)", color: "var(--dsh-mem-accent-text)" },
2069
+ chipMuted: { background: "var(--dsh-mem-bg-inset)", color: "var(--dsh-mem-text-2)" },
2070
+ note: { fontSize: 11, color: "var(--dsh-mem-text-3)", margin: "6px 0 0" }
2071
+ };
2072
+ function EmbeddingSection(props) {
2073
+ const rpc = props.rpc;
2074
+ const loose = asLoose(rpc);
2075
+ const [st, setSt] = (0, import_react9.useState)(null);
2076
+ const [err, setErr] = (0, import_react9.useState)(null);
2077
+ const busyPollRef = (0, import_react9.useRef)(null);
2078
+ const [rBase, setRBase] = (0, import_react9.useState)("");
2079
+ const [rModel, setRModel] = (0, import_react9.useState)("");
2080
+ const [rDims, setRDims] = (0, import_react9.useState)("");
2081
+ const [rKey, setRKey] = (0, import_react9.useState)("");
2082
+ const remoteDirty = (0, import_react9.useRef)({ base: false, model: false, dims: false });
2083
+ const baseTimer = (0, import_react9.useRef)(void 0);
2084
+ const modelTimer = (0, import_react9.useRef)(void 0);
2085
+ const load = (0, import_react9.useCallback)(() => {
2086
+ rpc("dsh-memory/embedding-state-get", {}).then((r) => {
2087
+ if (r && r.ok && r.value && r.value.supported !== false) {
2088
+ const v = r.value;
2089
+ if (busyPollRef.current) {
2090
+ busyPollRef.current.v = !!(v.download && (v.download.phase === "downloading" || v.download.phase === "verifying") || v.apply.busy || v.reindex && v.reindex.running || v.runtime && v.runtime.phase === "installing");
2091
+ }
2092
+ setSt(v);
2093
+ setErr(null);
2094
+ } else if (r && r.ok && r.value && r.value.supported === false) {
2095
+ setSt(null);
2096
+ setErr("__unsupported__");
2097
+ } else {
2098
+ setErr(r && !r.ok ? r.error.message : "RPC error");
2099
+ }
2100
+ }).catch((e) => {
2101
+ setErr(String(e && e.message || e));
2102
+ });
2103
+ }, [rpc]);
2104
+ (0, import_react9.useEffect)(() => {
2105
+ load();
2106
+ }, [load]);
2107
+ (0, import_react9.useEffect)(() => {
2108
+ let stopped = false;
2109
+ const busyFlag = { v: false };
2110
+ busyPollRef.current = busyFlag;
2111
+ const tick = () => {
2112
+ if (stopped) return;
2113
+ load();
2114
+ timer = setTimeout(tick, busyFlag.v ? 1200 : 5e3);
2115
+ };
2116
+ let timer = setTimeout(tick, 1200);
2117
+ return () => {
2118
+ stopped = true;
2119
+ clearTimeout(timer);
2120
+ busyPollRef.current = null;
2121
+ };
2122
+ }, [load]);
2123
+ (0, import_react9.useEffect)(() => () => {
2124
+ clearTimeout(baseTimer.current);
2125
+ clearTimeout(modelTimer.current);
2126
+ }, []);
2127
+ (0, import_react9.useEffect)(() => {
2128
+ if (!st || !st.remote) return;
2129
+ if (!remoteDirty.current.base) setRBase(st.remote.baseURL || "");
2130
+ if (!remoteDirty.current.model) setRModel(st.remote.model || "");
2131
+ if (!remoteDirty.current.dims) setRDims(st.remote.dimensions > 0 ? String(st.remote.dimensions) : "");
2132
+ }, [st]);
2133
+ const commitRemote = (patch) => {
2134
+ rpc("dsh-memory/settings-set", patch).then((r) => {
2135
+ if (!r || !r.ok) setErr(r && r.error ? "远程连接保存失败:" + r.error.message : "远程连接保存失败");
2136
+ else {
2137
+ setErr(null);
2138
+ load();
2139
+ }
2140
+ }).catch((e) => setErr("远程连接保存失败:" + String(e && e.message || e)));
2141
+ };
2142
+ const onRemoteBase = (v) => {
2143
+ remoteDirty.current.base = true;
2144
+ setRBase(v);
2145
+ clearTimeout(baseTimer.current);
2146
+ baseTimer.current = setTimeout(() => {
2147
+ remoteDirty.current.base = false;
2148
+ commitRemote({ embedRemoteBaseURL: v.trim() });
2149
+ }, 600);
2150
+ };
2151
+ const onRemoteModel = (v) => {
2152
+ remoteDirty.current.model = true;
2153
+ setRModel(v);
2154
+ clearTimeout(modelTimer.current);
2155
+ modelTimer.current = setTimeout(() => {
2156
+ remoteDirty.current.model = false;
2157
+ commitRemote({ embedRemoteModel: v.trim() });
2158
+ }, 600);
2159
+ };
2160
+ const commitRemoteDims = () => {
2161
+ const raw = rDims.trim();
2162
+ if (raw === "") {
2163
+ remoteDirty.current.dims = false;
2164
+ commitRemote({ embedRemoteDimensions: 0 });
2165
+ return;
2166
+ }
2167
+ const n = Number(raw);
2168
+ if (!Number.isInteger(n) || n <= 0 || n > 1e5) {
2169
+ setErr("嵌入维度须为 1~100000 的整数(留空 = 跟随部署配置)");
2170
+ return;
2171
+ }
2172
+ remoteDirty.current.dims = false;
2173
+ commitRemote({ embedRemoteDimensions: n });
2174
+ };
2175
+ const commitRemoteKey = () => {
2176
+ const v = rKey.trim();
2177
+ if (v) commitRemote({ embedRemoteApiKey: v });
2178
+ setRKey("");
2179
+ };
2180
+ const call = (endpoint, payload, confirmText) => {
2181
+ if (confirmText && !window.confirm(confirmText)) return;
2182
+ loose(endpoint, payload).then((r) => {
2183
+ if (!r || !r.ok) setErr(r && r.error ? r.error.message : "操作失败");
2184
+ else {
2185
+ setErr(null);
2186
+ load();
2187
+ }
2188
+ }).catch((e) => {
2189
+ setErr(String(e && e.message || e));
2190
+ });
2191
+ };
2192
+ if (err === "__unsupported__") {
2193
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-mem-rb-card", children: [
2194
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { fontWeight: 600, marginBottom: 4 }, children: "语义检索(嵌入)" }),
2195
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-muted", children: "存储处于降级状态,嵌入管理不可用。" })
2196
+ ] });
2197
+ }
2198
+ if (!st) {
2199
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-mem-rb-card", children: [
2200
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-muted", children: err ? "嵌入状态读取失败:" + err : "嵌入状态读取中…" }),
2201
+ err ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(NButton, { style: { marginTop: 8 }, onClick: load, children: "重试" }) : null
2202
+ ] });
2203
+ }
2204
+ const switchConfirm = "切换嵌入源后将按新模型重建向量索引(期间语义检索暂退化为关键词匹配,不影响对话)。确定切换?";
2205
+ const onSource = (key) => {
2206
+ if (key === "local") {
2207
+ const ready = [];
2208
+ for (let i = 0; i < st.models.length; i++) if (st.models[i].state === "downloaded") ready.push(st.models[i].id);
2209
+ if (ready.length === 0) {
2210
+ setErr("请先在下方下载一个本地嵌入模型,再切换到本地档。");
2211
+ return;
2212
+ }
2213
+ const current = st.activeModel && ready.indexOf(st.activeModel) >= 0 ? st.activeModel : ready[0];
2214
+ call("dsh-memory/embedding-source-set", { source: "local", activeModel: current }, switchConfirm);
2215
+ return;
2216
+ }
2217
+ call("dsh-memory/embedding-source-set", { source: key }, key === "remote" ? switchConfirm : null);
2218
+ };
2219
+ const dl = st.download;
2220
+ const dlActive = dl && (dl.phase === "downloading" || dl.phase === "verifying");
2221
+ const ap = st.apply;
2222
+ const localInfo = st.local;
2223
+ const rt = st.runtime;
2224
+ const remote = st.remote;
2225
+ let runtimeRow = null;
2226
+ if (rt.phase === "installing") {
2227
+ runtimeRow = /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { marginTop: 8, fontSize: 12 }, children: [
2228
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: S.flexRow, children: [
2229
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { children: "安装推理运行时中… 已耗时 " + Math.round(rt.elapsedMs / 1e3) + "s(约 100~200MB,视网络)" }),
2230
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: S.grow }),
2231
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(NButton, { onClick: () => call("dsh-memory/embedding-runtime-cancel", {}), children: "取消" })
2232
+ ] }),
2233
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("pre", { style: { ...S.pre, maxHeight: 68, marginTop: 6, fontSize: 11, opacity: 0.85 }, children: (rt.lastLines || []).join("\n") || "等待 npm 输出…" })
2234
+ ] });
2235
+ } else if (rt.phase === "error") {
2236
+ runtimeRow = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsh-mem-danger)" }, children: "运行时安装失败:" + (rt.error || "未知") + "(重新切换嵌入源可重试)" });
2237
+ } else if (rt.phase === "ready") {
2238
+ runtimeRow = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-muted", style: { marginTop: 8 }, children: "推理运行时就绪(transformers.js v" + rt.installedVersion + ")" });
2239
+ } else if (st.source === "local") {
2240
+ runtimeRow = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-muted", style: { marginTop: 8 }, children: "首次启用本地嵌入时会自动安装推理运行时(约 100~200MB)。" });
2241
+ }
2242
+ let applyRow = null;
2243
+ if (ap.phase === "warming") {
2244
+ applyRow = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-muted", style: { marginTop: 8 }, children: "加载嵌入模型中…(首次需数秒)" });
2245
+ } else if (ap.phase === "switching") {
2246
+ applyRow = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-muted", style: { marginTop: 8 }, children: "切换嵌入源中…" });
2247
+ } else if (ap.phase === "error") {
2248
+ applyRow = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsh-mem-danger)" }, children: "切换失败:" + ap.message + "(已保存的嵌入源不变,重启后仍按原源运行)" });
2249
+ } else if (st.reindex && st.reindex.running) {
2250
+ const rj = st.reindex;
2251
+ const rDone = rj.l1Done + rj.l0Done;
2252
+ const rTotal = rj.l1Total + rj.l0Total;
2253
+ const rPct = rTotal > 0 ? Math.round(rDone / rTotal * 100) : 0;
2254
+ applyRow = /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { marginTop: 8 }, children: [
2255
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: S.flexRow, children: [
2256
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-mem-rb-muted", children: "重嵌入中 L1 " + rj.l1Done + "/" + rj.l1Total + " · L0 " + rj.l0Done + "/" + rj.l0Total + "(" + rPct + "%)" }),
2257
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: S.grow }),
2258
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(NButton, { onClick: () => call("dsh-memory/embedding-reindex-cancel", {}), children: "取消" })
2259
+ ] }),
2260
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { ...S.flexRow, marginTop: 6 }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-bar", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-fill", style: { width: rPct + "%" } }) }) })
2261
+ ] });
2262
+ }
2263
+ const modelCards = st.models.map((m) => {
2264
+ const isActive = st.source === "local" && st.activeModel === m.id;
2265
+ const mDl = dlActive && dl.modelId === m.id;
2266
+ const pct = mDl && dl.overallTotal > 0 ? Math.round(dl.overallReceived / dl.overallTotal * 100) : 0;
2267
+ let action;
2268
+ if (mDl) {
2269
+ action = /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { flex: 1, minWidth: 200 }, children: [
2270
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: S.flexRow, children: [
2271
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-mem-rb-muted", style: { whiteSpace: "nowrap" }, children: (dl.phase === "verifying" ? "校验中 " : "") + fmtMB(dl.overallReceived) + " / " + fmtMB(dl.overallTotal) + "(文件 " + dl.fileIndex + "/" + dl.fileCount + "," + pct + "%" + (dl.speedBps > 0 && dl.phase === "downloading" ? "," + fmtMB(dl.speedBps) + "/s" : "") + ")" }),
2272
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: S.grow }),
2273
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(NButton, { onClick: () => call("dsh-memory/embedding-download-cancel", {}), children: "取消" })
2274
+ ] }),
2275
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { ...S.flexRow, marginTop: 6 }, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-bar", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-fill", style: { width: pct + "%" } }) }) })
2276
+ ] });
2277
+ } else if (isActive) {
2278
+ action = /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: S.flexRow, children: [
2279
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-mem-tag dsh-mem-tag-work-task", children: "使用中" }),
2280
+ localInfo && localInfo.state === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-mem-rb-muted", children: "模型加载中…" }) : null,
2281
+ localInfo && localInfo.state === "failed" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: { fontSize: 12, color: "var(--dsh-mem-danger)" }, children: "加载失败:" + (localInfo.error || "") }) : null,
2282
+ localInfo && localInfo.state === "ready" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dsh-mem-rb-muted", children: "已就绪" }) : null
2283
+ ] });
2284
+ } else if (m.state === "downloaded") {
2285
+ action = /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: S.flexRow, children: [
2286
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2287
+ NButton,
2288
+ {
2289
+ disabled: ap.busy,
2290
+ onClick: () => call("dsh-memory/embedding-source-set", { source: "local", activeModel: m.id }, switchConfirm),
2291
+ children: "启用"
2292
+ }
2293
+ ),
2294
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2295
+ NButton,
2296
+ {
2297
+ disabled: dlActive,
2298
+ onClick: () => call("dsh-memory/embedding-model-delete", { modelId: m.id }, "删除已下载的 " + m.name + "(" + fmtMB(m.totalBytes) + ")?"),
2299
+ children: "删除"
2300
+ }
2301
+ )
2302
+ ] });
2303
+ } else {
2304
+ action = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2305
+ NButton,
2306
+ {
2307
+ disabled: dlActive || !st.ceilings.local,
2308
+ title: !st.ceilings.local ? "部署已禁用本地嵌入模型" : "",
2309
+ onClick: () => call("dsh-memory/embedding-download-start", { modelId: m.id }),
2310
+ children: (m.state === "partial" ? "继续下载 " : "下载 ") + fmtMB(m.totalBytes)
2311
+ }
2312
+ );
2313
+ }
2314
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
2315
+ "div",
2316
+ {
2317
+ style: { ...S.flexRow, padding: "8px 0", borderBottom: "1px solid var(--dsh-mem-border)", flexWrap: "wrap" },
2318
+ children: [
2319
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: { minWidth: 150 }, children: [
2320
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { fontWeight: 600 }, children: m.name }),
2321
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-muted", children: m.tags.join(" · ") + " · " + m.dims + " 维 · 上下文 " + m.contextTokens })
2322
+ ] }),
2323
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { flex: 1, minWidth: 180, fontSize: 12, color: "var(--dsh-mem-text-2)" }, children: m.description }),
2324
+ action
2325
+ ]
2326
+ },
2327
+ m.id
2328
+ );
2329
+ });
2330
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dsh-mem-rb-card", children: [
2331
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: S.flexRow, children: [
2332
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { fontWeight: 600, whiteSpace: "nowrap" }, children: "语义检索(嵌入源)" }),
2333
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: S.grow }),
2334
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2335
+ Segmented,
2336
+ {
2337
+ value: st.source,
2338
+ options: [
2339
+ { key: "off", label: "关闭" },
2340
+ { key: "local", label: "本地", disabled: !st.ceilings.local, disabledTitle: "部署已禁用本地嵌入模型" },
2341
+ { key: "remote", label: "远程", disabled: !st.ceilings.remote, disabledTitle: "部署未配置远程嵌入(baseUrl/apiKey/model/dimensions)" }
2342
+ ],
2343
+ onChange: onSource
2344
+ }
2345
+ )
2346
+ ] }),
2347
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dsh-mem-rb-muted", style: { marginTop: 4 }, children: st.source === "off" ? "当前:关键词(BM25)检索,不做向量嵌入" : st.source === "remote" ? "当前:远程嵌入(" + (remote.model || "未配置模型") + (remote.baseURL ? " · " + remote.baseURL : "") + ")" : "当前:本地嵌入" + (st.activeModel ? "(" + st.activeModel + ")" : "") }),
2348
+ st.activeNote ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { marginTop: 4, fontSize: 12, color: "var(--dsh-mem-danger)" }, children: st.activeNote }) : null,
2349
+ err && err !== "__unsupported__" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { marginTop: 6, fontSize: 12, color: "var(--dsh-mem-danger)" }, children: err }) : null,
2350
+ dl && dl.phase === "error" ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: { marginTop: 6, fontSize: 12, color: "var(--dsh-mem-danger)" }, children: "下载失败:" + (dl.error || "") }) : null,
2351
+ runtimeRow,
2352
+ applyRow,
2353
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: RSTY.block, children: [
2354
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: RSTY.title, children: "远程连接(生效值;运行时覆盖优先于部署 YAML)" }),
2355
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: RSTY.note, children: "生效端点 " + (remote.baseURL || "(未配置)") + " · 模型 " + (remote.model || "(未配置)") + " · " + (remote.dimensions > 0 ? remote.dimensions + " 维" : "维度跟随部署") + (remote.apiKeySet ? " · 密钥已设置" : " · 密钥未设置") }),
2356
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: RSTY.row, children: [
2357
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: RSTY.label, children: "端点 URL" }),
2358
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2359
+ NInput,
2360
+ {
2361
+ style: RSTY.input,
2362
+ placeholder: "如 https://api.xxx/v1/embeddings 的服务基址",
2363
+ value: rBase,
2364
+ onChange: (e) => onRemoteBase(e.target.value),
2365
+ onKeyDown: (e) => {
2366
+ if (e.key === "Enter") {
2367
+ clearTimeout(baseTimer.current);
2368
+ remoteDirty.current.base = false;
2369
+ commitRemote({ embedRemoteBaseURL: rBase.trim() });
2370
+ }
2371
+ }
2372
+ }
2373
+ )
2374
+ ] }),
2375
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: RSTY.row, children: [
2376
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: RSTY.label, children: "模型名" }),
2377
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2378
+ NInput,
2379
+ {
2380
+ style: RSTY.input,
2381
+ placeholder: "如 text-embedding-3-small",
2382
+ value: rModel,
2383
+ onChange: (e) => onRemoteModel(e.target.value),
2384
+ onKeyDown: (e) => {
2385
+ if (e.key === "Enter") {
2386
+ clearTimeout(modelTimer.current);
2387
+ remoteDirty.current.model = false;
2388
+ commitRemote({ embedRemoteModel: rModel.trim() });
2389
+ }
2390
+ }
2391
+ }
2392
+ ),
2393
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: RSTY.label, children: "维度" }),
2394
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2395
+ NInput,
2396
+ {
2397
+ style: { width: 90, flexShrink: 0 },
2398
+ type: "number",
2399
+ min: 0,
2400
+ placeholder: "跟随部署",
2401
+ title: "向量维度(留空/0 = 跟随部署配置)",
2402
+ value: rDims,
2403
+ onChange: (e) => {
2404
+ remoteDirty.current.dims = true;
2405
+ setRDims(e.target.value);
2406
+ },
2407
+ onKeyDown: (e) => {
2408
+ if (e.key === "Enter") commitRemoteDims();
2409
+ },
2410
+ onBlur: commitRemoteDims
2411
+ }
2412
+ )
2413
+ ] }),
2414
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { style: RSTY.row, children: [
2415
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: RSTY.label, children: "API Key" }),
2416
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2417
+ NInput,
2418
+ {
2419
+ style: RSTY.input,
2420
+ type: "password",
2421
+ autoComplete: "new-password",
2422
+ placeholder: remote.apiKeySet ? "••••••••(已设置;输入新值提交可覆盖)" : "远程服务需要密钥时填写",
2423
+ value: rKey,
2424
+ onChange: (e) => setRKey(e.target.value),
2425
+ onKeyDown: (e) => {
2426
+ if (e.key === "Enter") commitRemoteKey();
2427
+ },
2428
+ onBlur: commitRemoteKey
2429
+ }
2430
+ ),
2431
+ remote.apiKeySet ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: RSTY.chipAccent, children: "已设置" }) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { style: RSTY.chipMuted, children: "未设置" }),
2432
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2433
+ NButton,
2434
+ {
2435
+ title: "写入空串清除运行时密钥(部署 YAML 的密钥不受影响)",
2436
+ onClick: () => commitRemote({ embedRemoteApiKey: "" }),
2437
+ children: "清除"
2438
+ }
2439
+ )
2440
+ ] })
2441
+ ] }),
2442
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { style: S.panelLabel, children: "本地模型目录(下载后离线可用,不随插件分发)" }),
2443
+ modelCards
2444
+ ] });
2445
+ }
2446
+
2447
+ // client/src/tabs/RebuildPanel.tsx
2448
+ var import_react10 = require("react");
2449
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2450
+ var RB_PHASE_LABEL = {
2451
+ preparing: "准备中(归档旧数据 · 清空检索库)",
2452
+ distilling: "分块蒸馏中",
2453
+ finalizing: "收尾(强制 L2 场景 + L3 画像)"
2454
+ };
2455
+ function RebuildPanel(props) {
2456
+ const rpc = props.rpc;
2457
+ const [rbRaw, setRb] = (0, import_react10.useState)(null);
2458
+ const [confirmOpen, setConfirmOpen] = (0, import_react10.useState)(false);
2459
+ const [busy, setBusy] = (0, import_react10.useState)(false);
2460
+ const [rbError, setRbError] = (0, import_react10.useState)(null);
2461
+ const refresh = (0, import_react10.useCallback)(() => {
2462
+ rpc("dsh-memory/rebuild-status", {}).then((r) => {
2463
+ if (r && r.ok) setRb(r.value);
2464
+ }).catch(() => {
2465
+ });
2466
+ }, [rpc]);
2467
+ (0, import_react10.useEffect)(() => {
2468
+ refresh();
2469
+ }, [refresh]);
2470
+ const running = !!(rbRaw && rbRaw.running);
2471
+ (0, import_react10.useEffect)(() => {
2472
+ if (!running) return;
2473
+ const timer = setInterval(refresh, 1500);
2474
+ return () => {
2475
+ clearInterval(timer);
2476
+ };
2477
+ }, [running, refresh]);
2478
+ if (!rbRaw || rbRaw.supported === false) return null;
2479
+ const rb = rbRaw;
2480
+ ensureThemeStyle();
2481
+ const start = () => {
2482
+ setBusy(true);
2483
+ setRbError(null);
2484
+ rpc("dsh-memory/rebuild-start", {}).then((r) => {
2485
+ setBusy(false);
2486
+ if (r && r.ok) {
2487
+ setConfirmOpen(false);
2488
+ setRb(r.value);
2489
+ } else {
2490
+ setRbError(r && r.error ? r.error.message : "启动失败");
2491
+ }
2492
+ }).catch((e) => {
2493
+ setBusy(false);
2494
+ setRbError(String(e && e.message || e));
2495
+ });
2496
+ };
2497
+ const cancel = () => {
2498
+ setBusy(true);
2499
+ rpc("dsh-memory/rebuild-cancel", {}).then((r) => {
2500
+ setBusy(false);
2501
+ if (r && r.ok) setRb(r.value);
2502
+ }).catch(() => {
2503
+ setBusy(false);
2504
+ });
2505
+ };
2506
+ const empty = !running && (rb.messageCount === 0 || rb.estCalls === 0);
2507
+ const pct = rb.total > 0 ? Math.round(rb.done / rb.total * 100) : 0;
2508
+ let lastNote = null;
2509
+ if (!running && rb.phase === "done") {
2510
+ lastNote = "上次重建:完成(" + rb.done + "/" + rb.total + " 会话,产出 " + rb.recordsBuilt + " 条记录)" + (rb.finishedAt ? " · " + fmtTime(new Date(rb.finishedAt).toISOString()) : "");
2511
+ } else if (!running && rb.phase === "cancelled") {
2512
+ lastNote = "上次重建:已取消(完成 " + rb.done + "/" + rb.total + " 会话,已重建部分保留)";
2513
+ } else if (!running && rb.phase === "failed") {
2514
+ lastNote = "上次重建:失败:" + (rb.error || "未知错误");
2515
+ }
2516
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "dsh-mem-rb-card", children: [
2517
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }, children: [
2518
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { fontWeight: 600, whiteSpace: "nowrap" }, children: "重建记忆" }),
2519
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "dsh-mem-rb-muted", style: { flex: 1, minWidth: 180 }, children: running ? (RB_PHASE_LABEL[rb.phase] || rb.phase) + " · " + rb.done + "/" + rb.total + " 会话(" + pct + "%)" : "从 L0 原始对话重新蒸馏 L1/L2/L3;旧数据先归档(不删除)" }),
2520
+ running ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(NButton, { disabled: busy || rb.cancelRequested, onClick: cancel, children: rb.cancelRequested ? "取消中…" : "取消重建" }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2521
+ NButton,
2522
+ {
2523
+ disabled: busy || empty,
2524
+ title: empty ? "L0 无消息,无可重建内容" : "重新蒸馏全部记忆",
2525
+ style: { color: "var(--dsh-mem-danger)" },
2526
+ onClick: () => {
2527
+ setConfirmOpen(true);
2528
+ },
2529
+ children: busy ? "…" : "开始重建"
2530
+ }
2531
+ )
2532
+ ] }),
2533
+ running ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10, marginTop: 10 }, children: [
2534
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "dsh-mem-rb-bar", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "dsh-mem-rb-fill", style: { width: pct + "%" } }) }),
2535
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "dsh-mem-rb-muted", style: { whiteSpace: "nowrap" }, children: "产出 " + rb.recordsBuilt + " 条" })
2536
+ ] }) : null,
2537
+ lastNote ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "dsh-mem-rb-muted", style: { marginTop: 8 }, children: lastNote }) : null,
2538
+ rbError ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsh-mem-danger)" }, children: rbError }) : null,
2539
+ confirmOpen ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2540
+ NModal,
2541
+ {
2542
+ open: true,
2543
+ onClose: () => {
2544
+ setConfirmOpen(false);
2545
+ },
2546
+ title: "确认重建全部记忆?",
2547
+ footer: [
2548
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2549
+ NButton,
2550
+ {
2551
+ onClick: () => {
2552
+ setConfirmOpen(false);
2553
+ },
2554
+ children: "取消"
2555
+ },
2556
+ "cancel"
2557
+ ),
2558
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(NButton, { variant: "primary", disabled: busy, onClick: start, children: busy ? "启动中…" : "开始重建" }, "confirm")
2559
+ ],
2560
+ children: [
2561
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { children: [
2562
+ "将以 L0 原始对话为事实源重新蒸馏:",
2563
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("b", { children: rb.sessionCount + " 个会话 · " + rb.messageCount + " 条消息" }),
2564
+ ",预计 ≥" + rb.estCalls + " 次蒸馏调用。"
2565
+ ] }),
2566
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { style: { marginTop: 8 }, children: "现有 L1 记忆 / L2 场景 / L3 画像会整体归档(*.bak.时间戳,可手工找回),随后清空重建;重建期间可正常对话,新对话的蒸馏优先进行;中途可取消,已重建部分保留。" })
2567
+ ]
2568
+ }
2569
+ ) : null
2570
+ ] });
2571
+ }
2572
+
2573
+ // client/src/tabs/RuminatePanel.tsx
2574
+ var import_react11 = require("react");
2575
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2576
+ var RM_PHASE_LABEL = {
2577
+ refreshing: "轻量刷新中",
2578
+ distilling: "L1 蒸馏中",
2579
+ consolidating: "L2 场景整合中",
2580
+ updating: "L3 画像更新中"
2581
+ };
2582
+ var RM_RUNNING_PHASES = ["refreshing", "distilling", "consolidating", "updating"];
2583
+ function RuminatePanel(props) {
2584
+ const rpc = props.rpc;
2585
+ const [rmRaw, setRm] = (0, import_react11.useState)(null);
2586
+ const [confirmOpen, setConfirmOpen] = (0, import_react11.useState)(false);
2587
+ const [busy, setBusy] = (0, import_react11.useState)(false);
2588
+ const [rmError, setRmError] = (0, import_react11.useState)(null);
2589
+ const refresh = (0, import_react11.useCallback)(() => {
2590
+ rpc("dsh-memory/ruminate-status", {}).then((r) => {
2591
+ if (r && r.ok) setRm(r.value);
2592
+ }).catch(() => {
2593
+ });
2594
+ }, [rpc]);
2595
+ (0, import_react11.useEffect)(() => {
2596
+ refresh();
2597
+ }, [refresh]);
2598
+ const running = !!(rmRaw && (rmRaw.running || RM_RUNNING_PHASES.indexOf(rmRaw.phase) >= 0));
2599
+ (0, import_react11.useEffect)(() => {
2600
+ if (!running) return;
2601
+ const timer = setInterval(refresh, 1500);
2602
+ return () => clearInterval(timer);
2603
+ }, [running, refresh]);
2604
+ const [, setTick] = (0, import_react11.useState)(0);
2605
+ (0, import_react11.useEffect)(() => {
2606
+ if (!running) return;
2607
+ const timer = setInterval(() => setTick((n) => n + 1), 1e3);
2608
+ return () => clearInterval(timer);
2609
+ }, [running]);
2610
+ if (!rmRaw || rmRaw.supported === false) return null;
2611
+ const rm = rmRaw;
2612
+ ensureThemeStyle();
2613
+ const start = () => {
2614
+ setBusy(true);
2615
+ setRmError(null);
2616
+ rpc("dsh-memory/ruminate-start", {}).then((r) => {
2617
+ setBusy(false);
2618
+ if (r && r.ok) {
2619
+ setConfirmOpen(false);
2620
+ setRm(r.value);
2621
+ } else {
2622
+ setRmError(r && r.error ? r.error.message : "启动失败");
2623
+ }
2624
+ }).catch((e) => {
2625
+ setBusy(false);
2626
+ setRmError(String(e && e.message || e));
2627
+ });
2628
+ };
2629
+ const cancel = () => {
2630
+ setBusy(true);
2631
+ rpc("dsh-memory/ruminate-cancel", {}).then((r) => {
2632
+ setBusy(false);
2633
+ if (r && r.ok) setRm(r.value);
2634
+ }).catch(() => {
2635
+ setBusy(false);
2636
+ });
2637
+ };
2638
+ const pct = rm.total > 0 ? Math.round(rm.done / rm.total * 100) : 0;
2639
+ const idleLike = !running;
2640
+ const elapsed = running && rm.startedAt ? Math.max(0, Date.now() - rm.startedAt) : 0;
2641
+ const elapsedText = elapsed > 0 ? elapsed >= 6e4 ? Math.floor(elapsed / 6e4) + "分" + Math.floor(elapsed % 6e4 / 1e3) + "秒" : Math.floor(elapsed / 1e3) + "秒" : "";
2642
+ let lastNote = null;
2643
+ if (idleLike && rm.phase === "done") {
2644
+ lastNote = "上次反刍整理:完成(" + rm.done + "/" + rm.total + " 会话,产出 " + rm.recordsBuilt + " 条记录)" + (rm.finishedAt ? " · " + fmtTime(new Date(rm.finishedAt).toISOString()) : "");
2645
+ } else if (idleLike && rm.phase === "cancelled") {
2646
+ lastNote = "上次反刍整理:已取消(完成 " + rm.done + "/" + rm.total + " 会话,已蒸馏部分保留)";
2647
+ } else if (idleLike && rm.phase === "failed") {
2648
+ lastNote = "上次反刍整理:失败:" + (rm.error || "未知错误");
2649
+ }
2650
+ const unit = rm.phase === "refreshing" ? "步" : "会话";
2651
+ const progressText = rm.total > 0 ? rm.done + "/" + rm.total + " " + unit + "(" + pct + "%)" : "进行中…";
2652
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "dsh-mem-rb-card", children: [
2653
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }, children: [
2654
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { fontWeight: 600, whiteSpace: "nowrap" }, children: "反刍整理" }),
2655
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "dsh-mem-rb-muted", style: { flex: 1, minWidth: 180 }, children: running ? (RM_PHASE_LABEL[rm.phase] || rm.phase) + " · " + progressText + (elapsedText ? " · 已用 " + elapsedText : "") : "冲刷未蒸馏缓冲,跑一轮 L1→L2→L3 消化;不清库不改 L0" }),
2656
+ running ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(NButton, { disabled: busy || rm.cancelRequested, onClick: cancel, children: rm.cancelRequested ? "取消中…" : "取消整理" }) : /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2657
+ NButton,
2658
+ {
2659
+ disabled: busy,
2660
+ title: "消化未蒸馏的记忆缓冲",
2661
+ style: { color: "var(--dsh-mem-accent)" },
2662
+ onClick: () => {
2663
+ setConfirmOpen(true);
2664
+ },
2665
+ children: busy ? "…" : "反刍整理"
2666
+ }
2667
+ )
2668
+ ] }),
2669
+ running ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 10, marginTop: 10 }, children: [
2670
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "dsh-mem-rb-bar", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "dsh-mem-rb-fill", style: { width: (rm.total > 0 ? pct : 100) + "%" } }) }),
2671
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "dsh-mem-rb-muted", style: { whiteSpace: "nowrap" }, children: "产出 " + rm.recordsBuilt + " 条" })
2672
+ ] }) : null,
2673
+ running && rm.detail ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "dsh-mem-rb-muted", style: { marginTop: 6, whiteSpace: "normal" }, children: "当前:" + rm.detail }) : null,
2674
+ lastNote ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "dsh-mem-rb-muted", style: { marginTop: 8 }, children: lastNote }) : null,
2675
+ rmError ? /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { marginTop: 8, fontSize: 12, color: "var(--dsh-mem-danger)" }, children: rmError }) : null,
2676
+ confirmOpen ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
2677
+ NModal,
2678
+ {
2679
+ open: true,
2680
+ onClose: () => setConfirmOpen(false),
2681
+ title: "确认反刍整理?",
2682
+ footer: [
2683
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
2684
+ NButton,
2685
+ {
2686
+ onClick: () => setConfirmOpen(false),
2687
+ children: "取消"
2688
+ },
2689
+ "cancel"
2690
+ ),
2691
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(NButton, { variant: "primary", disabled: busy, onClick: start, children: busy ? "启动中…" : "反刍整理" }, "confirm")
2692
+ ],
2693
+ children: [
2694
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { children: "将扫描未蒸馏缓冲,按会话逐个跑蒸馏管线,完成后强制 L2 场景整合与 L3 画像更新。" }),
2695
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { style: { marginTop: 8 }, children: "与重建不同:不清空 L1 检索库、不归档旧产物、不改 L0;仅消化攒而未蒸馏的切片,无缓冲时做轻量 L2/L3 刷新。" })
2696
+ ]
2697
+ }
2698
+ ) : null
2699
+ ] });
2700
+ }
2701
+
2702
+ // client/src/tabs/OverviewTab.tsx
2703
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2704
+ function OverviewTab(props) {
2705
+ const rpc = props.rpc;
2706
+ const [stats, setStats] = (0, import_react12.useState)(null);
2707
+ const [settingsData, setSettingsData] = (0, import_react12.useState)(null);
2708
+ const [error, setError] = (0, import_react12.useState)(null);
2709
+ const load = (0, import_react12.useCallback)(() => {
2710
+ rpc("dsh-memory/stats", {}).then((r) => {
2711
+ if (r && r.ok) setStats(r.value);
2712
+ else setError(r && r.error ? r.error.message : "RPC error");
2713
+ }).catch((e) => {
2714
+ setError(String(e && e.message || e));
2715
+ });
2716
+ rpc("dsh-memory/settings-get", {}).then((r) => {
2717
+ if (r && r.ok) setSettingsData(r.value);
2718
+ }).catch(() => {
2719
+ });
2720
+ }, [rpc]);
2721
+ (0, import_react12.useEffect)(() => {
2722
+ load();
2723
+ const timer = setInterval(load, 5e3);
2724
+ return () => {
2725
+ clearInterval(timer);
2726
+ };
2727
+ }, [load]);
2728
+ const toggle = (key, value) => {
2729
+ if (!settingsData) return;
2730
+ const prev = settingsData;
2731
+ const patch = { [key]: value };
2732
+ const next = { ...prev, settings: { ...prev.settings, [key]: value } };
2733
+ setSettingsData(next);
2734
+ rpc("dsh-memory/settings-set", patch).then((r) => {
2735
+ if (!r || !r.ok) {
2736
+ setSettingsData(prev);
2737
+ setError(r && r.error ? "开关写入失败:" + r.error.message : "开关写入失败");
2738
+ } else {
2739
+ setError(null);
2740
+ }
2741
+ }).catch((e) => {
2742
+ setSettingsData(prev);
2743
+ setError("开关写入失败:" + String(e && e.message || e));
2744
+ });
2745
+ };
2746
+ const tiles = [];
2747
+ const infos = [];
2748
+ if (stats) {
2749
+ const th = stats.thresholds || { l2MinNewMemories: 5, l3Interval: 20 };
2750
+ tiles.push({ num: String(stats.l0Today), label: "L0 今日消息" });
2751
+ tiles.push({ num: String(stats.l1Count), label: "L1 原子记忆" });
2752
+ tiles.push({ num: String(stats.sceneCount), label: "L2 场景块" });
2753
+ tiles.push({ num: String(stats.pendingExtract), label: "待重试消息" });
2754
+ infos.push(["数据目录", stats.dataDir]);
2755
+ infos.push(["插件版本", "v" + stats.version]);
2756
+ infos.push(["默认档", modeLabel(stats.family)]);
2757
+ infos.push(["L1 累计抽取", String(stats.l1TotalExtracted)]);
2758
+ infos.push(["L3 画像", stats.personaChars > 0 ? stats.personaChars + " 字符" : "未生成"]);
2759
+ infos.push(["上次 L1 抽取", fmtTime(stats.lastExtractAt)]);
2760
+ infos.push(["上次 L2 整合", fmtTime(stats.lastL2At)]);
2761
+ infos.push(["上次 L3 蒸馏", fmtTime(stats.lastL3At)]);
2762
+ infos.push(["待 L2 新记忆", stats.memoriesSinceL2 + " / " + (th.l2MinNewMemories != null ? th.l2MinNewMemories : 5)]);
2763
+ infos.push(["待 L3 新记忆", stats.memoriesSinceL3 + " / " + (th.l3Interval != null ? th.l3Interval : 20)]);
2764
+ }
2765
+ const degraded = stats && stats.message && stats.message !== "running";
2766
+ const master = settingsData && settingsData.settings ? settingsData.settings.enabled : true;
2767
+ let ceilingNote = "";
2768
+ if (settingsData && settingsData.ceilings) {
2769
+ const off = [];
2770
+ if (!settingsData.ceilings.capture) off.push("捕获");
2771
+ if (!settingsData.ceilings.distill) off.push("蒸馏");
2772
+ if (!settingsData.ceilings.recall) off.push("召回");
2773
+ if (off.length > 0) ceilingNote = "注意:部署配置已停用 " + off.join("、") + "(运行时开关无法开启)";
2774
+ }
2775
+ const mutate = settingsData && settingsData.settings ? !!settingsData.settings.memoryMutate : false;
2776
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
2777
+ settingsData && settingsData.supported === false ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: S.hint, children: "settings 服务不可用,记忆模式开关未启用(记忆保持全开)。" }) : settingsData ? /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: S.switchPanel, children: [
2778
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: S.panelLabel, children: "记忆模式" }),
2779
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2780
+ SwitchRow,
2781
+ {
2782
+ label: "记忆模式",
2783
+ desc: master ? "已开启:捕获对话并蒸馏记忆" : "已关闭:不捕获、不蒸馏、不注入(数据保留)",
2784
+ checked: master,
2785
+ onChange: (v) => {
2786
+ toggle("enabled", v);
2787
+ }
2788
+ }
2789
+ ),
2790
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2791
+ SwitchRow,
2792
+ {
2793
+ label: "捕获",
2794
+ desc: "L0:记录原始对话(关闭后蒸馏也无输入)",
2795
+ checked: settingsData.settings.capture,
2796
+ disabled: !master,
2797
+ onChange: (v) => {
2798
+ toggle("capture", v);
2799
+ }
2800
+ }
2801
+ ),
2802
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2803
+ SwitchRow,
2804
+ {
2805
+ label: "蒸馏",
2806
+ desc: "L1 抽取 + L2 场景 + L3 画像",
2807
+ checked: settingsData.settings.distill,
2808
+ disabled: !master,
2809
+ onChange: (v) => {
2810
+ toggle("distill", v);
2811
+ }
2812
+ }
2813
+ ),
2814
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2815
+ SwitchRow,
2816
+ {
2817
+ label: "召回",
2818
+ desc: "对话时注入相关记忆与画像",
2819
+ checked: settingsData.settings.recall,
2820
+ disabled: !master,
2821
+ onChange: (v) => {
2822
+ toggle("recall", v);
2823
+ }
2824
+ }
2825
+ ),
2826
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: S.panelLabel, children: "高权限模式" }),
2827
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2828
+ SwitchRow,
2829
+ {
2830
+ label: "高权限模式",
2831
+ desc: mutate ? "已开启:模型获得记忆写入/删除工具,记忆 Tab 可删除指定记忆" : "默认关闭;开启后模型获得写删记忆工具,面板解锁记忆删除",
2832
+ checked: mutate,
2833
+ onChange: (v) => {
2834
+ toggle("memoryMutate", v);
2835
+ }
2836
+ }
2837
+ ),
2838
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: S.panelLabel, children: "蒸馏参数" }),
2839
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(DistillSettings, { rpc, disabled: !master, data: settingsData, setData: setSettingsData, onError: setError }),
2840
+ ceilingNote ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: S.hint, children: ceilingNote }) : null
2841
+ ] }) : null,
2842
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(EmbeddingSection, { rpc }),
2843
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(RebuildPanel, { rpc }),
2844
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(RuminatePanel, { rpc }),
2845
+ degraded ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: { ...S.error, marginBottom: 10 }, children: "⚠ " + stats.message + "。上方数据为最后一次成功读取的值,记忆功能当前未工作。" }) : null,
2846
+ error ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: S.error, children: "获取状态失败:" + error }) : !stats ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: S.intro, children: "正在读取记忆状态…" }) : /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { children: [
2847
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: S.panelLabel, children: "记忆概况" }),
2848
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: S.statGrid, children: tiles.map((t) => {
2849
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "dsh-mem-card", style: S.statTile, children: [
2850
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: S.statNum, children: t.num }),
2851
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: S.statLabel, children: t.label })
2852
+ ] }, t.label);
2853
+ }) }),
2854
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { style: S.panelLabel, children: "运行状态" }),
2855
+ infos.map((row2) => {
2856
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { style: S.infoRow, children: [
2857
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: S.infoKey, children: row2[0] }),
2858
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { style: S.infoVal, children: row2[1] })
2859
+ ] }, row2[0]);
2860
+ })
2861
+ ] }),
2862
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { style: S.hint, children: "浏览各层记忆内容请切换上方 Tab;原始对话(L0)不入浏览器,可由模型侧 conversation_search 工具查询。" })
2863
+ ] });
2864
+ }
2865
+
2866
+ // client/src/tabs/PersonaTab.tsx
2867
+ var import_react13 = require("react");
2868
+ var import_jsx_runtime13 = require("react/jsx-runtime");
2869
+ function PersonaTab(props) {
2870
+ const rpc = props.rpc;
2871
+ const [content, setContent] = (0, import_react13.useState)(null);
2872
+ const [error, setError] = (0, import_react13.useState)(null);
2873
+ const load = (0, import_react13.useCallback)(() => {
2874
+ setError(null);
2875
+ rpc("dsh-memory/persona", {}).then((r) => {
2876
+ if (r && r.ok) setContent(r.value.content);
2877
+ else setError(r && r.error ? r.error.message : "RPC error");
2878
+ }).catch((e) => {
2879
+ setError(String(e && e.message || e));
2880
+ });
2881
+ }, [rpc]);
2882
+ (0, import_react13.useEffect)(() => {
2883
+ load();
2884
+ }, [load]);
2885
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { children: [
2886
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
2887
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { style: S.muted, children: error ? "加载失败" : content === null ? "加载中…" : content ? content.length + " 字符" : "未生成画像" }),
2888
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { style: S.grow }),
2889
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(NButton, { onClick: load, children: "刷新" })
2890
+ ] }),
2891
+ error ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { style: { ...S.error, marginBottom: 10 }, children: "画像读取失败:" + error + "(点右上“刷新”重试)" }) : null,
2892
+ content ? /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("pre", { style: S.pre, children: content }) : content === null ? null : /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("p", { style: S.intro, children: "画像尚未生成;蒸馏若干记忆后 L3 会自动产出。" })
2893
+ ] });
2894
+ }
2895
+
2896
+ // client/src/tabs/RecordsTab.tsx
2897
+ var import_react14 = require("react");
2898
+ var import_jsx_runtime14 = require("react/jsx-runtime");
2899
+ var TYPE_CHOICES = [
2900
+ "persona",
2901
+ "episodic",
2902
+ "instruction",
2903
+ "work_fact",
2904
+ "work_task",
2905
+ "work_method",
2906
+ "work_artifact"
2907
+ ];
2908
+ var HALL_CHOICES = [
2909
+ { id: "work", label: "工作" },
2910
+ { id: "relationships", label: "人际关系" },
2911
+ { id: "general", label: "通用" },
2912
+ { id: "finance", label: "财务" },
2913
+ { id: "journey", label: "旅程" }
2914
+ ];
2915
+ var HALL_LABEL = Object.fromEntries(HALL_CHOICES.map((h) => [h.id, h.label]));
2916
+ var DELETE_LIMIT = 200;
2917
+ function RecordsTab(props) {
2918
+ const rpc = props.rpc;
2919
+ const limit = 50;
2920
+ const [items, setItems] = (0, import_react14.useState)([]);
2921
+ const [hasMore, setHasMore] = (0, import_react14.useState)(false);
2922
+ const [total, setTotal] = (0, import_react14.useState)(null);
2923
+ const [sceneOptions, setSceneOptions] = (0, import_react14.useState)([]);
2924
+ const [loading, setLoading] = (0, import_react14.useState)(false);
2925
+ const [truncated, setTruncated] = (0, import_react14.useState)(false);
2926
+ const [error, setError] = (0, import_react14.useState)(null);
2927
+ const [expandedId, setExpandedId] = (0, import_react14.useState)(null);
2928
+ const [sel, setSel] = (0, import_react14.useState)(/* @__PURE__ */ new Set());
2929
+ const [hiPriv, setHiPriv] = (0, import_react14.useState)(false);
2930
+ const [hiPrivBusy, setHiPrivBusy] = (0, import_react14.useState)(false);
2931
+ const [query, setQuery] = (0, import_react14.useState)("");
2932
+ const [typeFilter, setTypeFilter] = (0, import_react14.useState)("");
2933
+ const [sceneFilter, setSceneFilter] = (0, import_react14.useState)("");
2934
+ const [hallFilter, setHallFilter] = (0, import_react14.useState)("");
2935
+ const [last, setLast] = (0, import_react14.useState)({ query: "", type: "", scene: "", hall: "" });
2936
+ const seqRef = (0, import_react14.useRef)(0);
2937
+ const fetchPage = (0, import_react14.useCallback)(
2938
+ (conds, offset, append) => {
2939
+ setLoading(true);
2940
+ setError(null);
2941
+ const token = ++seqRef.current;
2942
+ const payload = { limit, offset };
2943
+ if (conds.query) payload.query = conds.query;
2944
+ if (conds.type) payload.type = conds.type;
2945
+ if (conds.scene) payload.scene = conds.scene;
2946
+ if (conds.hall) payload.hall = conds.hall;
2947
+ rpc("dsh-memory/list-records", payload).then((r) => {
2948
+ if (token !== seqRef.current) return;
2949
+ setLoading(false);
2950
+ if (!r || !r.ok) {
2951
+ setError(r && r.error ? r.error.message : "RPC error");
2952
+ return;
2953
+ }
2954
+ const v = r.value;
2955
+ setItems((prev) => append ? prev.concat(v.items) : v.items);
2956
+ if (!append) setSel(/* @__PURE__ */ new Set());
2957
+ setHasMore(!!v.hasMore);
2958
+ setTotal(v.total === void 0 || v.total === null ? null : v.total);
2959
+ setTruncated(!!v.truncated);
2960
+ if (v.scenes) setSceneOptions(v.scenes);
2961
+ }).catch((e) => {
2962
+ if (token !== seqRef.current) return;
2963
+ setLoading(false);
2964
+ setError(String(e && e.message || e));
2965
+ });
2966
+ },
2967
+ [rpc]
2968
+ );
2969
+ const search = () => {
2970
+ const conds = { query: query.trim(), type: typeFilter, scene: sceneFilter, hall: hallFilter };
2971
+ setLast(conds);
2972
+ fetchPage(conds, 0, false);
2973
+ };
2974
+ (0, import_react14.useEffect)(() => {
2975
+ fetchPage({ query: "", type: "", scene: "", hall: "" }, 0, false);
2976
+ }, [fetchPage]);
2977
+ const loadHiPriv = (0, import_react14.useCallback)(() => {
2978
+ rpc("dsh-memory/settings-get", {}).then((r) => {
2979
+ if (r && r.ok && r.value) setHiPriv(!!r.value.settings.memoryMutate);
2980
+ }).catch(() => {
2981
+ });
2982
+ }, [rpc]);
2983
+ (0, import_react14.useEffect)(() => {
2984
+ loadHiPriv();
2985
+ }, [loadHiPriv]);
2986
+ const toggleHiPriv = () => {
2987
+ const next = !hiPriv;
2988
+ if (next && !window.confirm("开启高权限模式:模型获得写入/删除记忆工具,记忆库可删除指定记忆。确定开启?")) return;
2989
+ setHiPrivBusy(true);
2990
+ rpc("dsh-memory/settings-set", { memoryMutate: next }).then((r) => {
2991
+ if (r && r.ok) setHiPriv(next);
2992
+ else if (r) setError(r.error ? r.error.message : "切换高权限模式失败");
2993
+ setHiPrivBusy(false);
2994
+ loadHiPriv();
2995
+ }).catch((e) => {
2996
+ setHiPrivBusy(false);
2997
+ setError(String(e && e.message || e));
2998
+ });
2999
+ };
3000
+ const toggleSel = (id) => {
3001
+ setSel((prev) => {
3002
+ const next = new Set(prev);
3003
+ if (next.has(id)) next.delete(id);
3004
+ else next.add(id);
3005
+ return next;
3006
+ });
3007
+ };
3008
+ const deleteSelected = () => {
3009
+ const ids = Array.from(sel);
3010
+ if (ids.length === 0) return;
3011
+ if (!hiPriv) {
3012
+ setError("高权限模式未开启:请在右上「高权限:关」或概览页开关中开启后,再删除记忆。");
3013
+ return;
3014
+ }
3015
+ if (ids.length > DELETE_LIMIT) {
3016
+ setError("一次最多删除 " + DELETE_LIMIT + " 条(当前勾选 " + ids.length + " 条),请分批操作。");
3017
+ return;
3018
+ }
3019
+ if (!window.confirm("删除勾选的 " + ids.length + " 条记忆?本操作不可逆(完整重建可能从 L0 复活,为已知边界)。")) return;
3020
+ rpc("dsh-memory/records-delete", { ids }).then((r) => {
3021
+ if (r && r.ok) {
3022
+ setSel(/* @__PURE__ */ new Set());
3023
+ if (expandedId && ids.indexOf(expandedId) >= 0) setExpandedId(null);
3024
+ fetchPage(last, 0, false);
3025
+ } else if (r) setError(r.error ? r.error.message : "删除失败");
3026
+ }).catch((e) => setError(String(e && e.message || e)));
3027
+ };
3028
+ const deleteRecord = (id) => {
3029
+ if (!window.confirm("删除该条记忆?本操作不可逆(完整重建可能从 L0 复活,为已知边界)。")) return;
3030
+ rpc("dsh-memory/records-delete", { ids: [id] }).then((r) => {
3031
+ if (r && r.ok) {
3032
+ setSel((prev) => {
3033
+ const next = new Set(prev);
3034
+ next.delete(id);
3035
+ return next;
3036
+ });
3037
+ if (expandedId === id) setExpandedId(null);
3038
+ fetchPage(last, 0, false);
3039
+ } else if (r) setError(r.error ? r.error.message : "删除失败");
3040
+ }).catch((e) => setError(String(e && e.message || e)));
3041
+ };
3042
+ const countText = total !== null ? "共 " + total + " 条" : items.length + " 条" + (hasMore ? "+" : "");
3043
+ const selCount = sel.size;
3044
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { children: [
3045
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: S.toolbar, children: [
3046
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3047
+ NInput,
3048
+ {
3049
+ style: { flex: 1, minWidth: 160 },
3050
+ placeholder: "搜索记忆内容(BM25 关键词)…",
3051
+ value: query,
3052
+ onChange: (e) => {
3053
+ setQuery(e.target.value);
3054
+ },
3055
+ onKeyDown: (e) => {
3056
+ if (e.key === "Enter") search();
3057
+ }
3058
+ }
3059
+ ),
3060
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3061
+ NSel,
3062
+ {
3063
+ style: { maxWidth: 200 },
3064
+ options: [{ id: "", label: "全部类型" }].concat(
3065
+ TYPE_CHOICES.map((t) => {
3066
+ return { id: t, label: TYPE_LABELS[t] || t };
3067
+ })
3068
+ ),
3069
+ value: typeFilter,
3070
+ onChange: setTypeFilter
3071
+ }
3072
+ ),
3073
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3074
+ NSel,
3075
+ {
3076
+ style: { maxWidth: 220 },
3077
+ options: [{ id: "", label: "全部情境" }].concat(
3078
+ sceneOptions.map((s) => {
3079
+ return { id: s, label: s.length > 24 ? s.slice(0, 24) + "…" : s };
3080
+ })
3081
+ ),
3082
+ value: sceneFilter,
3083
+ onChange: setSceneFilter
3084
+ }
3085
+ ),
3086
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3087
+ NSel,
3088
+ {
3089
+ style: { maxWidth: 150 },
3090
+ options: [{ id: "", label: "全部 Hall" }].concat(
3091
+ HALL_CHOICES.map((h) => {
3092
+ return { id: h.id, label: h.label };
3093
+ })
3094
+ ),
3095
+ value: hallFilter,
3096
+ onChange: setHallFilter
3097
+ }
3098
+ ),
3099
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(NButton, { onClick: search, children: "搜索" }),
3100
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3101
+ NButton,
3102
+ {
3103
+ style: {
3104
+ color: hiPriv ? "var(--dsh-mem-danger)" : void 0,
3105
+ border: hiPriv ? "1px solid var(--dsh-mem-danger)" : void 0
3106
+ },
3107
+ disabled: hiPrivBusy,
3108
+ title: hiPriv ? "关闭高权限模式(收回模型写删工具与删除权限)" : "开启高权限模式以写删记忆",
3109
+ onClick: toggleHiPriv,
3110
+ children: hiPriv ? "高权限:开" : "高权限:关"
3111
+ }
3112
+ )
3113
+ ] }),
3114
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
3115
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { style: S.muted, children: loading ? "加载中…" : countText }),
3116
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3117
+ NButton,
3118
+ {
3119
+ style: selCount > 0 ? { color: "var(--dsh-mem-danger)" } : void 0,
3120
+ disabled: selCount === 0,
3121
+ title: hiPriv ? "删除勾选的记忆(一次最多 " + DELETE_LIMIT + " 条)" : "高权限模式未开启,删除不可用",
3122
+ onClick: deleteSelected,
3123
+ children: "删除选中" + (selCount > 0 ? "(" + selCount + ")" : "")
3124
+ }
3125
+ ),
3126
+ selCount > 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3127
+ NButton,
3128
+ {
3129
+ onClick: () => {
3130
+ setSel(/* @__PURE__ */ new Set());
3131
+ },
3132
+ children: "清空选择"
3133
+ }
3134
+ ) : null,
3135
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: S.grow }),
3136
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3137
+ NButton,
3138
+ {
3139
+ onClick: () => {
3140
+ fetchPage(last, 0, false);
3141
+ },
3142
+ children: "刷新"
3143
+ }
3144
+ )
3145
+ ] }),
3146
+ !hiPriv ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: S.hint, children: "提示:删除记忆需先开启高权限模式(右上开关或概览页「高权限模式」),关闭时删除按钮不可用。" }) : null,
3147
+ error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: S.error, children: error }) : null,
3148
+ truncated ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: S.hint, children: "搜索分页已达检索上限(200 条),更早的结果未显示。请用更精确的关键词或类型/情境过滤。" }) : null,
3149
+ items.length === 0 && !loading && !error ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { style: S.intro, children: "暂无记忆。对话几轮后,蒸馏管线会自动抽取记忆。" }) : items.map((m) => {
3150
+ const open = expandedId === m.id;
3151
+ const checked = sel.has(m.id);
3152
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
3153
+ "div",
3154
+ {
3155
+ className: "dsh-mem-card dsh-mem-card-hover",
3156
+ style: { ...S.card, cursor: "pointer", ...checked ? { borderLeft: "3px solid var(--dsh-mem-danger)" } : null },
3157
+ onClick: () => {
3158
+ setExpandedId(open ? null : m.id);
3159
+ },
3160
+ children: [
3161
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: S.cardHead, children: [
3162
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3163
+ "input",
3164
+ {
3165
+ type: "checkbox",
3166
+ checked,
3167
+ style: { margin: 0, cursor: "pointer", flexShrink: 0 },
3168
+ title: "勾选以批量删除",
3169
+ onClick: (e) => {
3170
+ e.stopPropagation();
3171
+ },
3172
+ onChange: () => {
3173
+ toggleSel(m.id);
3174
+ }
3175
+ }
3176
+ ),
3177
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "dsh-mem-tag dsh-mem-tag-" + m.type, children: TYPE_LABELS[m.type] || m.type }),
3178
+ m.hall ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "dsh-mem-tag dsh-mem-tag-work-fact", children: "Hall · " + (HALL_LABEL[m.hall] || m.hall) }) : null,
3179
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { style: S.muted, children: "优先级 " + m.priority }),
3180
+ m.score !== null && m.score !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { style: S.muted, children: "相关度 " + Number(m.score).toFixed(2) }) : null,
3181
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: S.grow }),
3182
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { style: S.muted, children: fmtTime(m.updatedAt) }),
3183
+ hiPriv ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3184
+ NButton,
3185
+ {
3186
+ style: { padding: "0 7px", minWidth: 26, height: 26, fontSize: 12, color: "var(--dsh-mem-danger)" },
3187
+ title: "删除该记忆(高权限)",
3188
+ onClick: (e) => {
3189
+ e.stopPropagation();
3190
+ deleteRecord(m.id);
3191
+ },
3192
+ children: "✕"
3193
+ }
3194
+ ) : null
3195
+ ] }),
3196
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: S.content, children: m.content }),
3197
+ open ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: S.detail, children: "id: " + m.id + "\n情境: " + (m.scene || "-") + "\n版本: v" + m.version + "(去重合并次数 " + m.version + ")\n创建: " + fmtTime(m.createdAt) + "\n活跃时间: " + (m.timestamps && m.timestamps.length > 0 ? m.timestamps.map(fmtTime).join(" → ") : "-") + "\n" + (m.sourceMessageIds && m.sourceMessageIds.length > 0 ? "来源消息: " + m.sourceMessageIds.join(", ") : "来源消息: -") }) : null
3198
+ ]
3199
+ },
3200
+ m.id
3201
+ );
3202
+ }),
3203
+ hasMore ? /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { style: S.flexRow, children: [
3204
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { style: S.grow }),
3205
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
3206
+ NButton,
3207
+ {
3208
+ disabled: loading,
3209
+ onClick: () => {
3210
+ if (!loading) fetchPage(last, items.length, true);
3211
+ },
3212
+ children: loading ? "加载中…" : "加载更多"
3213
+ }
3214
+ )
3215
+ ] }) : null
3216
+ ] });
3217
+ }
3218
+
3219
+ // client/src/tabs/ScenesTab.tsx
3220
+ var import_react15 = require("react");
3221
+ var import_jsx_runtime15 = require("react/jsx-runtime");
3222
+ function SceneCard(props) {
3223
+ const s = props.s;
3224
+ const [open, setOpen] = (0, import_react15.useState)(false);
3225
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "dsh-mem-card dsh-mem-card-hover", style: S.card, children: [
3226
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3227
+ "div",
3228
+ {
3229
+ style: { ...S.sceneHead, cursor: "pointer", userSelect: "none" },
3230
+ onClick: () => {
3231
+ setOpen(!open);
3232
+ },
3233
+ children: [
3234
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "dsh-mem-scene-chev", style: { transform: open ? "rotate(90deg)" : "none" }, children: "▸" }),
3235
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.sceneTitle, children: s.path }),
3236
+ s.heat ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "热度 " + s.heat }) : null,
3237
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.grow }),
3238
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "更新 " + fmtTime(s.updated) })
3239
+ ]
3240
+ }
3241
+ ),
3242
+ s.summary ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { ...S.muted, marginBottom: 6 }, children: s.summary }) : null,
3243
+ open ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("pre", { style: S.pre, children: s.content || "(空)" }) : null
3244
+ ] });
3245
+ }
3246
+ function ScenesTab(props) {
3247
+ const rpc = props.rpc;
3248
+ const [items, setItems] = (0, import_react15.useState)(null);
3249
+ const [error, setError] = (0, import_react15.useState)(null);
3250
+ const load = (0, import_react15.useCallback)(() => {
3251
+ rpc("dsh-memory/scenes", {}).then((r) => {
3252
+ if (r && r.ok) {
3253
+ setItems(r.value.items);
3254
+ setError(null);
3255
+ } else setError(r && r.error ? r.error.message : "RPC error");
3256
+ }).catch((e) => {
3257
+ setError(String(e && e.message || e));
3258
+ });
3259
+ }, [rpc]);
3260
+ (0, import_react15.useEffect)(() => {
3261
+ load();
3262
+ }, [load]);
3263
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { children: [
3264
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { ...S.flexRow, marginBottom: 10 }, children: [
3265
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: items ? items.length + " 个场景块" : "加载中…" }),
3266
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.grow }),
3267
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(NButton, { onClick: load, children: "刷新" })
3268
+ ] }),
3269
+ error ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.error, children: error }) : null,
3270
+ items && items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { style: S.intro, children: "暂无场景块。累计 5 条新记忆后 L2 会自动整合出第一个场景。" }) : null,
3271
+ (items || []).map((s) => {
3272
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SceneCard, { s }, s.path);
3273
+ })
3274
+ ] });
3275
+ }
3276
+
3277
+ // client/src/panel.tsx
3278
+ var import_jsx_runtime16 = require("react/jsx-runtime");
3279
+ var TABS = [
3280
+ ["overview", "概览"],
3281
+ ["records", "记忆"],
3282
+ ["scenes", "场景"],
3283
+ ["persona", "画像"],
3284
+ ["cost", "成本"],
3285
+ ["log", "日志"]
3286
+ ];
3287
+ function MemoryPanel(props) {
3288
+ const rpc = props.rpc;
3289
+ const [tab, setTab] = (0, import_react16.useState)("overview");
3290
+ ensureThemeStyle();
3291
+ (0, import_react16.useEffect)(() => {
3292
+ watchSidebarIcon();
3293
+ }, []);
3294
+ let body;
3295
+ if (tab === "overview") body = /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(OverviewTab, { rpc });
3296
+ else if (tab === "records") body = /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(RecordsTab, { rpc });
3297
+ else if (tab === "scenes") body = /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(ScenesTab, { rpc });
3298
+ else if (tab === "persona") body = /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(PersonaTab, { rpc });
3299
+ else if (tab === "cost") body = /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(CostTab, { rpc });
3300
+ else body = /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(LogTab, { rpc });
3301
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "dsh-mem-root", style: S.section, children: [
3302
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h2", { style: S.heading, children: "记忆 (Memory)" }),
3303
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("p", { style: S.intro, children: "L0~L3 分层蒸馏记忆:浏览被记住的内容,控制记忆模式开关。" }),
3304
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { style: S.tabbar, children: TABS.map((t) => {
3305
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3306
+ "button",
3307
+ {
3308
+ className: tab === t[0] ? "dsh-mem-tab dsh-mem-tab-on" : "dsh-mem-tab",
3309
+ onClick: () => {
3310
+ setTab(t[0]);
3311
+ },
3312
+ children: t[1]
3313
+ },
3314
+ t[0]
3315
+ );
3316
+ }) }),
3317
+ body
3318
+ ] });
3319
+ }
3320
+
3321
+ // client/src/pill/MemoryModePill.tsx
3322
+ var import_react19 = require("react");
3323
+
3324
+ // src/util/context-occupancy.ts
3325
+ var CONTEXT_METER_CIRCUMFERENCE = 34.55751918948772;
3326
+ function haloDashArray(occupancyRatio, circumference = CONTEXT_METER_CIRCUMFERENCE, minLen = 0) {
3327
+ const clamped = Number.isFinite(occupancyRatio) ? Math.min(1, Math.max(0, occupancyRatio)) : 0;
3328
+ const len = Math.max(clamped * circumference, clamped > 0 ? minLen : 0);
3329
+ return `${len} ${circumference}`;
3330
+ }
3331
+ var RADIUS_EPSILON = 1e-6;
3332
+ function isContextMeterAnchor(sig) {
3333
+ if (sig.ariaHasPopup !== "dialog") return false;
3334
+ if (sig.viewBox !== "0 0 14 14") return false;
3335
+ const radii = sig.circleRadii;
3336
+ if (!Array.isArray(radii) || radii.length !== 2) return false;
3337
+ return radii.every((r) => Math.abs(r - 5.5) < RADIUS_EPSILON);
3338
+ }
3339
+
3340
+ // client/src/meter/occupancy-indicator.ts
3341
+ var statsCall = null;
3342
+ var snapshotBySession = /* @__PURE__ */ new Map();
3343
+ var activeSessionId = null;
3344
+ var panelListeners = /* @__PURE__ */ new Set();
3345
+ var panelWasOpen = false;
3346
+ var snapshotListeners = /* @__PURE__ */ new Set();
3347
+ function initOccupancyIndicator(call) {
3348
+ statsCall = call;
3349
+ }
3350
+ function noteOccupancySession(sessionId) {
3351
+ if (sessionId === activeSessionId) return;
3352
+ activeSessionId = sessionId;
3353
+ panelWasOpen = false;
3354
+ if (sessionId !== null && !snapshotBySession.has(sessionId)) void fetchSnapshot(true);
3355
+ }
3356
+ function onMeterPanelOpen(listener) {
3357
+ panelListeners.add(listener);
3358
+ return () => panelListeners.delete(listener);
3359
+ }
3360
+ function onMeterSnapshotUpdate(listener) {
3361
+ snapshotListeners.add(listener);
3362
+ return () => snapshotListeners.delete(listener);
3363
+ }
3364
+ function effectiveView(snap) {
3365
+ const recall = Math.max(snap?.backfillRecallTokens ?? 0, snap?.recallTokens ?? 0);
3366
+ const ledgerProfile = snap?.profileTokens ?? 0;
3367
+ const profile = ledgerProfile > 0 ? ledgerProfile : snap?.backfillProfileTokens ?? 0;
3368
+ return { stock: recall + profile, recall, profile, window: snap?.contextWindowTokens ?? null };
3369
+ }
3370
+ function currentMeterSnapshot() {
3371
+ const sid = activeSessionId;
3372
+ if (sid === null) return null;
3373
+ const snap = snapshotBySession.get(sid);
3374
+ if (!snap) return null;
3375
+ const v = effectiveView(snap);
3376
+ return {
3377
+ stockTokens: v.stock,
3378
+ recallTokens: v.recall,
3379
+ profileTokens: v.profile,
3380
+ contextWindowTokens: v.window,
3381
+ mode: snap.mode
3382
+ };
3383
+ }
3384
+ function currentAnchor() {
3385
+ return anchorCache?.button ?? null;
3386
+ }
3387
+ var FETCH_MIN_INTERVAL_MS = 2e3;
3388
+ var FETCH_FAILURE_REMOVE_AFTER = 3;
3389
+ var lastFetchStartedAt = 0;
3390
+ var fetchInFlight = false;
3391
+ var fetchFailureStreak = 0;
3392
+ async function fetchSnapshot(force = false) {
3393
+ const sid = activeSessionId;
3394
+ if (!statsCall || !sid || fetchInFlight) return;
3395
+ if (!force && Date.now() - lastFetchStartedAt < FETCH_MIN_INTERVAL_MS) return;
3396
+ fetchInFlight = true;
3397
+ lastFetchStartedAt = Date.now();
3398
+ try {
3399
+ const res = await statsCall("dsh-memory/session-stats", { sessionId: sid });
3400
+ const v = res && res.ok ? res.value : void 0;
3401
+ if (!res || !res.ok) {
3402
+ fetchFailureStreak++;
3403
+ scheduleReconcile();
3404
+ return;
3405
+ }
3406
+ if (sid !== activeSessionId) return;
3407
+ snapshotBySession.set(sid, {
3408
+ stockTokens: v?.supported && v.memoryOccupancy ? v.memoryOccupancy.stockTokens : null,
3409
+ recallTokens: v?.supported && v.memoryOccupancy ? v.memoryOccupancy.recallTokens : null,
3410
+ profileTokens: v?.supported && v.memoryOccupancy ? v.memoryOccupancy.profileTokens : null,
3411
+ backfillRecallTokens: v?.supported ? v.occupancyBackfill?.recallTokens ?? null : null,
3412
+ backfillProfileTokens: v?.supported ? v.occupancyBackfill?.profileTokens ?? null : null,
3413
+ contextWindowTokens: v?.supported ? v.contextWindowTokens ?? null : null,
3414
+ mode: v?.supported ? v.mode ?? null : null,
3415
+ updatedAt: Date.now()
3416
+ });
3417
+ fetchFailureStreak = 0;
3418
+ for (const l of snapshotListeners) l();
3419
+ scheduleReconcile();
3420
+ } catch {
3421
+ fetchFailureStreak++;
3422
+ scheduleReconcile();
3423
+ } finally {
3424
+ fetchInFlight = false;
3425
+ }
3426
+ }
3427
+ var PARASITE_CLASS = "dsh-mem-parasite";
3428
+ var MIN_VISIBLE_ARC = 2;
3429
+ var observer = null;
3430
+ var reconcileScheduled = false;
3431
+ var anchorCache = null;
3432
+ function watchContextMeter() {
3433
+ if (observer !== null || typeof document === "undefined" || !document.body) return;
3434
+ observer = new MutationObserver(onMutations);
3435
+ observer.observe(document.body, {
3436
+ childList: true,
3437
+ subtree: true,
3438
+ attributes: true,
3439
+ attributeFilter: ["stroke-dasharray", "aria-expanded", "aria-label"]
3440
+ });
3441
+ scheduleReconcile();
3442
+ }
3443
+ function onMutations(mutations) {
3444
+ for (const m of mutations) {
3445
+ const target = m.target;
3446
+ if (target && typeof target.classList?.contains === "function" && target.classList.contains(PARASITE_CLASS)) {
3447
+ continue;
3448
+ }
3449
+ scheduleReconcile();
3450
+ return;
3451
+ }
3452
+ }
3453
+ function scheduleReconcile() {
3454
+ if (reconcileScheduled) return;
3455
+ reconcileScheduled = true;
3456
+ queueMicrotask(() => {
3457
+ reconcileScheduled = false;
3458
+ reconcile();
3459
+ });
3460
+ }
3461
+ function findAnchor() {
3462
+ const candidates = document.querySelectorAll('button[aria-haspopup="dialog"]');
3463
+ for (let i = 0; i < candidates.length; i++) {
3464
+ const button = candidates[i];
3465
+ if (!button) continue;
3466
+ const svg = button.querySelector("svg");
3467
+ if (!svg) continue;
3468
+ const circles = svg.querySelectorAll("circle");
3469
+ const radii = [];
3470
+ for (let c = 0; c < circles.length; c++) {
3471
+ const r = parseFloat(circles[c]?.getAttribute("r") ?? "");
3472
+ if (Number.isFinite(r)) radii.push(r);
3473
+ }
3474
+ if (isContextMeterAnchor({
3475
+ ariaHasPopup: button.getAttribute("aria-haspopup"),
3476
+ viewBox: svg.getAttribute("viewBox"),
3477
+ circleRadii: radii
3478
+ })) {
3479
+ return { button, svg };
3480
+ }
3481
+ }
3482
+ return null;
3483
+ }
3484
+ function reconcile() {
3485
+ if (!anchorCache || !anchorCache.button.isConnected || !anchorCache.svg.isConnected) {
3486
+ const found = document.body ? findAnchor() : null;
3487
+ anchorCache = found ? found : null;
3488
+ }
3489
+ if (!anchorCache) return;
3490
+ const open = anchorCache.button.getAttribute("aria-expanded") === "true";
3491
+ if (open !== panelWasOpen) {
3492
+ panelWasOpen = open;
3493
+ for (const l of panelListeners) l(open);
3494
+ if (open) void fetchSnapshot(true);
3495
+ }
3496
+ applyHalo();
3497
+ }
3498
+ function applyHalo() {
3499
+ const svg = anchorCache?.svg;
3500
+ if (!svg) return;
3501
+ const snap = activeSessionId !== null ? snapshotBySession.get(activeSessionId) : void 0;
3502
+ const failing = fetchFailureStreak >= FETCH_FAILURE_REMOVE_AFTER;
3503
+ const view = failing ? { stock: 0, recall: 0, profile: 0, window: null } : effectiveView(snap);
3504
+ const win = view.window;
3505
+ const ratio = win !== null && win > 0 ? Math.min(1, Math.max(0, view.stock / win)) : null;
3506
+ const existing = svg.querySelector(`circle.${PARASITE_CLASS}`);
3507
+ if (ratio === null || ratio <= 0) {
3508
+ existing?.remove();
3509
+ return;
3510
+ }
3511
+ let halo = existing;
3512
+ if (!halo) {
3513
+ halo = document.createElementNS("http://www.w3.org/2000/svg", "circle");
3514
+ halo.setAttribute("class", PARASITE_CLASS);
3515
+ halo.setAttribute("cx", "7");
3516
+ halo.setAttribute("cy", "7");
3517
+ halo.setAttribute("r", "6.4");
3518
+ halo.setAttribute("fill", "none");
3519
+ halo.setAttribute("stroke", "var(--dsh-mem-accent)");
3520
+ halo.setAttribute("stroke-width", "0.85");
3521
+ halo.setAttribute("transform", "rotate(-90 7 7)");
3522
+ halo.style.filter = "drop-shadow(0 0 3px var(--dsh-mem-accent))";
3523
+ halo.setAttribute("aria-hidden", "true");
3524
+ halo.style.pointerEvents = "none";
3525
+ svg.appendChild(halo);
3526
+ }
3527
+ halo.setAttribute(
3528
+ "stroke-dasharray",
3529
+ haloDashArray(ratio, CONTEXT_METER_CIRCUMFERENCE, MIN_VISIBLE_ARC)
3530
+ );
3531
+ const fill = svg.querySelector("circle:nth-of-type(2)");
3532
+ const offKey = fill?.getAttribute("stroke-dasharray") ?? "";
3533
+ if (svg.dataset.offKey !== void 0 && svg.dataset.offKey !== offKey) {
3534
+ void fetchSnapshot();
3535
+ }
3536
+ svg.dataset.offKey = offKey;
3537
+ }
3538
+
3539
+ // client/src/meter/panel-section.ts
3540
+ var SECTION_TAG = "dsh-mem-panel";
3541
+ var inited = false;
3542
+ var mounted = null;
3543
+ var panelOpen = false;
3544
+ function fmtTokens(n) {
3545
+ if (n >= 1e6) {
3546
+ const v = n / 1e6;
3547
+ return `${v < 100 ? v.toFixed(1) : Math.round(v)}M`;
3548
+ }
3549
+ if (n >= 1e3) {
3550
+ const v = n / 1e3;
3551
+ return `${v < 100 ? v.toFixed(1) : Math.round(v)}K`;
3552
+ }
3553
+ return String(Math.round(n));
3554
+ }
3555
+ function initPanelSection(read) {
3556
+ if (inited) return;
3557
+ inited = true;
3558
+ onMeterPanelOpen((open) => {
3559
+ panelOpen = open;
3560
+ if (!open) {
3561
+ mounted?.remove();
3562
+ mounted = null;
3563
+ return;
3564
+ }
3565
+ tryMount(read);
3566
+ });
3567
+ onMeterSnapshotUpdate(() => {
3568
+ if (panelOpen && !mounted) tryMount(read);
3569
+ });
3570
+ }
3571
+ function tryMount(read) {
3572
+ const view = read();
3573
+ if (!view || view.stockTokens === null || view.stockTokens <= 0) return;
3574
+ const anchor = currentAnchor();
3575
+ if (!anchor || document.activeElement !== anchor) return;
3576
+ const target = findDialogRoot();
3577
+ if (!target) return;
3578
+ mounted?.remove();
3579
+ mounted = renderSection(view);
3580
+ target.appendChild(mounted);
3581
+ }
3582
+ function findDialogRoot() {
3583
+ const dialogs = document.querySelectorAll('[role="dialog"]');
3584
+ for (let i = dialogs.length - 1; i >= 0; i--) {
3585
+ const el = dialogs[i];
3586
+ if (!el || !el.isConnected) continue;
3587
+ if (el.querySelector(`[data-${SECTION_TAG}]`) || el.hasAttribute(`data-${SECTION_TAG}`)) continue;
3588
+ const style = window.getComputedStyle(el);
3589
+ if (style.display === "none" || style.visibility === "hidden") continue;
3590
+ return el;
3591
+ }
3592
+ return null;
3593
+ }
3594
+ function row(dotColor, label, tokens) {
3595
+ const div = document.createElement("div");
3596
+ div.style.cssText = "display:flex;justify-content:space-between;align-items:center;padding:3px 0;font-size:12px;";
3597
+ const left = document.createElement("span");
3598
+ left.style.cssText = `display:inline-flex;align-items:center;gap:6px;color:var(--dsh-mem-text-2);`;
3599
+ const dot = document.createElement("i");
3600
+ dot.style.cssText = `width:7px;height:7px;border-radius:50%;background:${dotColor};display:inline-block;`;
3601
+ left.append(dot, document.createTextNode(label));
3602
+ const right = document.createElement("span");
3603
+ right.textContent = `~${fmtTokens(tokens)}`;
3604
+ right.style.cssText = "font-variant-numeric:tabular-nums;color:var(--dsh-mem-text-1);";
3605
+ div.append(left, right);
3606
+ return div;
3607
+ }
3608
+ function renderSection(view) {
3609
+ const section = document.createElement("section");
3610
+ section.setAttribute(`data-${SECTION_TAG}`, "");
3611
+ section.style.cssText = [
3612
+ "border-top:1px solid var(--dsh-mem-border)",
3613
+ "margin-top:6px",
3614
+ "padding-top:8px",
3615
+ "font-size:12px",
3616
+ "line-height:1.5"
3617
+ ].join(";");
3618
+ const title = document.createElement("div");
3619
+ title.textContent = "记忆占用";
3620
+ title.style.cssText = "color:var(--dsh-mem-text-2);font-weight:600;margin-bottom:4px;";
3621
+ section.append(title);
3622
+ if (view.recallTokens !== null && view.recallTokens > 0) {
3623
+ section.append(row("var(--dsh-mem-accent)", "召回片段", view.recallTokens));
3624
+ }
3625
+ if (view.profileTokens !== null && view.profileTokens > 0) {
3626
+ section.append(row("var(--dsh-mem-accent)", "记忆稳定区", view.profileTokens));
3627
+ }
3628
+ if (view.mode === "off") {
3629
+ const offNote = document.createElement("div");
3630
+ offNote.textContent = "已停用 · 显示现存残留";
3631
+ offNote.style.cssText = "color:var(--dsh-mem-text-3);padding-top:2px;";
3632
+ section.append(offNote);
3633
+ }
3634
+ return section;
3635
+ }
3636
+
3637
+ // client/src/pill/ModeSlider.tsx
3638
+ var import_react18 = require("react");
3639
+
3640
+ // client/src/pill/SessionInfoArea.tsx
3641
+ var import_react17 = require("react");
3642
+ var import_jsx_runtime17 = require("react/jsx-runtime");
3643
+ function sinfoCell(val, label, title) {
3644
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { title: title || void 0, children: [
3645
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dsh-mem-sinfo-val", children: val }),
3646
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dsh-mem-sinfo-label", children: label })
3647
+ ] });
3648
+ }
3649
+ function SessionInfoArea(props) {
3650
+ const rpc = props.rpc;
3651
+ const sessionId = props.sessionId;
3652
+ const [stats, setStats] = (0, import_react17.useState)(void 0);
3653
+ const busyRef = (0, import_react17.useRef)(false);
3654
+ (0, import_react17.useEffect)(() => {
3655
+ if (!rpc || !sessionId) return void 0;
3656
+ let alive = true;
3657
+ let timer = null;
3658
+ let seq = 0;
3659
+ const tick = () => {
3660
+ const token = ++seq;
3661
+ rpc("dsh-memory/session-stats", { sessionId }).then((r) => {
3662
+ if (!alive || token !== seq) return;
3663
+ if (r && r.ok && r.value) {
3664
+ if (r.value.supported === false) {
3665
+ setStats(null);
3666
+ } else {
3667
+ const v = r.value;
3668
+ setStats(v);
3669
+ const d = v.distill || {};
3670
+ const g = v.global || {};
3671
+ busyRef.current = (d.pendingSlice || 0) > 0 || (d.parkedSlices || 0) > 0 || (g.pendingTotal || 0) > 0;
3672
+ }
3673
+ }
3674
+ }).catch(() => {
3675
+ }).then(() => {
3676
+ if (alive) timer = setTimeout(tick, busyRef.current ? 2e3 : 5e3);
3677
+ });
3678
+ };
3679
+ tick();
3680
+ return () => {
3681
+ alive = false;
3682
+ if (timer) clearTimeout(timer);
3683
+ };
3684
+ }, [rpc, sessionId]);
3685
+ if (stats === null) return null;
3686
+ if (stats === void 0) {
3687
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dsh-mem-sinfo", children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "dsh-mem-sinfo-grid", children: [
3688
+ sinfoCell("…", "召回命中"),
3689
+ sinfoCell("…", "攒批进度"),
3690
+ sinfoCell("…", "本会话记忆"),
3691
+ sinfoCell("…", "会话消息")
3692
+ ] }) });
3693
+ }
3694
+ const rc = stats.recall || {};
3695
+ const di = stats.distill || {};
3696
+ const gl = stats.global || {};
3697
+ const isOff = stats.mode === "off";
3698
+ let rcVal;
3699
+ let rcLabel;
3700
+ let rcTitle;
3701
+ if (rc.enabled === false) {
3702
+ rcVal = "停用";
3703
+ rcLabel = "召回命中";
3704
+ const reasonText = {
3705
+ deploy: "部署未启用",
3706
+ global: "全局开关关闭",
3707
+ session: "会话只写",
3708
+ mode: "档位关闭"
3709
+ };
3710
+ rcTitle = rc.reason ? "召回已停用(" + (reasonText[rc.reason] ?? rc.reason) + ")" : "召回已停用(开关关闭 / 档位关闭 / 部署未启用)";
3711
+ } else {
3712
+ rcVal = (rc.hitTurns || 0) + "/" + (rc.injectedTurns || 0);
3713
+ rcLabel = "召回命中 · " + (rc.totalHits || 0) + " 条";
3714
+ rcTitle = "最近一轮命中 " + (rc.lastHits || 0) + " 条,耗时 " + (rc.lastDurationMs || 0) + "ms" + ((rc.timeouts || 0) > 0 ? ",超时跳过 " + rc.timeouts + " 次" : "");
3715
+ }
3716
+ let dVal;
3717
+ let dLabel;
3718
+ let dTitle;
3719
+ if (isOff) {
3720
+ dVal = String(di.parkedSlices || 0);
3721
+ dLabel = "挂起切片";
3722
+ dTitle = "档位关闭:未蒸馏切片挂起,切回档位后继续";
3723
+ } else {
3724
+ dVal = (di.pendingSlice || 0) + "/" + (di.threshold != null ? di.threshold : "-");
3725
+ dLabel = (di.parkedSlices || 0) > 0 ? "攒批 · 挂起 " + di.parkedSlices : "攒批进度";
3726
+ dTitle = "达到阈值后自动蒸馏(阈值随使用渐进爬坡到稳态)";
3727
+ }
3728
+ const pTitle = di.lastDistillAt ? "最近蒸馏 " + fmtTime(di.lastDistillAt) : "本会话尚未蒸馏";
3729
+ const warn = gl.degraded ? "⚠ 存储不可用,记忆功能已停用" : null;
3730
+ let note = null;
3731
+ if (!gl.degraded) {
3732
+ if (stats.retrieval === "keyword" && !isOff) note = "检索降级:纯关键词(向量不可用)";
3733
+ else if (stats.retrieval === "none") note = "检索不可用(FTS 与向量均失效)";
3734
+ }
3735
+ const ago = fmtAgo(gl.lastExtractAt);
3736
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "dsh-mem-sinfo", children: [
3737
+ warn ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dsh-mem-sinfo-warn", children: warn }) : null,
3738
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "dsh-mem-sinfo-grid", children: [
3739
+ sinfoCell(rcVal, rcLabel, rcTitle),
3740
+ sinfoCell(dVal, dLabel, dTitle),
3741
+ sinfoCell(String(di.producedRecords || 0), "本会话记忆", pTitle),
3742
+ sinfoCell(stats.l0Count != null ? String(stats.l0Count) : "…", "会话消息")
3743
+ ] }),
3744
+ note ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dsh-mem-sinfo-note", children: note }) : null,
3745
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "dsh-mem-sinfo-sum", children: "待蒸馏 " + (gl.pendingTotal || 0) + " · 上次蒸馏 " + (ago || "尚未蒸馏") })
3746
+ ] });
3747
+ }
3748
+
3749
+ // client/src/pill/ModeSlider.tsx
3750
+ var import_jsx_runtime18 = require("react/jsx-runtime");
3751
+ function ModeSlider(props) {
3752
+ ensureThemeStyle();
3753
+ const trackRef = (0, import_react18.useRef)(null);
3754
+ const [drag, setDrag] = (0, import_react18.useState)(null);
3755
+ const canvasRef = (0, import_react18.useRef)(null);
3756
+ const geoRef = (0, import_react18.useRef)(null);
3757
+ const clampX = (x) => {
3758
+ if (x < 0) return 0;
3759
+ if (x > INNER_W) return INNER_W;
3760
+ return x;
3761
+ };
3762
+ const xFromClientX = (clientX) => {
3763
+ const rect = trackRef.current.getBoundingClientRect();
3764
+ return clampX(clientX - rect.left - THUMB / 2);
3765
+ };
3766
+ const onPointerDown = (e) => {
3767
+ e.preventDefault();
3768
+ e.currentTarget.setPointerCapture(e.pointerId);
3769
+ setDrag({ x: xFromClientX(e.clientX), lastX: e.clientX, t: e.timeStamp, v: 0 });
3770
+ };
3771
+ const onPointerMove = (e) => {
3772
+ if (drag === null) return;
3773
+ const dt = e.timeStamp - drag.t;
3774
+ const instV = dt > 0 ? (e.clientX - drag.lastX) / dt : drag.v;
3775
+ setDrag({
3776
+ x: xFromClientX(e.clientX),
3777
+ lastX: e.clientX,
3778
+ t: e.timeStamp,
3779
+ v: drag.v * 0.7 + instV * 0.3
3780
+ // EMA:瞬时抖动不放大,松手投影用
3781
+ });
3782
+ };
3783
+ const onPointerUp = (e) => {
3784
+ if (drag === null) return;
3785
+ const projected = xFromClientX(e.clientX) + Math.max(-30, Math.min(30, drag.v * 120));
3786
+ const idx = Math.round(clampX(projected) / INNER_W * (MODES.length - 1));
3787
+ setDrag(null);
3788
+ props.onCommit(MODES[idx].key);
3789
+ };
3790
+ const thumbLeft = drag !== null ? drag.x : modeIndex(props.mode) / (MODES.length - 1) * INNER_W;
3791
+ const activeIdx = Math.min(MODES.length - 1, Math.max(0, Math.round(thumbLeft / INNER_W * (MODES.length - 1))));
3792
+ const info = MODES[activeIdx];
3793
+ geoRef.current = {
3794
+ origin: thumbLeft + THUMB / 2,
3795
+ // 密度/亮度中心 = 圆球中心
3796
+ rightEdge: thumbLeft + THUMB,
3797
+ // 粒子活动区右界 = 填充右缘(不越过圆球)
3798
+ tier: activeIdx,
3799
+ // 场强档位(与填充/气泡同源;拖拽预览即时升降级)
3800
+ show: activeIdx > 0 || drag !== null,
3801
+ // 与填充显隐同源
3802
+ dragging: drag !== null
3803
+ };
3804
+ (0, import_react18.useEffect)(() => {
3805
+ const canvas = canvasRef.current;
3806
+ if (!canvas) return void 0;
3807
+ const ctx = canvas.getContext && canvas.getContext("2d");
3808
+ if (!ctx) return void 0;
3809
+ const reduced = window.matchMedia("(prefers-reduced-motion: reduce)");
3810
+ let width = 1;
3811
+ let height = 1;
3812
+ let frame = 0;
3813
+ let grid = [];
3814
+ let cell = 5;
3815
+ const gap = 1.1;
3816
+ let fieldOn = false;
3817
+ let fieldStart = 0;
3818
+ let lastDrawn = 0;
3819
+ const resize = () => {
3820
+ const b = canvas.getBoundingClientRect();
3821
+ const ratio = Math.min(window.devicePixelRatio || 1, 2);
3822
+ width = Math.max(1, b.width);
3823
+ height = Math.max(1, b.height);
3824
+ canvas.width = Math.max(1, Math.round(width * ratio));
3825
+ canvas.height = Math.max(1, Math.round(height * ratio));
3826
+ ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
3827
+ cell = width < 280 ? 5 : 6;
3828
+ grid = [];
3829
+ for (let row2 = 0; row2 * cell < height; row2++) {
3830
+ for (let column = 0; column * cell < width; column++) {
3831
+ grid.push({
3832
+ x: column * cell,
3833
+ y: row2 * cell,
3834
+ base: Math.abs(Math.sin(column * 12.9898 + row2 * 78.233) * 43758.5453) % 1,
3835
+ tempo: Math.abs(Math.sin(column * 7.13 + row2 * 19.41) * 19341.731) % 1,
3836
+ phase: Math.abs(Math.sin(column * 31.17 + row2 * 11.93) * 28437.123) % 1
3837
+ });
3838
+ }
3839
+ }
3840
+ };
3841
+ const draw = (time) => {
3842
+ const st = geoRef.current || { origin: 0, rightEdge: 0, tier: 0, show: false, dragging: false };
3843
+ ctx.clearRect(0, 0, width, height);
3844
+ if (!st.show || st.rightEdge <= 0) {
3845
+ fieldOn = false;
3846
+ return;
3847
+ }
3848
+ if (!fieldOn) {
3849
+ fieldOn = true;
3850
+ fieldStart = time;
3851
+ }
3852
+ const dark = document.body.hasAttribute("data-ds-dark-theme");
3853
+ const tier = FIELD_TIERS[st.tier] || FIELD_TIERS[1];
3854
+ const elapsed = Math.max(0, time - fieldStart);
3855
+ const reveal = reduced.matches ? 1 : smStep(0, 1, elapsed / 900);
3856
+ const ripplePhase = elapsed % 1200 / 1200;
3857
+ const tempo = tier.tempo * (st.dragging ? 2 : 1);
3858
+ const dim = dark ? [124, 144, 250] : [61, 91, 224];
3859
+ const hot = dark ? [214, 224, 255] : [126, 148, 250];
3860
+ ctx.save();
3861
+ ctx.beginPath();
3862
+ if (ctx.roundRect) ctx.roundRect(0, 0, st.rightEdge, height, height / 2);
3863
+ else ctx.rect(0, 0, st.rightEdge, height);
3864
+ ctx.clip();
3865
+ for (let i = 0; i < grid.length; i++) {
3866
+ const c = grid[i];
3867
+ const dx = Math.abs(c.x + cell * 0.5 - st.origin) / Math.max(1, st.rightEdge * 0.5);
3868
+ if (dx > 1) continue;
3869
+ const near = Math.min(1, Math.max(0, 1 - dx * 1.1));
3870
+ if (c.base > tier.density - near * 0.3) continue;
3871
+ const flicker = 0.5 + 0.5 * Math.sin(elapsed * 0.012 * tempo + c.tempo * 6.283 + c.phase * 6.283);
3872
+ const wave = tier.wave ? 0.5 + 0.5 * Math.sin((dx * 2 - ripplePhase) * 6.283) : 0.62;
3873
+ const revealA = smStep(0, 1, reveal * (1 - dx * 0.85) + dx * 0.15);
3874
+ const alpha = Math.min(1, (0.26 + 0.44 * flicker + near * 0.28) * (0.28 + 0.72 * wave) * revealA * tier.alpha);
3875
+ if (alpha < 0.02) continue;
3876
+ const glowMix = Math.max(0, flicker * wave - 0.45) * 1.6;
3877
+ ctx.fillStyle = "rgba(" + Math.round(dim[0] + (hot[0] - dim[0]) * glowMix) + "," + Math.round(dim[1] + (hot[1] - dim[1]) * glowMix) + "," + Math.round(dim[2] + (hot[2] - dim[2]) * glowMix) + "," + alpha.toFixed(3) + ")";
3878
+ ctx.fillRect(c.x + gap * 0.5, c.y + gap * 0.5, cell - gap, cell - gap);
3879
+ }
3880
+ ctx.restore();
3881
+ };
3882
+ const loop = (time) => {
3883
+ if (time - lastDrawn >= 33) {
3884
+ lastDrawn = time;
3885
+ draw(time);
3886
+ }
3887
+ frame = window.requestAnimationFrame(loop);
3888
+ };
3889
+ const redrawStatic = () => {
3890
+ if (reduced.matches) draw(performance.now());
3891
+ };
3892
+ const ro = new ResizeObserver(() => {
3893
+ resize();
3894
+ redrawStatic();
3895
+ });
3896
+ const themeObs = new MutationObserver(() => {
3897
+ redrawStatic();
3898
+ });
3899
+ ro.observe(canvas);
3900
+ themeObs.observe(document.body, { attributes: true, attributeFilter: ["data-ds-dark-theme"] });
3901
+ resize();
3902
+ draw(performance.now());
3903
+ if (!reduced.matches) frame = window.requestAnimationFrame(loop);
3904
+ return () => {
3905
+ window.cancelAnimationFrame(frame);
3906
+ ro.disconnect();
3907
+ themeObs.disconnect();
3908
+ };
3909
+ }, []);
3910
+ const popRef = (0, import_react18.useRef)(null);
3911
+ const shiftRef = (0, import_react18.useRef)(0);
3912
+ const [shiftX, setShiftX] = (0, import_react18.useState)(0);
3913
+ (0, import_react18.useLayoutEffect)(() => {
3914
+ const clamp = () => {
3915
+ const el = popRef.current;
3916
+ if (!el) return;
3917
+ const r = el.getBoundingClientRect();
3918
+ if (r.width === 0) return;
3919
+ const left = r.left - shiftRef.current;
3920
+ const edge = 8;
3921
+ let next = 0;
3922
+ if (left < edge) next = edge - left;
3923
+ else if (left + r.width > window.innerWidth - edge) {
3924
+ next = window.innerWidth - edge - (left + r.width);
3925
+ }
3926
+ if (next !== shiftRef.current) {
3927
+ shiftRef.current = next;
3928
+ setShiftX(next);
3929
+ }
3930
+ };
3931
+ clamp();
3932
+ window.addEventListener("resize", clamp);
3933
+ const iv = window.setInterval(clamp, 100);
3934
+ return () => {
3935
+ window.removeEventListener("resize", clamp);
3936
+ window.clearInterval(iv);
3937
+ };
3938
+ }, []);
3939
+ const stops = [];
3940
+ for (let i = 0; i < MODES.length; i++) {
3941
+ const stopLeft = i / (MODES.length - 1) * INNER_W + THUMB / 2;
3942
+ stops.push(
3943
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3944
+ "div",
3945
+ {
3946
+ style: {
3947
+ position: "absolute",
3948
+ left: stopLeft - 3,
3949
+ top: (RAIL_H - 6) / 2,
3950
+ width: 6,
3951
+ height: 6,
3952
+ borderRadius: "50%",
3953
+ background: "var(--dsh-mem-dot)",
3954
+ zIndex: 2,
3955
+ pointerEvents: "none"
3956
+ }
3957
+ },
3958
+ "stop" + i
3959
+ )
3960
+ );
3961
+ }
3962
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3963
+ "div",
3964
+ {
3965
+ ref: popRef,
3966
+ style: {
3967
+ position: "absolute",
3968
+ bottom: "calc(100% + 8px)",
3969
+ left: "50%",
3970
+ transform: "translateX(calc(-50% + " + shiftX + "px))",
3971
+ zIndex: 1e3
3972
+ },
3973
+ children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3974
+ "div",
3975
+ {
3976
+ className: "dsh-mem-popover",
3977
+ style: { position: "relative", padding: "14px 16px" },
3978
+ children: [
3979
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3980
+ "div",
3981
+ {
3982
+ ref: trackRef,
3983
+ className: "dsh-mem-hitband",
3984
+ style: {
3985
+ position: "relative",
3986
+ // 容器宽 = thumb 活动范围(0..INNER_W + THUMB),点击映射与视觉两端严格对齐
3987
+ width: TRACK_W,
3988
+ height: RAIL_H,
3989
+ borderRadius: 999,
3990
+ background: "var(--dsh-mem-track)",
3991
+ touchAction: "none",
3992
+ cursor: drag === null ? "pointer" : "grabbing"
3993
+ },
3994
+ onPointerDown,
3995
+ onPointerMove,
3996
+ onPointerUp,
3997
+ onPointerCancel: onPointerUp,
3998
+ children: [
3999
+ activeIdx > 0 || drag !== null ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4000
+ "div",
4001
+ {
4002
+ style: {
4003
+ position: "absolute",
4004
+ left: 0,
4005
+ top: 0,
4006
+ bottom: 0,
4007
+ width: thumbLeft + THUMB,
4008
+ borderRadius: 999,
4009
+ background: "linear-gradient(90deg, var(--dsh-mem-fill-1), var(--dsh-mem-fill-2))",
4010
+ pointerEvents: "none",
4011
+ zIndex: 1,
4012
+ transition: drag === null ? "width 120ms ease" : "none"
4013
+ }
4014
+ }
4015
+ ) : null,
4016
+ stops,
4017
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4018
+ "canvas",
4019
+ {
4020
+ ref: canvasRef,
4021
+ className: "dsh-mem-particles",
4022
+ style: {
4023
+ position: "absolute",
4024
+ left: 0,
4025
+ top: 0,
4026
+ width: "100%",
4027
+ height: "100%",
4028
+ pointerEvents: "none",
4029
+ zIndex: 2,
4030
+ filter: drag !== null ? "saturate(1.45) brightness(1.28) contrast(1.06)" : "none"
4031
+ }
4032
+ }
4033
+ ),
4034
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4035
+ "div",
4036
+ {
4037
+ style: {
4038
+ position: "absolute",
4039
+ left: thumbLeft,
4040
+ top: (RAIL_H - THUMB) / 2,
4041
+ width: THUMB,
4042
+ height: THUMB,
4043
+ borderRadius: "50%",
4044
+ background: "var(--dsh-mem-thumb)",
4045
+ border: "1px solid var(--dsh-mem-accent)",
4046
+ boxShadow: drag !== null ? "0 2px 8px rgba(0,0,0,0.35)" : "0 1px 4px rgba(0,0,0,0.25)",
4047
+ pointerEvents: "none",
4048
+ transition: drag === null ? "left 120ms ease" : "none",
4049
+ zIndex: 3
4050
+ }
4051
+ }
4052
+ ),
4053
+ drag !== null ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "dsh-mem-bubble", style: { left: thumbLeft + THUMB / 2, zIndex: 4 }, children: info.label }) : null
4054
+ ]
4055
+ }
4056
+ ),
4057
+ props.error ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { style: { fontSize: 11, color: "var(--dsh-mem-danger)", marginTop: 10, whiteSpace: "nowrap" }, children: props.error }) : null,
4058
+ props.recall !== void 0 && props.onCommitRecall ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4059
+ "div",
4060
+ {
4061
+ style: {
4062
+ borderTop: "1px solid var(--dsh-mem-border)",
4063
+ marginTop: 10,
4064
+ paddingTop: 8,
4065
+ display: "flex",
4066
+ justifyContent: "space-between",
4067
+ alignItems: "center",
4068
+ gap: 8
4069
+ },
4070
+ children: [
4071
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { style: { fontSize: 12, color: "var(--dsh-mem-text-3)" }, children: "注入" }),
4072
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4073
+ Segmented,
4074
+ {
4075
+ value: props.recall === null ? "follow" : props.recall ? "on" : "off",
4076
+ disabled: props.mode === "off",
4077
+ options: [
4078
+ { key: "follow", label: "跟随全局", title: "清除本会话覆盖,跟随全局召回开关" },
4079
+ { key: "on", label: "开", title: "本会话强制注入记忆" },
4080
+ { key: "off", label: "关", title: "只写:记忆照常沉淀,但不注入本会话" }
4081
+ ],
4082
+ onChange: (key) => props.onCommitRecall(key === "on" ? true : key === "off" ? false : null)
4083
+ }
4084
+ )
4085
+ ]
4086
+ }
4087
+ ) : null,
4088
+ props.rpc && props.sessionId ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(SessionInfoArea, { rpc: props.rpc, sessionId: props.sessionId }) : null
4089
+ ]
4090
+ }
4091
+ )
4092
+ }
4093
+ );
4094
+ }
4095
+
4096
+ // client/src/pill/MemoryModePill.tsx
4097
+ var import_jsx_runtime19 = require("react/jsx-runtime");
4098
+ function MemoryModePill(props) {
4099
+ const rpc = props.rpc;
4100
+ const sessionId = props.sessionId || props.session && props.session.sessionId;
4101
+ const [mode, setMode] = (0, import_react19.useState)(null);
4102
+ const [recall, setRecall] = (0, import_react19.useState)(null);
4103
+ const [recallResolved, setRecallResolved] = (0, import_react19.useState)(true);
4104
+ const [error, setError] = (0, import_react19.useState)(null);
4105
+ const [open, setOpen] = (0, import_react19.useState)(false);
4106
+ const wrapRef = (0, import_react19.useRef)(null);
4107
+ const seqRef = (0, import_react19.useRef)(0);
4108
+ const load = (0, import_react19.useCallback)(() => {
4109
+ if (!sessionId || !rpc) return;
4110
+ const token = ++seqRef.current;
4111
+ setError(null);
4112
+ rpc("dsh-memory/session-mode-get", { sessionId }).then((r) => {
4113
+ if (token !== seqRef.current) return;
4114
+ if (r && r.ok && r.value) {
4115
+ setMode(r.value.mode);
4116
+ setRecall(r.value.recall);
4117
+ setRecallResolved(r.value.recallResolved);
4118
+ } else setError(r && !r.ok ? r.error.message : "RPC error");
4119
+ }).catch((e) => {
4120
+ if (token !== seqRef.current) return;
4121
+ setError(String(e && e.message || e));
4122
+ });
4123
+ }, [sessionId, rpc]);
4124
+ (0, import_react19.useEffect)(() => {
4125
+ load();
4126
+ }, [load]);
4127
+ (0, import_react19.useEffect)(() => {
4128
+ watchSidebarIcon();
4129
+ }, []);
4130
+ (0, import_react19.useEffect)(() => {
4131
+ initOccupancyIndicator(
4132
+ (endpoint, payload) => rpc(endpoint, payload)
4133
+ );
4134
+ initPanelSection(currentMeterSnapshot);
4135
+ watchContextMeter();
4136
+ noteOccupancySession(sessionId ?? null);
4137
+ }, [sessionId, rpc]);
4138
+ (0, import_react19.useEffect)(() => {
4139
+ if (!open) return;
4140
+ const onDown = (e) => {
4141
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
4142
+ };
4143
+ const onKey = (e) => {
4144
+ if (e.key === "Escape") setOpen(false);
4145
+ };
4146
+ document.addEventListener("pointerdown", onDown);
4147
+ document.addEventListener("keydown", onKey);
4148
+ return () => {
4149
+ document.removeEventListener("pointerdown", onDown);
4150
+ document.removeEventListener("keydown", onKey);
4151
+ };
4152
+ }, [open]);
4153
+ const commit = (next) => {
4154
+ if (!rpc || !sessionId || mode === null || next === mode) return;
4155
+ const prev = mode;
4156
+ const token = seqRef.current;
4157
+ setMode(next);
4158
+ setError(null);
4159
+ rpc("dsh-memory/session-mode-set", { sessionId, mode: next }).then((r) => {
4160
+ if (token !== seqRef.current) return;
4161
+ if (!r || !r.ok) {
4162
+ setMode(prev);
4163
+ setError(r && r.error ? "档位写入失败:" + r.error.message : "档位写入失败");
4164
+ }
4165
+ }).catch((e) => {
4166
+ if (token !== seqRef.current) return;
4167
+ setMode(prev);
4168
+ setError("档位写入失败:" + String(e && e.message || e));
4169
+ });
4170
+ };
4171
+ const commitRecall = (next) => {
4172
+ if (!rpc || !sessionId || mode === null || next === recall) return;
4173
+ const prevRecall = recall;
4174
+ const prevResolved = recallResolved;
4175
+ const token = seqRef.current;
4176
+ setRecall(next);
4177
+ if (next !== null) setRecallResolved(next);
4178
+ setError(null);
4179
+ rpc("dsh-memory/session-mode-set", { sessionId, mode, recall: next }).then((r) => {
4180
+ if (token !== seqRef.current) return;
4181
+ if (!r || !r.ok) {
4182
+ setRecall(prevRecall);
4183
+ setRecallResolved(prevResolved);
4184
+ setError(r && r.error ? "注入设置失败:" + r.error.message : "注入设置失败");
4185
+ } else {
4186
+ setRecall(r.value.recall);
4187
+ setRecallResolved(r.value.recallResolved);
4188
+ }
4189
+ }).catch((e) => {
4190
+ if (token !== seqRef.current) return;
4191
+ setRecall(prevRecall);
4192
+ setRecallResolved(prevResolved);
4193
+ setError("注入设置失败:" + String(e && e.message || e));
4194
+ });
4195
+ };
4196
+ if (!sessionId || !rpc) return null;
4197
+ const info = modeInfo(mode);
4198
+ const loaded = mode !== null;
4199
+ const isOff = loaded && mode === "off";
4200
+ const isFlow = loaded && !isOff;
4201
+ const faceLabel = !loaded ? error ? "⚠" : "…" : isOff ? info.label : !recallResolved ? "只写" : info.label;
4202
+ ensureThemeStyle();
4203
+ const pillStyle = {
4204
+ position: "relative",
4205
+ // dsh-mem-pill-hit 的 ::after 隐形热区以此为定位基准
4206
+ display: "inline-flex",
4207
+ alignItems: "center",
4208
+ gap: 4,
4209
+ height: 24,
4210
+ padding: "0 10px",
4211
+ borderRadius: 999,
4212
+ fontSize: 12,
4213
+ fontWeight: 500,
4214
+ lineHeight: "20px",
4215
+ cursor: "pointer",
4216
+ // 流光档的边框/背景由 .dsh-mem-flow 的双层背景提供(流光边 + 不透明内底),
4217
+ // inline 只给文字色 / 光晕 / 流光内底混色通道(--dsh-mem-pill-tint)
4218
+ color: isFlow ? info.color : "var(--dsh-mem-text-2)"
4219
+ };
4220
+ if (isFlow) {
4221
+ pillStyle.boxShadow = "0 0 12px color-mix(in srgb, " + info.color + " 30%, transparent)";
4222
+ pillStyle["--dsh-mem-pill-tint"] = info.color;
4223
+ }
4224
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { ref: wrapRef, style: { position: "relative", display: "inline-flex" }, children: [
4225
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4226
+ "button",
4227
+ {
4228
+ type: "button",
4229
+ title: error ? "档位读取失败:" + error + "(点击重试)" : "本会话记忆档位(点击切换)",
4230
+ onClick: () => {
4231
+ if (error) load();
4232
+ setOpen(!open);
4233
+ },
4234
+ className: (isFlow ? "dsh-mem-flow" : "dsh-mem-pill-off") + " dsh-mem-pill-hit",
4235
+ style: pillStyle,
4236
+ children: [
4237
+ "记忆 · ",
4238
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { children: faceLabel })
4239
+ ]
4240
+ }
4241
+ ),
4242
+ open ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4243
+ ModeSlider,
4244
+ {
4245
+ mode: mode || "auto",
4246
+ onCommit: commit,
4247
+ recall: loaded ? recall : void 0,
4248
+ onCommitRecall: commitRecall,
4249
+ error,
4250
+ rpc,
4251
+ sessionId
4252
+ }
4253
+ ) : null
4254
+ ] });
4255
+ }
4256
+
4257
+ // client/src/entry.tsx
4258
+ var inject = ["slots", "connection"];
4259
+ function apply(ctx) {
4260
+ const rpc = makeRpc(ctx);
4261
+ ctx.slots.inject("settings.section", () => {
4262
+ return ctx.slots.register(
4263
+ {
4264
+ name: "settings.section",
4265
+ id: "dsh-memory",
4266
+ order: 200,
4267
+ label: "记忆",
4268
+ inject: () => ({ rpc })
4269
+ },
4270
+ MemoryPanel
4271
+ );
4272
+ });
4273
+ ctx.slots.inject("conversation.input.left", () => {
4274
+ return ctx.slots.register(
4275
+ {
4276
+ name: "conversation.input.left",
4277
+ id: "dsh-memory-mode",
4278
+ order: 100,
4279
+ inject: (sessionId) => ({ sessionId, rpc })
4280
+ },
4281
+ MemoryModePill
4282
+ );
4283
+ });
4284
+ }
4285
+
4286
+ // esbuild 对具名导出会整体替换 module.exports(__toCommonJS:getter + __esModule);
4287
+ // 摊平回官方 bundle 同款的普通数据属性对象(含 toStringTag),loader 只按属性读取。
4288
+ var __flat = {};
4289
+ for (var __k in module.exports) __flat[__k] = module.exports[__k];
4290
+ Object.defineProperty(__flat, Symbol.toStringTag, { value: "Module" });
4291
+ return __flat;
4292
+ }
4293
+ });