luckytri 1.3.2 → 1.3.3
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/docs/en/CHANGELOG.md +6 -0
- package/docs/zh/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/public/guide.en.html +3 -3
- package/public/guide.html +3 -3
- package/server/core/conversation-grounding.js +216 -1
- package/server/core/conversation-needs.js +10 -3
- package/server/core/dialogue-context.js +139 -1
- package/server/core/dialogue-contract.js +2 -2
- package/server/core/response-validator.js +1 -1
- package/server/core/turn-processor.js +2 -0
- package/server/core/turn.js +20 -1
- package/server/core/understanding.js +60 -4
- package/server/core/vision-manager.js +76 -7
- package/server/i18n/errors.js +2 -0
- package/server/mind/time/sharing.js +582 -482
- package/server/mind/time/works.js +51 -48
|
@@ -11,6 +11,15 @@ const GUESS = /可能|应该是|估计|大概|好像|也许|说不定|我猜|不
|
|
|
11
11
|
const OPINION = /我觉得|我认为|我感觉|在我看来|个人觉得|我更喜欢|还是.{0,6}好/;
|
|
12
12
|
const HEARSAY = /听说|据说|他说|她说|有人说|别人说|朋友说/;
|
|
13
13
|
const COMPLAIN = /烦死|好烦|气死|无语|崩溃|受不了|服了|累死|心累|难受/;
|
|
14
|
+
const QUESTION =
|
|
15
|
+
/[??]\s*$|(?:吗|呢|么|嘛)[??。!!~~]*$|(?:为什么|为啥|怎么|什么|哪|几点|多少|是不是|能不能|要不要|会不会|有没有|可不可以|啥意思)/;
|
|
16
|
+
const PROPOSAL = /要不|不如|建议|一起(?:写|做|看|玩)|写个|写一部|要不要/;
|
|
17
|
+
const FEELING = /想你|喜欢你|爱你|抱抱|好累|好开心|难过|生气|想哭/;
|
|
18
|
+
const SHARE = /你看|给你看|发来|贴了|分享|刚写|写完了|看这个/;
|
|
19
|
+
const CORRECTION = /不是|说错|理解错|记错|不对|纠正|搞错/;
|
|
20
|
+
const GREETING = /^(?:你好|早|晚安|在吗|嗨|哈喽|hello)/i;
|
|
21
|
+
const CLOSING = /^(?:嗯+|好(?:的|吧)?|行|对|晚安|拜拜|再见|hh+|哈+)$/i;
|
|
22
|
+
const IMAGE_ONLY = /^(?:\[(?:图片|表情|媒体)\]\s*)+$/;
|
|
14
23
|
|
|
15
24
|
export function messageCues(text) {
|
|
16
25
|
const value = String(text || "");
|
|
@@ -29,6 +38,22 @@ export function messageCues(text) {
|
|
|
29
38
|
return { tone, stance };
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
export function intentHint(text) {
|
|
42
|
+
const value = String(text || "").trim();
|
|
43
|
+
if (!value) return "other";
|
|
44
|
+
if (IMAGE_ONLY.test(value)) return "media";
|
|
45
|
+
const bare = value.replace(/\[提及成员\]|@\S+/g, "").trim();
|
|
46
|
+
if (CORRECTION.test(value) && value.length < 80) return "correction";
|
|
47
|
+
if (PROPOSAL.test(value)) return "proposal";
|
|
48
|
+
if (FEELING.test(value)) return "feeling";
|
|
49
|
+
if (SHARE.test(value)) return "share";
|
|
50
|
+
if (GREETING.test(bare)) return "greeting";
|
|
51
|
+
if (CLOSING.test(bare)) return "closing";
|
|
52
|
+
if (QUESTION.test(value)) return "question";
|
|
53
|
+
if (bare && bare.length <= 8) return "short-reply";
|
|
54
|
+
return "other";
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
// Consecutive messages from one person in the batch, read as one turn of
|
|
33
58
|
// speech: "我明天" + "要去面试" + "好紧张" is one sentence typed in pieces.
|
|
34
59
|
export function utterances(messages = [], batchIds = []) {
|
|
@@ -64,10 +89,28 @@ export function utterances(messages = [], batchIds = []) {
|
|
|
64
89
|
}));
|
|
65
90
|
}
|
|
66
91
|
|
|
92
|
+
export function openQuestions(messages = [], batchIds = []) {
|
|
93
|
+
const fresh = new Set(batchIds);
|
|
94
|
+
return messages
|
|
95
|
+
.filter(
|
|
96
|
+
(m) =>
|
|
97
|
+
fresh.has(m.id) &&
|
|
98
|
+
m.role === "user" &&
|
|
99
|
+
QUESTION.test(String(m.text || "").trim()),
|
|
100
|
+
)
|
|
101
|
+
.map((m) => ({
|
|
102
|
+
messageId: m.id,
|
|
103
|
+
speaker: m.speaker,
|
|
104
|
+
name: m.name,
|
|
105
|
+
text: String(m.text || "").slice(0, 120),
|
|
106
|
+
clue: "这是本轮未答的问题线索,understanding.point 应对齐其中之一",
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
67
110
|
export function understandingHints(messages = [], batchIds = []) {
|
|
68
111
|
const fresh = new Set(batchIds);
|
|
69
|
-
const
|
|
70
|
-
|
|
112
|
+
const rows = messages.filter((m) => fresh.has(m.id) && m.role === "user");
|
|
113
|
+
const cues = rows
|
|
71
114
|
.map((m) => ({ messageId: m.id, ...messageCues(m.text) }))
|
|
72
115
|
.filter((c) => c.tone.length || c.stance)
|
|
73
116
|
.map((c) => ({
|
|
@@ -75,11 +118,24 @@ export function understandingHints(messages = [], batchIds = []) {
|
|
|
75
118
|
...(c.tone.length ? { tone: c.tone } : {}),
|
|
76
119
|
...(c.stance ? { stance: c.stance } : {}),
|
|
77
120
|
}));
|
|
121
|
+
const intents = rows
|
|
122
|
+
.map((m) => ({
|
|
123
|
+
messageId: m.id,
|
|
124
|
+
intentHint: intentHint(m.text),
|
|
125
|
+
...(intentHint(m.text) === "question"
|
|
126
|
+
? { pointClue: String(m.text || "").slice(0, 80) }
|
|
127
|
+
: {}),
|
|
128
|
+
}))
|
|
129
|
+
.filter((row) => row.intentHint !== "other");
|
|
78
130
|
const groups = utterances(messages, batchIds);
|
|
79
|
-
|
|
131
|
+
const questions = openQuestions(messages, batchIds);
|
|
132
|
+
if (!cues.length && !groups.length && !intents.length && !questions.length)
|
|
133
|
+
return null;
|
|
80
134
|
return {
|
|
81
135
|
...(groups.length ? { utterances: groups } : {}),
|
|
82
136
|
...(cues.length ? { surfaceCues: cues } : {}),
|
|
83
|
-
|
|
137
|
+
...(intents.length ? { intents } : {}),
|
|
138
|
+
...(questions.length ? { openQuestions: questions } : {}),
|
|
139
|
+
note: "从字面看出的语气、意图与未答问题线索,不是结论:反讽、玩笑要结合前后文;连续几条是同一个人分开打的一句话;point 最终由你填写并对齐。",
|
|
84
140
|
};
|
|
85
141
|
}
|
|
@@ -518,7 +518,12 @@ export function classifyVision(db, sessionId, images) {
|
|
|
518
518
|
const pending = [];
|
|
519
519
|
for (const image of images || []) {
|
|
520
520
|
const description = recallVision(db, visionCacheKeys(sessionId, image));
|
|
521
|
-
if (description)
|
|
521
|
+
if (description)
|
|
522
|
+
cached.push({
|
|
523
|
+
messageId: image.messageId,
|
|
524
|
+
description: sanitizeVisionDescription(description),
|
|
525
|
+
speaker: image.speaker,
|
|
526
|
+
});
|
|
522
527
|
else pending.push(image);
|
|
523
528
|
}
|
|
524
529
|
return { cached, pending };
|
|
@@ -538,8 +543,13 @@ export function classifyLoaded(
|
|
|
538
543
|
const keys = visionCacheKeys(sessionId, image);
|
|
539
544
|
const description = recallVision(db, keys);
|
|
540
545
|
if (description) {
|
|
541
|
-
|
|
542
|
-
|
|
546
|
+
const clean = sanitizeVisionDescription(description);
|
|
547
|
+
cached.push({
|
|
548
|
+
messageId: image.messageId,
|
|
549
|
+
description: clean,
|
|
550
|
+
speaker: image.speaker,
|
|
551
|
+
});
|
|
552
|
+
saveVision(db, sessionId, [{ image, description: clean }]);
|
|
543
553
|
continue;
|
|
544
554
|
}
|
|
545
555
|
const quoted = !batch.has(image.messageId);
|
|
@@ -556,7 +566,8 @@ export function descriptionsFor(images, result) {
|
|
|
556
566
|
for (const row of rows) {
|
|
557
567
|
if (!row || typeof row.description !== "string" || !row.description.trim())
|
|
558
568
|
continue;
|
|
559
|
-
const description = row.description
|
|
569
|
+
const description = sanitizeVisionDescription(row.description);
|
|
570
|
+
if (!description) continue;
|
|
560
571
|
const key = String(row.messageId);
|
|
561
572
|
if (!byMessage.has(key)) byMessage.set(key, []);
|
|
562
573
|
byMessage.get(key).push(description);
|
|
@@ -573,6 +584,41 @@ export function descriptionsFor(images, result) {
|
|
|
573
584
|
return paired;
|
|
574
585
|
}
|
|
575
586
|
|
|
587
|
+
// Drop topic / intent guesses that leak into cacheable image notes. Visible
|
|
588
|
+
// content stays; neighbouring chat topics must not become part of the picture.
|
|
589
|
+
const VISION_TOPIC_BLEED =
|
|
590
|
+
/发送意图|发图意图|发送人.{0,24}(?:正参与|正在|当时|此刻).{0,48}(?:聊天|话题|讨论)|当时正(?:在|参与).{0,48}(?:聊天|话题|讨论)|参与关于.{0,48}的聊天|跟[「『"“'].{0,40}[」』"”'].{0,12}放一起|自动归到|属于当前话题|延续(?:了)?(?:当前|前面|之前)?.{0,12}话题|结合前后文推断|从语境看发送/;
|
|
591
|
+
|
|
592
|
+
export function sanitizeVisionDescription(value) {
|
|
593
|
+
return String(value || "")
|
|
594
|
+
.split(/(?<=[。!?\n])/)
|
|
595
|
+
.map((part) => part.trim())
|
|
596
|
+
.filter((part) => part && !VISION_TOPIC_BLEED.test(part))
|
|
597
|
+
.join("")
|
|
598
|
+
.trim()
|
|
599
|
+
.slice(0, 1000);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function messageOwner(snapshot, messageId) {
|
|
603
|
+
const rows = [
|
|
604
|
+
...(snapshot?.messages || []),
|
|
605
|
+
...(snapshot?.sourceRows || []).map((row) => ({
|
|
606
|
+
id: row.seq,
|
|
607
|
+
speaker: row.userId,
|
|
608
|
+
name: row.name,
|
|
609
|
+
})),
|
|
610
|
+
];
|
|
611
|
+
const hit = rows.find(
|
|
612
|
+
(row) => row.id === messageId || String(row.id) === String(messageId),
|
|
613
|
+
);
|
|
614
|
+
return hit
|
|
615
|
+
? {
|
|
616
|
+
speaker: hit.speaker ?? hit.userId,
|
|
617
|
+
name: hit.name || "",
|
|
618
|
+
}
|
|
619
|
+
: {};
|
|
620
|
+
}
|
|
621
|
+
|
|
576
622
|
export function mergeVision(snapshot, observations) {
|
|
577
623
|
const prev = Array.isArray(snapshot.vision?.observations)
|
|
578
624
|
? snapshot.vision.observations
|
|
@@ -583,15 +629,38 @@ export function mergeVision(snapshot, observations) {
|
|
|
583
629
|
);
|
|
584
630
|
for (const item of observations || []) {
|
|
585
631
|
if (!item?.description) continue;
|
|
586
|
-
const
|
|
632
|
+
const description = sanitizeVisionDescription(item.description);
|
|
633
|
+
if (!description) continue;
|
|
634
|
+
const key = `${item.messageId}\n${description}`;
|
|
587
635
|
if (seen.has(key)) continue;
|
|
588
636
|
seen.add(key);
|
|
637
|
+
const owner = messageOwner(snapshot, item.messageId);
|
|
589
638
|
next.push({
|
|
590
639
|
messageId: item.messageId,
|
|
591
|
-
description
|
|
640
|
+
description,
|
|
641
|
+
...(item.speaker != null || owner.speaker != null
|
|
642
|
+
? { speaker: item.speaker ?? owner.speaker }
|
|
643
|
+
: {}),
|
|
644
|
+
...(item.name || owner.name ? { name: item.name || owner.name } : {}),
|
|
592
645
|
});
|
|
593
646
|
}
|
|
594
|
-
|
|
647
|
+
// Re-attach ownership on every merge so cached notes keep a sender.
|
|
648
|
+
for (const row of next) {
|
|
649
|
+
const owner = messageOwner(snapshot, row.messageId);
|
|
650
|
+
if (row.speaker == null && owner.speaker != null)
|
|
651
|
+
row.speaker = owner.speaker;
|
|
652
|
+
if (!row.name && owner.name) row.name = owner.name;
|
|
653
|
+
row.description = sanitizeVisionDescription(row.description);
|
|
654
|
+
}
|
|
655
|
+
const kept = next.filter((row) => row.description);
|
|
656
|
+
if (kept.length) snapshot.vision = { observations: kept };
|
|
657
|
+
else delete snapshot.vision;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export function enrichVisionOwnership(snapshot) {
|
|
661
|
+
if (!snapshot?.vision?.observations?.length) return snapshot;
|
|
662
|
+
mergeVision(snapshot, []);
|
|
663
|
+
return snapshot;
|
|
595
664
|
}
|
|
596
665
|
|
|
597
666
|
function compactMessage(message) {
|
package/server/i18n/errors.js
CHANGED
|
@@ -564,6 +564,8 @@ export const ERRORS = {
|
|
|
564
564
|
"The work's source does not allow sharing in this session",
|
|
565
565
|
需要明确分享选择: "A clear sharing choice is needed",
|
|
566
566
|
成果整理格式无效: "The result-sorting format is invalid",
|
|
567
|
+
"汇报缺少分享开场,需标明这是在分享自己的作品":
|
|
568
|
+
"The report needs a sharing opener that makes clear she is sharing her own work",
|
|
567
569
|
汇报涉及私下内容: "The report touches private content",
|
|
568
570
|
需要几句自然汇报: "A few natural sentences of report are needed",
|
|
569
571
|
汇报需要简短的一句一句说:
|