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.
@@ -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 cues = messages
70
- .filter((m) => fresh.has(m.id) && m.role === "user")
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
- if (!cues.length && !groups.length) return null;
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
- note: "从字面看出的语气与把握程度提示,不是结论:反讽、玩笑要结合前后文判断;连续几条是同一个人分开打的一句话。",
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) cached.push({ messageId: image.messageId, 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
- cached.push({ messageId: image.messageId, description });
542
- saveVision(db, sessionId, [{ image, description }]);
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.trim().slice(0, 1000);
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 key = `${item.messageId}\n${item.description}`;
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: String(item.description).slice(0, 1000),
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
- if (next.length) snapshot.vision = { observations: next };
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) {
@@ -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
  汇报需要简短的一句一句说: