lume-dsh-plugin 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +460 -0
- package/README.md +245 -275
- package/lib/client.js +242 -181
- package/lib/core/card.js +11 -9
- package/lib/core/citations.js +235 -0
- package/lib/core/coverage.js +149 -0
- package/lib/core/dialogue-mining.js +49 -11
- package/lib/core/knowledge.js +165 -0
- package/lib/core/leak-detector.js +1 -3
- package/lib/core/ledger.js +129 -18
- package/lib/core/manifest.js +3 -1
- package/lib/core/memory-id.js +105 -0
- package/lib/core/metrics.js +270 -0
- package/lib/core/persona-limits.js +25 -0
- package/lib/core/scope.js +124 -0
- package/lib/core/signals.js +285 -5
- package/lib/core/task-memory.js +143 -0
- package/lib/core/text.js +45 -3
- package/lib/host/backfill.js +218 -0
- package/lib/host/bootstrap.js +130 -0
- package/lib/host/boundary.js +1 -3
- package/lib/host/clauses.js +180 -0
- package/lib/host/config.js +7 -0
- package/lib/host/diag.js +44 -7
- package/lib/host/distill-prompt.js +365 -0
- package/lib/host/distill.js +22 -348
- package/lib/host/extraction.js +11 -3
- package/lib/host/host-context.js +1 -0
- package/lib/host/host-events.js +100 -0
- package/lib/host/identity.js +12 -29
- package/lib/host/inbound.js +133 -0
- package/lib/host/injection.js +2 -6
- package/lib/host/llm-aux.js +130 -0
- package/lib/host/llm-route.js +3 -0
- package/lib/host/methods.js +182 -11
- package/lib/host/metrics-log.js +169 -0
- package/lib/host/notices.js +62 -0
- package/lib/host/project-access.js +210 -0
- package/lib/host/project.js +153 -2
- package/lib/host/prompt-blocks.js +113 -0
- package/lib/host/protocol.js +144 -11
- package/lib/host/reflection.js +28 -5
- package/lib/host/registry.js +0 -4
- package/lib/host/requirements-scan.js +108 -0
- package/lib/host/rpc-bridge.js +23 -4
- package/lib/host/rpc.js +1 -1
- package/lib/host/sections.js +48 -0
- package/lib/host/session-deps.js +27 -0
- package/lib/host/session-events.js +371 -0
- package/lib/host/session-runtime.js +18 -5
- package/lib/host/thinking.js +10 -1
- package/lib/host/tools.js +367 -0
- package/lib/host/triggers.js +30 -6
- package/lib/host/turn-boundary.js +116 -0
- package/lib/host/wiring.js +280 -0
- package/lib/host/workspace-map.js +81 -0
- package/lib/index.js +334 -836
- package/package.json +13 -4
package/lib/client.js
CHANGED
|
@@ -83,6 +83,40 @@ function detectChatLog(text) {
|
|
|
83
83
|
return chat ? chat.speakers : null;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/client/distill-job.ts
|
|
88
|
+
/** 阶段顺序:客户端据此渲染进度点(宿主只给当前阶段名,不落文案)。 */
|
|
89
|
+
const STAGE_ORDER = [
|
|
90
|
+
"mining",
|
|
91
|
+
"contract",
|
|
92
|
+
"corpus"
|
|
93
|
+
];
|
|
94
|
+
/** 素材上限(≈2 万字);超出由 RPC 层拒绝。 */
|
|
95
|
+
const TEXT_CAP = 2e4;
|
|
96
|
+
/** 聊天记录素材的宽容上限:原始文本含双人对白 + 时间戳,噪音过半。 */
|
|
97
|
+
const CHAT_TEXT_CAP = 2e5;
|
|
98
|
+
/** 轮询间隔:任务制兜住 10~90s 的不可控蒸馏耗时。 */
|
|
99
|
+
const POLL_INTERVAL_MS = 2e3;
|
|
100
|
+
function applyJobStatus(job) {
|
|
101
|
+
if (job === null || job === void 0) return { kind: "lost" };
|
|
102
|
+
if (job.status === "running") return job.stage ? {
|
|
103
|
+
kind: "stage",
|
|
104
|
+
stage: job.stage
|
|
105
|
+
} : { kind: "wait" };
|
|
106
|
+
if (job.status === "done") return job.card ? {
|
|
107
|
+
kind: "done",
|
|
108
|
+
card: {
|
|
109
|
+
...job.card,
|
|
110
|
+
memory: job.card.memory ?? void 0
|
|
111
|
+
}
|
|
112
|
+
} : { kind: "wait" };
|
|
113
|
+
if (job.status === "error") return {
|
|
114
|
+
kind: "error",
|
|
115
|
+
reason: job.error ?? "unknown"
|
|
116
|
+
};
|
|
117
|
+
return { kind: "wait" };
|
|
118
|
+
}
|
|
119
|
+
|
|
86
120
|
//#endregion
|
|
87
121
|
//#region src/client/form-styles.ts
|
|
88
122
|
/** 共享表单内联样式:原生 DOM 控件,视觉对齐官方原语(仓库既有模式)。 */
|
|
@@ -112,14 +146,7 @@ const labelStyle = {
|
|
|
112
146
|
* (仓库既有模式,primitives 没有 Textarea)。轮询 2s 一次,任务制兜住 10~90s
|
|
113
147
|
* 的不可控蒸馏耗时;宿主重启导致任务丢失时提示重蒸。
|
|
114
148
|
*/
|
|
115
|
-
const STAGE_ORDER = [
|
|
116
|
-
"mining",
|
|
117
|
-
"contract",
|
|
118
|
-
"corpus"
|
|
119
|
-
];
|
|
120
|
-
const TEXT_CAP = 2e4;
|
|
121
149
|
/** 聊天记录素材的宽容上限:原始文本含双人对话+时间戳,噪音过半。 */
|
|
122
|
-
const CHAT_TEXT_CAP = 2e5;
|
|
123
150
|
const miniBtn = {
|
|
124
151
|
background: "none",
|
|
125
152
|
border: "none",
|
|
@@ -189,32 +216,32 @@ function DistillModal({ open, onClose, onSaved, t, callRpc }) {
|
|
|
189
216
|
const timer = setInterval(async () => {
|
|
190
217
|
try {
|
|
191
218
|
const res = await callRpc("distillStatus", { jobId });
|
|
192
|
-
if (cancelled) return;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
219
|
+
if (cancelled || !res?.ok) return;
|
|
220
|
+
switch (applyJobStatus(res.value).kind) {
|
|
221
|
+
case "lost":
|
|
222
|
+
setError(t("distill.lost"));
|
|
223
|
+
setPhase("input");
|
|
224
|
+
break;
|
|
225
|
+
case "stage":
|
|
226
|
+
setStage(res.value.stage ?? null);
|
|
227
|
+
break;
|
|
228
|
+
case "done": {
|
|
229
|
+
const action = applyJobStatus(res.value);
|
|
230
|
+
if (action.kind === "done") setCard(action.card);
|
|
231
|
+
setStage("corpus");
|
|
232
|
+
setPhase("preview");
|
|
233
|
+
setShowComplete(true);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case "error": {
|
|
237
|
+
const action = applyJobStatus(res.value);
|
|
238
|
+
if (action.kind === "error") setError(t("distill.failed", { message: action.reason }));
|
|
239
|
+
setPhase("input");
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
215
242
|
}
|
|
216
243
|
} catch {}
|
|
217
|
-
},
|
|
244
|
+
}, POLL_INTERVAL_MS);
|
|
218
245
|
return () => {
|
|
219
246
|
cancelled = true;
|
|
220
247
|
clearInterval(timer);
|
|
@@ -262,7 +289,7 @@ function DistillModal({ open, onClose, onSaved, t, callRpc }) {
|
|
|
262
289
|
const importFile = async (file) => {
|
|
263
290
|
if (!file) return;
|
|
264
291
|
const content = await file.text();
|
|
265
|
-
if (content.length >
|
|
292
|
+
if (content.length > 2e5) {
|
|
266
293
|
setError(t("distill.too.long", { cap }));
|
|
267
294
|
return;
|
|
268
295
|
}
|
|
@@ -749,31 +776,23 @@ function DistillModal({ open, onClose, onSaved, t, callRpc }) {
|
|
|
749
776
|
}
|
|
750
777
|
|
|
751
778
|
//#endregion
|
|
752
|
-
//#region src/client/
|
|
753
|
-
/**
|
|
754
|
-
* 记忆星图:某个角色的长期记忆,以力导向星空图可视化。
|
|
755
|
-
*
|
|
756
|
-
* 渲染为自定义宽幅遮罩层(不依赖 Modal 原语——Modal 的 dialog 宽度上限 380px,
|
|
757
|
-
* 对记忆星图这种需要横屏空间的内容过于局促,改为自绘遮罩 + 居中卡片)。
|
|
758
|
-
*
|
|
759
|
-
* - Canvas 渲染:星空背景 + 力导向布局 + 缓慢绕圈漂移 + 发光卡片 + 语义连线
|
|
760
|
-
* - 核心记忆(身份/称呼类,宿主 isCoreMemory 判定)紫色 + ★,普通记忆青色
|
|
761
|
-
* - 点击卡片 → 右侧详情面板(只读正文 + 关联记忆 + 编辑/删除)
|
|
762
|
-
* - 编辑:textarea → updateMemory;删除:deleteMemory
|
|
763
|
-
* - 顶部日期筛选:全部 / 最近 7 天 / 30 天 / 90 天
|
|
764
|
-
*/
|
|
779
|
+
//#region src/client/graph-layout.ts
|
|
765
780
|
const CARD_W = 260;
|
|
766
781
|
const CARD_H = 72;
|
|
767
|
-
const CARD_R = 12;
|
|
768
782
|
const CORE_COLOR = "#a78bfa";
|
|
769
783
|
const NORMAL_COLOR = "#67e8f9";
|
|
770
|
-
const OVERLAY_W = 960;
|
|
771
784
|
const FILTER_MS = {
|
|
772
785
|
all: 0,
|
|
773
786
|
"7d": 6048e5,
|
|
774
787
|
"30d": 2592e6,
|
|
775
788
|
"90d": 7776e6
|
|
776
789
|
};
|
|
790
|
+
/** 时间筛选:now 由调用方传入(纯函数,便于测试)。 */
|
|
791
|
+
function filterByAge(items, filter, now) {
|
|
792
|
+
const ms = FILTER_MS[filter];
|
|
793
|
+
return items.filter((m) => !ms || now - m.at <= ms);
|
|
794
|
+
}
|
|
795
|
+
/** 词法切分:英文按词、中文按二元组(让"相似"在中文上也可用)。 */
|
|
777
796
|
function tokenize(text) {
|
|
778
797
|
const tokens = [];
|
|
779
798
|
const lowered = text.toLowerCase();
|
|
@@ -787,8 +806,10 @@ function tokenize(text) {
|
|
|
787
806
|
}
|
|
788
807
|
return tokens;
|
|
789
808
|
}
|
|
809
|
+
/** Jaccard 相似度(基于 tokenize 的集合)。 */
|
|
790
810
|
function jaccard(a, b) {
|
|
791
|
-
const sa = new Set(tokenize(a))
|
|
811
|
+
const sa = new Set(tokenize(a));
|
|
812
|
+
const sb = new Set(tokenize(b));
|
|
792
813
|
if (sa.size === 0 || sb.size === 0) return 0;
|
|
793
814
|
let hit = 0;
|
|
794
815
|
sa.forEach((t) => {
|
|
@@ -796,8 +817,8 @@ function jaccard(a, b) {
|
|
|
796
817
|
});
|
|
797
818
|
return hit / (sa.size + sb.size - hit);
|
|
798
819
|
}
|
|
799
|
-
function relTime(ts) {
|
|
800
|
-
const sec = Math.floor((
|
|
820
|
+
function relTime(ts, now = Date.now()) {
|
|
821
|
+
const sec = Math.floor((now - ts) / 1e3);
|
|
801
822
|
if (sec < 60) return "刚刚";
|
|
802
823
|
const min = Math.floor(sec / 60);
|
|
803
824
|
if (min < 60) return `${min} 分钟前`;
|
|
@@ -805,15 +826,17 @@ function relTime(ts) {
|
|
|
805
826
|
if (hr < 24) return `${hr} 小时前`;
|
|
806
827
|
return `${Math.floor(hr / 24)} 天前`;
|
|
807
828
|
}
|
|
808
|
-
function hexGlow(hex, alpha) {
|
|
809
|
-
return `rgba(${parseInt(hex.slice(1, 3), 16)},${parseInt(hex.slice(3, 5), 16)},${parseInt(hex.slice(5, 7), 16)},${alpha})`;
|
|
810
|
-
}
|
|
811
829
|
function rgba(hex, alpha) {
|
|
812
830
|
return `rgba(${parseInt(hex.slice(1, 3), 16)},${parseInt(hex.slice(3, 5), 16)},${parseInt(hex.slice(5, 7), 16)},${alpha})`;
|
|
813
831
|
}
|
|
832
|
+
/** 星图的辉光色(与 rgba 同源,语义不同:一个是光晕,一个是描边)。 */
|
|
833
|
+
function hexGlow(hex, alpha) {
|
|
834
|
+
return rgba(hex, alpha);
|
|
835
|
+
}
|
|
814
836
|
function brighten(hex) {
|
|
815
837
|
return `rgb(${Math.min(255, parseInt(hex.slice(1, 3), 16) + 50)},${Math.min(255, parseInt(hex.slice(3, 5), 16) + 50)},${Math.min(255, parseInt(hex.slice(5, 7), 16) + 50)})`;
|
|
816
838
|
}
|
|
839
|
+
/** 按 canvas 实测宽度折行(依赖传入的 ctx,便于用假 ctx 测试)。 */
|
|
817
840
|
function wrapText(ctx, text, maxW) {
|
|
818
841
|
const chars = text.split("");
|
|
819
842
|
const lines = [];
|
|
@@ -828,6 +851,132 @@ function wrapText(ctx, text, maxW) {
|
|
|
828
851
|
if (cur) lines.push(cur);
|
|
829
852
|
return lines;
|
|
830
853
|
}
|
|
854
|
+
const EDGE_MIN = .12;
|
|
855
|
+
const CORE_EDGE = .15;
|
|
856
|
+
const LINK_DIST = 160;
|
|
857
|
+
const LINK_K = .025;
|
|
858
|
+
const CENTER_PULL = .012;
|
|
859
|
+
const DAMPING = .86;
|
|
860
|
+
const MARGIN = 90;
|
|
861
|
+
/**
|
|
862
|
+
* 建图:节点随机落在中心区域,两两相似度 ≥0.12 连边(核心记忆与所有节点弱连)。
|
|
863
|
+
* rand 注入是为了可复现(测试喂固定序列)。
|
|
864
|
+
*/
|
|
865
|
+
function buildGraph(items, opts) {
|
|
866
|
+
const rand = opts.rand ?? Math.random;
|
|
867
|
+
const W = opts.w, H = opts.h;
|
|
868
|
+
const nodes = items.map((m, id) => {
|
|
869
|
+
const x = W / 2 + (rand() - .5) * W * .22;
|
|
870
|
+
const y = H / 2 + (rand() - .5) * H * .22;
|
|
871
|
+
return {
|
|
872
|
+
id,
|
|
873
|
+
text: m.text,
|
|
874
|
+
at: m.at,
|
|
875
|
+
core: m.core,
|
|
876
|
+
x,
|
|
877
|
+
y,
|
|
878
|
+
ax: x,
|
|
879
|
+
ay: y,
|
|
880
|
+
orbitR: 3 + rand() * 5,
|
|
881
|
+
orbitPhase: rand() * Math.PI * 2,
|
|
882
|
+
orbitSpeed: .006 + rand() * .008,
|
|
883
|
+
vx: 0,
|
|
884
|
+
vy: 0,
|
|
885
|
+
pinned: false
|
|
886
|
+
};
|
|
887
|
+
});
|
|
888
|
+
const edges = [];
|
|
889
|
+
for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) {
|
|
890
|
+
const w = jaccard(nodes[i].text, nodes[j].text);
|
|
891
|
+
if (w >= EDGE_MIN) edges.push({
|
|
892
|
+
source: i,
|
|
893
|
+
target: j,
|
|
894
|
+
weight: w
|
|
895
|
+
});
|
|
896
|
+
else if (nodes[i].core || nodes[j].core) edges.push({
|
|
897
|
+
source: i,
|
|
898
|
+
target: j,
|
|
899
|
+
weight: CORE_EDGE
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
return {
|
|
903
|
+
nodes,
|
|
904
|
+
edges
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
/** 一步力导向迭代:斥力 + 弹簧 + 向心力 + 阻尼 + 边界钳制(纯数学,就地改节点)。 */
|
|
908
|
+
function forceStep(nodes, edges, w, h, rand = Math.random) {
|
|
909
|
+
const cx = w / 2, cy = h / 2;
|
|
910
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
911
|
+
const a = nodes[i];
|
|
912
|
+
if (a.pinned) continue;
|
|
913
|
+
for (let j = i + 1; j < nodes.length; j++) {
|
|
914
|
+
const b = nodes[j];
|
|
915
|
+
if (b.pinned) continue;
|
|
916
|
+
let dx = a.x - b.x, dy = a.y - b.y, d2 = dx * dx + dy * dy;
|
|
917
|
+
if (d2 < 1) {
|
|
918
|
+
dx = rand() - .5;
|
|
919
|
+
dy = rand() - .5;
|
|
920
|
+
d2 = 1;
|
|
921
|
+
}
|
|
922
|
+
const d = Math.sqrt(d2), f = 12100 / d;
|
|
923
|
+
a.vx += dx / d * f;
|
|
924
|
+
a.vy += dy / d * f;
|
|
925
|
+
b.vx -= dx / d * f;
|
|
926
|
+
b.vy -= dy / d * f;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
for (const e of edges) {
|
|
930
|
+
const a = nodes[e.source], b = nodes[e.target];
|
|
931
|
+
const dx = b.x - a.x, dy = b.y - a.y;
|
|
932
|
+
const d = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
933
|
+
const f = (d - LINK_DIST) * LINK_K * e.weight;
|
|
934
|
+
if (!a.pinned) {
|
|
935
|
+
a.vx += dx / d * f;
|
|
936
|
+
a.vy += dy / d * f;
|
|
937
|
+
}
|
|
938
|
+
if (!b.pinned) {
|
|
939
|
+
b.vx -= dx / d * f;
|
|
940
|
+
b.vy -= dy / d * f;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
for (const n of nodes) {
|
|
944
|
+
if (n.pinned) continue;
|
|
945
|
+
n.vx += (cx - n.x) * CENTER_PULL;
|
|
946
|
+
n.vy += (cy - n.y) * CENTER_PULL;
|
|
947
|
+
n.x += n.vx;
|
|
948
|
+
n.y += n.vy;
|
|
949
|
+
n.vx *= DAMPING;
|
|
950
|
+
n.vy *= DAMPING;
|
|
951
|
+
if (n.x < MARGIN) n.x = MARGIN;
|
|
952
|
+
else if (n.x > w - MARGIN) n.x = w - MARGIN;
|
|
953
|
+
if (n.y < MARGIN) n.y = MARGIN;
|
|
954
|
+
else if (n.y > h - MARGIN) n.y = h - MARGIN;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
/** 命中测试:从后往前(后来的在上层)。 */
|
|
958
|
+
function hitTestAt(nodes, mx, my) {
|
|
959
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
960
|
+
const n = nodes[i];
|
|
961
|
+
if (mx >= n.x - 260 / 2 && mx <= n.x + 260 / 2 && my >= n.y - 72 / 2 && my <= n.y + 72 / 2) return i;
|
|
962
|
+
}
|
|
963
|
+
return -1;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
//#endregion
|
|
967
|
+
//#region src/client/memory.tsx
|
|
968
|
+
/**
|
|
969
|
+
* 记忆星图:某个角色的长期记忆,以力导向星空图可视化。
|
|
970
|
+
*
|
|
971
|
+
* 渲染为自定义宽幅遮罩层(不依赖 Modal 原语——Modal 的 dialog 宽度上限 380px,
|
|
972
|
+
* 对记忆星图这种需要横屏空间的内容过于局促,改为自绘遮罩 + 居中卡片)。
|
|
973
|
+
*
|
|
974
|
+
* - Canvas 渲染:星空背景 + 力导向布局 + 缓慢绕圈漂移 + 发光卡片 + 语义连线
|
|
975
|
+
* - 核心记忆(身份/称呼类,宿主 isCoreMemory 判定)紫色 + ★,普通记忆青色
|
|
976
|
+
* - 点击卡片 → 右侧详情面板(只读正文 + 关联记忆 + 编辑/删除)
|
|
977
|
+
* - 编辑:textarea → updateMemory;删除:deleteMemory
|
|
978
|
+
* - 顶部日期筛选:全部 / 最近 7 天 / 30 天 / 90 天
|
|
979
|
+
*/
|
|
831
980
|
function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc }) {
|
|
832
981
|
const canvasRef = (0, react.useRef)(null);
|
|
833
982
|
const graphRef = (0, react.useRef)({
|
|
@@ -857,108 +1006,24 @@ function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc })
|
|
|
857
1006
|
(0, react.useEffect)(() => {
|
|
858
1007
|
if (open) load();
|
|
859
1008
|
}, [open, personaName]);
|
|
860
|
-
const filtered = memories
|
|
1009
|
+
const filtered = filterByAge(memories, filter, Date.now());
|
|
861
1010
|
(0, react.useEffect)(() => {
|
|
862
1011
|
const canvas = canvasRef.current;
|
|
863
1012
|
if (!canvas || !open) return;
|
|
864
1013
|
const dpr = window.devicePixelRatio || 1;
|
|
865
|
-
const W =
|
|
1014
|
+
const W = 960, H = 544;
|
|
866
1015
|
canvas.width = W * dpr;
|
|
867
1016
|
canvas.height = H * dpr;
|
|
868
|
-
canvas.style.width = "
|
|
869
|
-
canvas.style.height = "
|
|
1017
|
+
canvas.style.width = W + "px";
|
|
1018
|
+
canvas.style.height = H + "px";
|
|
870
1019
|
const ctx = canvas.getContext("2d");
|
|
871
1020
|
if (!ctx) return;
|
|
872
1021
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
873
|
-
const nodes = filtered
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
return {
|
|
877
|
-
id,
|
|
878
|
-
text: m.text,
|
|
879
|
-
at: m.at,
|
|
880
|
-
core: m.core,
|
|
881
|
-
x,
|
|
882
|
-
y,
|
|
883
|
-
ax: x,
|
|
884
|
-
ay: y,
|
|
885
|
-
orbitR: 3 + Math.random() * 5,
|
|
886
|
-
orbitPhase: Math.random() * Math.PI * 2,
|
|
887
|
-
orbitSpeed: .006 + Math.random() * .008,
|
|
888
|
-
vx: 0,
|
|
889
|
-
vy: 0,
|
|
890
|
-
pinned: false
|
|
891
|
-
};
|
|
1022
|
+
const { nodes, edges } = buildGraph(filtered, {
|
|
1023
|
+
w: W,
|
|
1024
|
+
h: H
|
|
892
1025
|
});
|
|
893
|
-
|
|
894
|
-
for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) {
|
|
895
|
-
const w = jaccard(nodes[i].text, nodes[j].text);
|
|
896
|
-
if (w >= .12) edges.push({
|
|
897
|
-
source: i,
|
|
898
|
-
target: j,
|
|
899
|
-
weight: w
|
|
900
|
-
});
|
|
901
|
-
else if (nodes[i].core) edges.push({
|
|
902
|
-
source: i,
|
|
903
|
-
target: j,
|
|
904
|
-
weight: .15
|
|
905
|
-
});
|
|
906
|
-
else if (nodes[j].core) edges.push({
|
|
907
|
-
source: i,
|
|
908
|
-
target: j,
|
|
909
|
-
weight: .15
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
const forceStep = () => {
|
|
913
|
-
const cx = W / 2, cy = H / 2;
|
|
914
|
-
for (let i = 0; i < nodes.length; i++) {
|
|
915
|
-
const a = nodes[i];
|
|
916
|
-
if (a.pinned) continue;
|
|
917
|
-
for (let j = i + 1; j < nodes.length; j++) {
|
|
918
|
-
const b = nodes[j];
|
|
919
|
-
if (b.pinned) continue;
|
|
920
|
-
let dx = a.x - b.x, dy = a.y - b.y, d2 = dx * dx + dy * dy;
|
|
921
|
-
if (d2 < 1) {
|
|
922
|
-
dx = Math.random() - .5;
|
|
923
|
-
dy = Math.random() - .5;
|
|
924
|
-
d2 = 1;
|
|
925
|
-
}
|
|
926
|
-
const d = Math.sqrt(d2), f = 12100 / d;
|
|
927
|
-
a.vx += dx / d * f;
|
|
928
|
-
a.vy += dy / d * f;
|
|
929
|
-
b.vx -= dx / d * f;
|
|
930
|
-
b.vy -= dy / d * f;
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
for (const e of edges) {
|
|
934
|
-
const a = nodes[e.source], b = nodes[e.target];
|
|
935
|
-
const dx = b.x - a.x, dy = b.y - a.y;
|
|
936
|
-
const d = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
937
|
-
const f = (d - 160) * .025 * e.weight;
|
|
938
|
-
if (!a.pinned) {
|
|
939
|
-
a.vx += dx / d * f;
|
|
940
|
-
a.vy += dy / d * f;
|
|
941
|
-
}
|
|
942
|
-
if (!b.pinned) {
|
|
943
|
-
b.vx -= dx / d * f;
|
|
944
|
-
b.vy -= dy / d * f;
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
for (const n of nodes) {
|
|
948
|
-
if (n.pinned) continue;
|
|
949
|
-
n.vx += (cx - n.x) * .012;
|
|
950
|
-
n.vy += (cy - n.y) * .012;
|
|
951
|
-
n.x += n.vx;
|
|
952
|
-
n.y += n.vy;
|
|
953
|
-
n.vx *= .86;
|
|
954
|
-
n.vy *= .86;
|
|
955
|
-
if (n.x < 90) n.x = 90;
|
|
956
|
-
else if (n.x > 870) n.x = 870;
|
|
957
|
-
if (n.y < 90) n.y = 90;
|
|
958
|
-
else if (n.y > 454) n.y = 454;
|
|
959
|
-
}
|
|
960
|
-
};
|
|
961
|
-
for (let i = 0; i < 250; i++) forceStep();
|
|
1026
|
+
for (let i = 0; i < 250; i++) forceStep(nodes, edges, W, H);
|
|
962
1027
|
nodes.forEach((n) => {
|
|
963
1028
|
n.ax = n.x;
|
|
964
1029
|
n.ay = n.y;
|
|
@@ -975,13 +1040,6 @@ function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc })
|
|
|
975
1040
|
phase: Math.random() * Math.PI * 2
|
|
976
1041
|
}));
|
|
977
1042
|
let hovered = null, dragging = null, dragOffX = 0, dragOffY = 0, dragMoved = false;
|
|
978
|
-
const hitTest = (mx, my) => {
|
|
979
|
-
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
980
|
-
const n = nodes[i];
|
|
981
|
-
if (mx >= n.x - CARD_W / 2 && mx <= n.x + CARD_W / 2 && my >= n.y - CARD_H / 2 && my <= n.y + CARD_H / 2) return i;
|
|
982
|
-
}
|
|
983
|
-
return -1;
|
|
984
|
-
};
|
|
985
1043
|
const onMove = (e) => {
|
|
986
1044
|
const rect = canvas.getBoundingClientRect();
|
|
987
1045
|
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
|
|
@@ -993,7 +1051,7 @@ function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc })
|
|
|
993
1051
|
dragMoved = true;
|
|
994
1052
|
return;
|
|
995
1053
|
}
|
|
996
|
-
const idx =
|
|
1054
|
+
const idx = hitTestAt(nodes, mx, my);
|
|
997
1055
|
hovered = idx >= 0 ? idx : null;
|
|
998
1056
|
canvas.style.cursor = idx >= 0 ? "grab" : "default";
|
|
999
1057
|
};
|
|
@@ -1001,7 +1059,7 @@ function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc })
|
|
|
1001
1059
|
const rect = canvas.getBoundingClientRect();
|
|
1002
1060
|
const mx = e.clientX - rect.left, my = e.clientY - rect.top;
|
|
1003
1061
|
dragMoved = false;
|
|
1004
|
-
const idx =
|
|
1062
|
+
const idx = hitTestAt(nodes, mx, my);
|
|
1005
1063
|
if (idx >= 0) {
|
|
1006
1064
|
dragging = nodes[idx];
|
|
1007
1065
|
dragging.pinned = true;
|
|
@@ -1022,7 +1080,7 @@ function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc })
|
|
|
1022
1080
|
const onClick = (e) => {
|
|
1023
1081
|
if (dragMoved) return;
|
|
1024
1082
|
const rect = canvas.getBoundingClientRect();
|
|
1025
|
-
const idx =
|
|
1083
|
+
const idx = hitTestAt(nodes, e.clientX - rect.left, e.clientY - rect.top);
|
|
1026
1084
|
setSelected(idx >= 0 ? idx : null);
|
|
1027
1085
|
if (idx >= 0) setEditing(false);
|
|
1028
1086
|
};
|
|
@@ -1082,7 +1140,7 @@ function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc })
|
|
|
1082
1140
|
const color = n.core ? CORE_COLOR : NORMAL_COLOR;
|
|
1083
1141
|
const hover = hovered === n.id, sel = selected === n.id;
|
|
1084
1142
|
const scale = hover ? 1.08 : 1;
|
|
1085
|
-
const w =
|
|
1143
|
+
const w = 260 * scale, h = 72 * scale;
|
|
1086
1144
|
const x = n.x - w / 2, y = n.y - h / 2;
|
|
1087
1145
|
ctx.save();
|
|
1088
1146
|
ctx.shadowColor = hexGlow(color, sel ? .7 : hover ? .5 : n.core ? .3 : .14);
|
|
@@ -1090,7 +1148,7 @@ function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc })
|
|
|
1090
1148
|
const bg2 = ctx.createLinearGradient(x, y, x, y + h);
|
|
1091
1149
|
bg2.addColorStop(0, "rgba(20,28,56,0.95)");
|
|
1092
1150
|
bg2.addColorStop(1, "rgba(10,14,32,0.96)");
|
|
1093
|
-
const r =
|
|
1151
|
+
const r = 12 * scale;
|
|
1094
1152
|
ctx.beginPath();
|
|
1095
1153
|
ctx.moveTo(x + r, y);
|
|
1096
1154
|
ctx.lineTo(x + w - r, y);
|
|
@@ -1213,7 +1271,7 @@ function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc })
|
|
|
1213
1271
|
},
|
|
1214
1272
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1215
1273
|
style: {
|
|
1216
|
-
width:
|
|
1274
|
+
width: 960,
|
|
1217
1275
|
maxWidth: "96vw",
|
|
1218
1276
|
background: "linear-gradient(160deg, rgba(20,28,58,0.97), rgba(10,14,32,0.99))",
|
|
1219
1277
|
border: "1px solid rgba(110,180,255,0.28)",
|
|
@@ -1462,17 +1520,19 @@ function MemoryStarMap({ open, onClose, personaName, personaLabel, t, callRpc })
|
|
|
1462
1520
|
}
|
|
1463
1521
|
|
|
1464
1522
|
//#endregion
|
|
1465
|
-
//#region src/client/
|
|
1523
|
+
//#region src/client/dom-utils.ts
|
|
1466
1524
|
/**
|
|
1467
|
-
*
|
|
1525
|
+
* client 侧的小工具(架构整理 ③)。
|
|
1468
1526
|
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
* - 导出任何人设(含内置)为自包含 JSON 卡片文件,可选是否包含记忆;
|
|
1473
|
-
* - 导入 JSON 卡片文件,同名覆盖需二次确认。
|
|
1527
|
+
* 只放**与 UI 框架无关**的东西:DOM 下载这种一次性副作用、以及可以单测的纯函数。
|
|
1528
|
+
* 为什么单独成文件:manage 的导出与将来的其它导出入口共用同一套命名/下载逻辑,
|
|
1529
|
+
* 免得每处各写一遍 `createElement("a")`。
|
|
1474
1530
|
*/
|
|
1475
|
-
/**
|
|
1531
|
+
/** 导出文件名:`<人设名>.lume.json`(人设名只保留安全字符,避免路径穿越/奇怪字符)。 */
|
|
1532
|
+
function exportFilename(personaName) {
|
|
1533
|
+
return `${personaName.trim().replace(/[\\/:*?"<>|\s]+/g, "_").replace(/^\.+/, "").slice(0, 60) || "persona"}.lume.json`;
|
|
1534
|
+
}
|
|
1535
|
+
/** 触发浏览器下载一段 JSON(DOM 副作用,不可单测的部分集中在这一行)。 */
|
|
1476
1536
|
function downloadJson(filename, obj) {
|
|
1477
1537
|
const blob = new Blob([JSON.stringify(obj, null, 2) + "\n"], { type: "application/json" });
|
|
1478
1538
|
const a = document.createElement("a");
|
|
@@ -1481,6 +1541,19 @@ function downloadJson(filename, obj) {
|
|
|
1481
1541
|
a.click();
|
|
1482
1542
|
URL.revokeObjectURL(a.href);
|
|
1483
1543
|
}
|
|
1544
|
+
|
|
1545
|
+
//#endregion
|
|
1546
|
+
//#region src/client/manage.tsx
|
|
1547
|
+
/**
|
|
1548
|
+
* 管理自定义人设:列出全部条目(内置的编辑/删除置灰),支持编辑契约、删除、导出与导入。
|
|
1549
|
+
*
|
|
1550
|
+
* - 删除走行内二次确认(删除会连带记忆/风格/档案,不可恢复);
|
|
1551
|
+
* - 编辑复用蒸馏预览的字段布局,键名是存储主键、创建后不可改;
|
|
1552
|
+
* - 保存复用 saveCustomPersona 的 upsert 语义(带原 createdAt);
|
|
1553
|
+
* - 导出任何人设(含内置)为自包含 JSON 卡片文件,可选是否包含记忆;
|
|
1554
|
+
* - 导入 JSON 卡片文件,同名覆盖需二次确认。
|
|
1555
|
+
*/
|
|
1556
|
+
/** 浏览器下载 JSON 文件(DSH webview 内可用)。 */
|
|
1484
1557
|
function ManageModal({ open, onClose, onSaved, t, callRpc, items }) {
|
|
1485
1558
|
const [phase, setPhase] = (0, react.useState)("list");
|
|
1486
1559
|
const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
|
|
@@ -1564,7 +1637,7 @@ function ManageModal({ open, onClose, onSaved, t, callRpc, items }) {
|
|
|
1564
1637
|
});
|
|
1565
1638
|
if (res?.ok && res.value) {
|
|
1566
1639
|
const bundle = res.value;
|
|
1567
|
-
downloadJson(
|
|
1640
|
+
downloadJson(exportFilename(name), bundle);
|
|
1568
1641
|
setExportTarget(null);
|
|
1569
1642
|
setNotice(t("manage.exported", { persona: name }));
|
|
1570
1643
|
} else setError(t("distill.failed", { message: "rejected" }));
|
|
@@ -1991,7 +2064,7 @@ const zh = {
|
|
|
1991
2064
|
"trigger.aria": "选择人设,当前 {persona}",
|
|
1992
2065
|
"menu.aria": "人设选择",
|
|
1993
2066
|
"status.loading": "正在加载人设…",
|
|
1994
|
-
|
|
2067
|
+
empty: "没有人设可用。",
|
|
1995
2068
|
"menu.custom.suffix": "(自定义)",
|
|
1996
2069
|
"distill.menu": "+ 蒸馏角色卡…",
|
|
1997
2070
|
"distill.title": "蒸馏角色卡",
|
|
@@ -2075,7 +2148,7 @@ const en = {
|
|
|
2075
2148
|
"trigger.aria": "Select persona, current {persona}",
|
|
2076
2149
|
"menu.aria": "Persona selection",
|
|
2077
2150
|
"status.loading": "Loading personas…",
|
|
2078
|
-
|
|
2151
|
+
empty: "No personas available.",
|
|
2079
2152
|
"menu.custom.suffix": " (custom)",
|
|
2080
2153
|
"distill.menu": "+ Distill a character card…",
|
|
2081
2154
|
"distill.title": "Distill a character card",
|
|
@@ -2157,18 +2230,6 @@ const en = {
|
|
|
2157
2230
|
|
|
2158
2231
|
//#endregion
|
|
2159
2232
|
//#region src/client/index.tsx
|
|
2160
|
-
/**
|
|
2161
|
-
* lume-dsh-plugin 客户端半边:输入栏左侧人设选择。
|
|
2162
|
-
*
|
|
2163
|
-
* 下拉用官方原语 @deepseek-ai/dsh-client-ui-primitives 的 Menu:
|
|
2164
|
-
* - side="top":输入栏位于视口底端时菜单向上弹出(B 项修复的核心)
|
|
2165
|
-
* - portal:渲染进 document.body 并随滚动/缩放跟随重定位,不受祖先 overflow 裁剪
|
|
2166
|
-
* - selectedId / onSelect / onClose(外点 + Escape)全部内建,取代 v0.1.0 手写的
|
|
2167
|
-
* document mousedown 监听与绝对定位样式
|
|
2168
|
-
*
|
|
2169
|
-
* 打包契约:本文件由 tsdown 包成 __ModuleLoader__ 工厂 bundle(见 tsdown.config.ts),
|
|
2170
|
-
* react 系与 primitives 为外部 require,由模块图解析。
|
|
2171
|
-
*/
|
|
2172
2233
|
const DISTILL_ITEM_ID = "lume-distill";
|
|
2173
2234
|
const MANAGE_ITEM_ID = "lume-manage";
|
|
2174
2235
|
/**
|
package/lib/core/card.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 导入侧校验键名合法性、内置名保护、字段截断上限,但不对 promptText
|
|
6
6
|
* 做语义校验(那是蒸馏管线的职责)。
|
|
7
7
|
*/
|
|
8
|
-
import { BUILTIN_PERSONA_NAMES, CORPUS_CAP, CORPUS_LINE_CAP, MEMORY_CAP, STYLE_CAP, sanitizeCorpus } from "
|
|
8
|
+
import { BUILTIN_PERSONA_NAMES, CORPUS_CAP, CORPUS_LINE_CAP, MEMORY_CAP, STYLE_CAP, sanitizeCorpus } from "./persona-limits.js";
|
|
9
9
|
export const CARD_FORMAT = "lume-persona-card";
|
|
10
10
|
export const CARD_VERSION = 1;
|
|
11
11
|
/** 序列化一张卡片。 */
|
|
@@ -58,16 +58,14 @@ export function parseCard(text) {
|
|
|
58
58
|
corpus: sanitizeCorpus(persona.corpus),
|
|
59
59
|
profileName: typeof persona.profileName === "string" && persona.profileName ? persona.profileName : null,
|
|
60
60
|
styleRules: Array.isArray(persona.styleRules)
|
|
61
|
-
? persona.styleRules
|
|
62
|
-
.filter((r) => typeof r?.rule === "string" && r.rule)
|
|
63
|
-
.slice(-STYLE_CAP)
|
|
61
|
+
? persona.styleRules.filter((r) => typeof r?.rule === "string" && r.rule).slice(-STYLE_CAP)
|
|
64
62
|
: [],
|
|
65
63
|
memory: Array.isArray(persona.memory)
|
|
66
|
-
? persona.memory
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
? persona.memory.filter((m) => typeof m?.text === "string" && m.text).slice(-MEMORY_CAP)
|
|
65
|
+
: undefined,
|
|
66
|
+
signatureWords: Array.isArray(persona.signatureWords)
|
|
67
|
+
? persona.signatureWords.filter((w) => typeof w === "string" && w.length > 0)
|
|
69
68
|
: undefined,
|
|
70
|
-
signatureWords: Array.isArray(persona.signatureWords) ? persona.signatureWords.filter((w) => typeof w === "string" && w.length > 0) : undefined,
|
|
71
69
|
},
|
|
72
70
|
},
|
|
73
71
|
};
|
|
@@ -77,7 +75,11 @@ export function parseCard(text) {
|
|
|
77
75
|
* 拒绝覆盖内置人设;返回规范化后的卡片(供导入写入)。
|
|
78
76
|
*/
|
|
79
77
|
export function normalizeCard(card) {
|
|
80
|
-
const name = card.name
|
|
78
|
+
const name = card.name
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
81
|
+
.replace(/^-+|-+$/g, "")
|
|
82
|
+
.slice(0, 32);
|
|
81
83
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
82
84
|
return { ok: false, error: `人设键名 "${name}" 不合法:必须以小写字母开头,只含小写字母/数字/连字符。` };
|
|
83
85
|
}
|