lume-dsh-plugin 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Token 优化算法(纯函数层):
3
+ * ① 少样本衰减 —— 会话前几轮给足示例,之后靠历史自我维持;
4
+ * ② 相关性检索 —— 本地分词 + 重叠打分(BM25 式的零成本近似),
5
+ * 记忆/风格规则只注入与当前消息相关的 top-k,核心条目恒注入。
6
+ */
7
+ /** 分词:拉丁/数字词 + CJK 二元组(够 BM25 式打分用,零依赖)。 */
8
+ export function tokenize(text) {
9
+ const tokens = [];
10
+ const lowered = text.toLowerCase();
11
+ for (const match of lowered.matchAll(/[a-z0-9]+/g)) {
12
+ tokens.push(match[0]);
13
+ }
14
+ const cjk = lowered.match(/[\u4e00-\u9fff\u3400-\u4dbf]+/g) ?? [];
15
+ for (const run of cjk) {
16
+ if (run.length === 1) {
17
+ tokens.push(run);
18
+ continue;
19
+ }
20
+ for (let i = 0; i < run.length - 1; i++) {
21
+ tokens.push(run.slice(i, i + 2));
22
+ }
23
+ }
24
+ return tokens;
25
+ }
26
+ /** Jaccard 相似度(用于去重门与相似判断)。 */
27
+ export function jaccard(a, b) {
28
+ const setA = new Set(tokenize(a));
29
+ const setB = new Set(tokenize(b));
30
+ if (setA.size === 0 || setB.size === 0)
31
+ return 0;
32
+ let intersection = 0;
33
+ for (const token of setA) {
34
+ if (setB.has(token))
35
+ intersection++;
36
+ }
37
+ return intersection / (setA.size + setB.size - intersection);
38
+ }
39
+ /**
40
+ * 相关分:查询词与文档词的交集占比(查询归一)。
41
+ * 0 = 无关;越高越相关。查询为空时恒 0。
42
+ */
43
+ export function relevanceScore(query, doc) {
44
+ const queryTokens = new Set(tokenize(query));
45
+ if (queryTokens.size === 0)
46
+ return 0;
47
+ const docTokens = new Set(tokenize(doc));
48
+ let hits = 0;
49
+ for (const token of queryTokens) {
50
+ if (docTokens.has(token))
51
+ hits++;
52
+ }
53
+ return hits / (queryTokens.size + 1);
54
+ }
55
+ /**
56
+ * 检索注入:按与查询的相关分取 top-k;无关(score = 0)条目被过滤。
57
+ * 打分带迷你 IDF:查询词若出现在全部候选里(如「用户」),视为停用词不计分——
58
+ * 否则人人含「用户」的记忆会全部误命中。查询为空(会话首条前)返回前 k 条。
59
+ */
60
+ export function topKByRelevance(items, textOf, query, k) {
61
+ if (k <= 0 || items.length === 0)
62
+ return [];
63
+ if (!query)
64
+ return items.slice(0, k);
65
+ const queryTokens = [...new Set(tokenize(query))];
66
+ if (queryTokens.length === 0)
67
+ return items.slice(0, k);
68
+ const docTokens = items.map((item) => new Set(tokenize(textOf(item))));
69
+ const total = items.length;
70
+ const effective = [];
71
+ for (const token of queryTokens) {
72
+ const df = docTokens.filter((tokens) => tokens.has(token)).length;
73
+ if (df < total)
74
+ effective.push(token); // 全集合出现的词是停用词
75
+ }
76
+ if (effective.length === 0)
77
+ return items.slice(0, k);
78
+ const scored = [];
79
+ for (let i = 0; i < items.length; i++) {
80
+ let hits = 0;
81
+ for (const token of effective) {
82
+ if (docTokens[i].has(token))
83
+ hits++;
84
+ }
85
+ const score = hits / (effective.length + 1);
86
+ if (score > 0)
87
+ scored.push({ item: items[i], score });
88
+ }
89
+ scored.sort((a, b) => b.score - a.score);
90
+ return scored.slice(0, k).map((s) => s.item);
91
+ }
92
+ /**
93
+ * 少样本衰减:`max(min, base - turnIndex)`。
94
+ * 会话前几轮示例给足建立语气,之后模型历史里全是自己的发言,
95
+ * 语气自我维持,示例可以退到保底值。
96
+ */
97
+ export function decaySampleCount(base, turnIndex, min = 2) {
98
+ const floored = Math.max(min, base - Math.max(0, turnIndex));
99
+ return Math.min(floored, base);
100
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 确定性采样原语:让「同一会话 + 同一人设」永远得到同一组语料示例。
3
+ *
4
+ * 取代 v0.1.0 的 Math.random() 采样——那次采样让每次 prompt 构建
5
+ * 的示例都不同,会话内人设风格不稳定。这里改为以
6
+ * fnv1a32(`${sessionId}:${personaName}`) 为种子的确定性洗牌:
7
+ * 零缓存状态、跨重启稳定、纯函数可测。
8
+ */
9
+ /** FNV-1a 32 位字符串哈希。 */
10
+ export function fnv1a32(input) {
11
+ let hash = 0x811c9dc5;
12
+ for (let i = 0; i < input.length; i++) {
13
+ hash ^= input.charCodeAt(i);
14
+ hash = Math.imul(hash, 0x01000193);
15
+ }
16
+ return hash >>> 0;
17
+ }
18
+ /** mulberry32 PRNG:极小、确定性、足够洗牌用。 */
19
+ export function mulberry32(seed) {
20
+ let a = seed >>> 0;
21
+ return () => {
22
+ a = (a + 0x6d2b79f5) | 0;
23
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
24
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
25
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
26
+ };
27
+ }
28
+ /** 确定性 Fisher-Yates 抽样:相同 (entries, n, seed) 永远得到相同子集与顺序。 */
29
+ export function sampleBySeed(entries, n, seed) {
30
+ if (n >= entries.length)
31
+ return [...entries];
32
+ const count = Math.max(0, n);
33
+ if (count === 0)
34
+ return [];
35
+ const rand = mulberry32(seed);
36
+ const pool = [...entries];
37
+ for (let i = pool.length - 1; i > 0; i--) {
38
+ const j = Math.floor(rand() * (i + 1));
39
+ [pool[i], pool[j]] = [pool[j], pool[i]];
40
+ }
41
+ return pool.slice(0, count);
42
+ }
43
+ /** 会话级稳定采样:种子键为 `${sessionId}:${personaName}`。 */
44
+ export function sampleForSession(entries, n, sessionId, personaName) {
45
+ return sampleBySeed(entries, n, fnv1a32(`${sessionId}:${personaName}`));
46
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 消息文本提取(纯函数):从 Cordis 消息对象中提取纯文本。
3
+ *
4
+ * user/message 的 data 即消息内容;assistant/message 的 data.message 即消息内容。
5
+ */
6
+ export function messageText(message) {
7
+ const content = message?.content;
8
+ if (!Array.isArray(content))
9
+ return "";
10
+ const parts = [];
11
+ for (const block of content) {
12
+ const text = block?.text;
13
+ if (typeof text === "string")
14
+ parts.push(text);
15
+ }
16
+ return parts.join(" ").trim();
17
+ }
@@ -0,0 +1,32 @@
1
+ function labelOf(registry, personaName) {
2
+ if (!personaName)
3
+ return "默认风格";
4
+ const persona = registry.resolve(personaName);
5
+ return registry.profileNameOf(personaName) ?? persona?.displayName ?? personaName;
6
+ }
7
+ /**
8
+ * 组装切换播报文本。
9
+ *
10
+ * 结构:
11
+ * - divider:接班分隔行,用户可见的硬性切换标记
12
+ * - takeover:接手招呼(仅 greeting 时)
13
+ * - correction:升级纠偏(leakEscalated 时)
14
+ * - identity / coherence:身份与连贯性规则
15
+ */
16
+ export function composeBoundary(input) {
17
+ const { registry, previous, current, greeting, escalated } = input;
18
+ // 接班分隔行:对话流里的硬性切换标记,用户可见,同时是新人设的就位仪式。
19
+ // 放在播报第一句、用强制措辞——实测放在长句尾部会被模型忽略。
20
+ const divider = current
21
+ ? `第一件事:本条回复的第一行,一字不改地单独输出这一行:\n── 「${labelOf(registry, current)}」接手 ──\n这一行是给用户的切换提示,不算出戏;从第二行起再进入正文。`
22
+ : "";
23
+ const takeover = greeting
24
+ ? `${divider}正文第一句用简短的接手招呼,让用户明确听到换人了。`
25
+ : divider;
26
+ const correction = escalated
27
+ ? "特别纠偏:上一条回复仍在沿用旧人设的语气,这是偏差。本条回复必须完全按当前人设的契约说话——称呼、自称、口头禅、句式全部切换,不残留任何旧痕迹。"
28
+ : "";
29
+ const identity = "从本条回复起,你就是当前人设本人:任何场景下(包括被问「你是谁」)都按当前人设的身份自我介绍,此前关于「AI 助手」的自我认知一律让位。";
30
+ const coherence = "语气与风格的连贯性以「人设任期为界」:当前人设的任期内保持连贯;前任任期内的旧语气不构成任何连贯性义务,对话历史中旧人设的表达一律视为前任的,而不是你的。";
31
+ return `【人设切换】此前对话由「${labelOf(registry, previous)}」负责,现在由「${labelOf(registry, current)}」接手。${coherence}此前对话中助手的语气属于旧人设,一律不再延续、不要模仿;从本条回复起,严格按当前人设的风格契约说话。${identity}${correction}${takeover}`;
32
+ }