luckytri 1.0.8 → 1.1.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/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/en/CHANGELOG.md +44 -24
- package/docs/en/architecture.md +4 -0
- package/docs/en/cognition.md +34 -0
- package/docs/en/guide.md +8 -6
- package/docs/zh/CHANGELOG.md +44 -24
- package/docs/zh/architecture.md +4 -0
- package/docs/zh/cognition.md +34 -0
- package/docs/zh/guide.md +8 -6
- package/package.json +112 -112
- package/public/app/assets/ChatsPage-DH4ZPRXB.js +3 -0
- package/public/app/assets/{Empty-C7yzOBzy.js → Empty-DRsHeX-L.js} +1 -1
- package/public/app/assets/Field-BfIUcaNa.js +1 -0
- package/public/app/assets/HeartPage-BxgSUJJ_.js +1 -0
- package/public/app/assets/LifePage-CAKr8h_q.js +1 -0
- package/public/app/assets/MemoryPage-CFq8L_TC.js +1 -0
- package/public/app/assets/NaturePage-BU1iDno3.js +3 -0
- package/public/app/assets/PeoplePage-C6i_nAfr.js +1 -0
- package/public/app/assets/PluginsPage-DRV8lzk3.js +2 -0
- package/public/app/assets/SystemPage-21AlQBiC.js +1 -0
- package/public/app/assets/SystemPage-CMcuUdLm.css +1 -0
- package/public/app/assets/TimePage-DUzjLnZV.js +3 -0
- package/public/app/assets/index-WAvrlAcO.js +4 -0
- package/public/app/assets/{knowledge-BJOIyuJo.js → knowledge-V_blbx9R.js} +1 -1
- package/public/app/assets/media-C7j5HqxK.js +1 -0
- package/public/app/assets/{mind-DtTVrW6Z.js → mind-BBAstMqH.js} +1 -1
- package/public/app/index.html +3 -3
- package/public/guide.en.html +13 -5
- package/public/guide.html +12 -5
- package/public/tutorial.en.md +3 -1
- package/public/tutorial.md +3 -1
- package/server/core/backoff.js +123 -0
- package/server/core/context-builder.js +307 -288
- package/server/core/contribution-check.js +50 -0
- package/server/core/conversation-cues.js +377 -385
- package/server/core/conversation-grounding.js +547 -304
- package/server/core/dialogue-context.js +247 -244
- package/server/core/dialogue-contract.js +33 -9
- package/server/core/embedding.js +246 -0
- package/server/core/expression-strategy.js +90 -0
- package/server/core/initiative-context.js +88 -68
- package/server/core/model-json.js +223 -190
- package/server/core/model-manager.js +256 -56
- package/server/core/model-presets.js +1045 -900
- package/server/core/orchestrator.js +41 -3
- package/server/core/persona-manager.js +364 -359
- package/server/core/reader-check.js +166 -62
- package/server/core/reply-delivery.js +688 -537
- package/server/core/repository.js +36 -0
- package/server/core/response-generator.js +101 -92
- package/server/core/response-validator.js +18 -3
- package/server/core/retired-prompts.js +53 -51
- package/server/core/time-expressions.js +143 -0
- package/server/core/turn-processor.js +175 -5
- package/server/core/turn.js +299 -44
- package/server/core/understanding.js +85 -0
- package/server/core/working-memory.js +149 -0
- package/server/i18n/decisions.js +225 -209
- package/server/i18n/errors.js +14 -0
- package/server/i18n/models.js +120 -111
- package/server/knowledge/api.js +362 -342
- package/server/knowledge/embedding-profile.js +158 -114
- package/server/knowledge/manager.js +6 -1
- package/server/mind/api.js +68 -0
- package/server/mind/continuity.js +634 -562
- package/server/mind/day-planner.js +52 -3
- package/server/mind/episodes.js +242 -0
- package/server/mind/expression-grounding.js +24 -21
- package/server/mind/index.js +13 -2
- package/server/mind/initiative.js +356 -338
- package/server/mind/life/index.js +6 -0
- package/server/mind/meetings.js +1112 -1069
- package/server/mind/memory.js +1091 -888
- package/server/mind/recall/index.js +776 -0
- package/server/mind/recall/schema.js +132 -0
- package/server/mind/recall/sources.js +220 -0
- package/server/mind/recall/store.js +368 -0
- package/server/mind/salience.js +202 -201
- package/server/mind/schema.js +568 -567
- package/server/mind/self.js +537 -496
- package/server/mind/time/activity-recall.js +65 -0
- package/server/mind/time/index.js +6 -2
- package/server/mind/time/tasks.js +36 -10
- package/server/mind/time/works.js +528 -527
- package/server/mind/view.js +63 -17
- package/server/storage/archive.js +1 -1
- package/public/app/assets/ChatsPage-CshdtTzV.js +0 -3
- package/public/app/assets/Field-BjFv4Rfn.js +0 -1
- package/public/app/assets/HeartPage-eqo6ru16.js +0 -1
- package/public/app/assets/LifePage-CN8GE5NK.js +0 -1
- package/public/app/assets/MemoryPage-CCpMN4Wg.js +0 -1
- package/public/app/assets/NaturePage-C0JqdBTR.js +0 -3
- package/public/app/assets/PeoplePage-BXGBbO4u.js +0 -1
- package/public/app/assets/PluginsPage-Dksp5AV5.js +0 -2
- package/public/app/assets/SystemPage-BenIyTvk.css +0 -1
- package/public/app/assets/SystemPage-Cd7aa8eE.js +0 -1
- package/public/app/assets/TimePage-Bs_zImb4.js +0 -3
- package/public/app/assets/index-CyRtYxP3.js +0 -4
- package/public/app/assets/media-Bwtx8blz.js +0 -1
package/server/core/turn.js
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { replyFocus } from "./conversation-cues.js";
|
|
2
2
|
import { INITIATIVE_PROMPT } from "../mind/initiative.js";
|
|
3
3
|
import { initiativeContext } from "./initiative-context.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
conversationGrounding,
|
|
6
|
+
prefersSingleBubble,
|
|
7
|
+
} from "./conversation-grounding.js";
|
|
5
8
|
import {
|
|
6
9
|
currentExchange,
|
|
7
10
|
dialogueContext,
|
|
8
11
|
explicitStop,
|
|
9
12
|
} from "./dialogue-context.js";
|
|
10
13
|
import { readerQuestions } from "./reader-check.js";
|
|
14
|
+
import {
|
|
15
|
+
botExchange,
|
|
16
|
+
CONTRIBUTION_CHECK,
|
|
17
|
+
CONTRIBUTION_REPAIR,
|
|
18
|
+
} from "./contribution-check.js";
|
|
19
|
+
import {
|
|
20
|
+
claimsLivedAction,
|
|
21
|
+
EXPRESSION_GROUNDING,
|
|
22
|
+
} from "../mind/expression-grounding.js";
|
|
11
23
|
|
|
12
24
|
export const CHOICES = ["speak", "react", "decline", "silent"];
|
|
25
|
+
const INTENT_KINDS = [
|
|
26
|
+
"question",
|
|
27
|
+
"proposal",
|
|
28
|
+
"feeling",
|
|
29
|
+
"sharing",
|
|
30
|
+
"correction",
|
|
31
|
+
"greeting",
|
|
32
|
+
"closing",
|
|
33
|
+
"other",
|
|
34
|
+
];
|
|
35
|
+
export const TONES = ["serious", "joking", "teasing", "ironic", "venting"];
|
|
36
|
+
export const STANCES = ["fact", "guess", "opinion", "joke", "hearsay"];
|
|
13
37
|
const LEGACY = {
|
|
14
38
|
REPLY: "speak",
|
|
15
39
|
MULTI_MESSAGE: "speak",
|
|
@@ -17,8 +41,22 @@ const LEGACY = {
|
|
|
17
41
|
SILENT: "silent",
|
|
18
42
|
};
|
|
19
43
|
|
|
20
|
-
export function maxBubbles(choice) {
|
|
21
|
-
|
|
44
|
+
export function maxBubbles(choice, snapshot) {
|
|
45
|
+
if (choice !== "speak") return 1;
|
|
46
|
+
return prefersSingleBubble(snapshot || {}) ? 1 : 3;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function joinBubbleParts(parts) {
|
|
50
|
+
return parts.reduce((joined, part) => {
|
|
51
|
+
if (!joined) return part;
|
|
52
|
+
if (/^(?:[-*•]|\d+[.)、])\s/u.test(part)) return `${joined}\n${part}`;
|
|
53
|
+
if (/[A-Za-z0-9]$/u.test(joined) && /^[A-Za-z0-9]/u.test(part))
|
|
54
|
+
return `${joined} ${part}`;
|
|
55
|
+
if (/[,、::]$/u.test(joined) || /^[,、;;]/u.test(part))
|
|
56
|
+
return `${joined}${part}`;
|
|
57
|
+
if (/[。!?!?;;…)」』】》]$/u.test(joined)) return `${joined}${part}`;
|
|
58
|
+
return `${joined},${part}`;
|
|
59
|
+
}, "");
|
|
22
60
|
}
|
|
23
61
|
|
|
24
62
|
const ACTION_NAME = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]{0,40}$/;
|
|
@@ -73,7 +111,8 @@ export function recallWording(snapshot) {
|
|
|
73
111
|
}
|
|
74
112
|
|
|
75
113
|
// One look at the conversation: what it means to her, how it moves her, and
|
|
76
|
-
// whether and how she answers.
|
|
114
|
+
// whether and how she answers. A bot exchange gets at most one repair pass
|
|
115
|
+
// when its first draft repeats old promises instead of answering new content.
|
|
77
116
|
export async function takeTurn(
|
|
78
117
|
models,
|
|
79
118
|
profile,
|
|
@@ -92,35 +131,179 @@ export async function takeTurn(
|
|
|
92
131
|
: snapshot.unavailableImages?.length
|
|
93
132
|
? "本轮图片没有读取成功,不要编造画面内容。"
|
|
94
133
|
: undefined;
|
|
95
|
-
|
|
96
|
-
profile,
|
|
97
|
-
"turn",
|
|
134
|
+
const turnSystem =
|
|
98
135
|
extra.occasion && ["presence", "outreach"].includes(extra.occasion.type)
|
|
99
136
|
? `${system}\n${INITIATIVE_PROMPT}`
|
|
100
|
-
: system
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
: system;
|
|
138
|
+
const turnInput = {
|
|
139
|
+
context: initiating
|
|
140
|
+
? initiativeContext(snapshot, extra.occasion)
|
|
141
|
+
: dialogueContext(snapshot),
|
|
142
|
+
...(extra.occasion
|
|
143
|
+
? {
|
|
144
|
+
occasion: initiating
|
|
145
|
+
? {
|
|
146
|
+
...extra.occasion,
|
|
147
|
+
initiative: initiativeContext(snapshot, extra.occasion)
|
|
148
|
+
.initiative,
|
|
149
|
+
}
|
|
150
|
+
: extra.occasion,
|
|
151
|
+
}
|
|
152
|
+
: {}),
|
|
153
|
+
...(imageGuide ? { imageGuide } : {}),
|
|
154
|
+
...(extra.pressure ? { pressure: extra.pressure } : {}),
|
|
155
|
+
...(recallWording(snapshot) ? { guidance: recallWording(snapshot) } : {}),
|
|
156
|
+
...(!initiating ? { exchange: currentExchange(snapshot) } : {}),
|
|
157
|
+
};
|
|
158
|
+
let raw = await models.call(
|
|
159
|
+
profile,
|
|
160
|
+
"turn",
|
|
161
|
+
turnSystem,
|
|
162
|
+
turnInput,
|
|
121
163
|
trace,
|
|
122
164
|
images,
|
|
123
165
|
);
|
|
166
|
+
const words = (
|
|
167
|
+
Array.isArray(raw?.bubbles) ? raw.bubbles : [raw?.bubbles]
|
|
168
|
+
).filter((word) => typeof word === "string");
|
|
169
|
+
if (
|
|
170
|
+
snapshot.initiative &&
|
|
171
|
+
raw?.choice === "speak" &&
|
|
172
|
+
claimsLivedAction(words.join("\n"))
|
|
173
|
+
) {
|
|
174
|
+
// Reject a fictional action before experience() can turn its appraisal
|
|
175
|
+
// into a new memory. A self-initiated message has no unanswered request
|
|
176
|
+
// that would justify filling in a different story through rewrites.
|
|
177
|
+
let grounded;
|
|
178
|
+
try {
|
|
179
|
+
grounded = await models.call(
|
|
180
|
+
profile,
|
|
181
|
+
"validation",
|
|
182
|
+
EXPRESSION_GROUNDING,
|
|
183
|
+
{
|
|
184
|
+
candidate: { words },
|
|
185
|
+
currentLife: snapshot.inner?.currentLife || {},
|
|
186
|
+
notes: [],
|
|
187
|
+
self: [],
|
|
188
|
+
},
|
|
189
|
+
trace,
|
|
190
|
+
);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
trace?.steps?.push(`主动经历核对暂不可用,先不分享:${error.message}`);
|
|
193
|
+
}
|
|
194
|
+
if (trace)
|
|
195
|
+
trace.experienceReview = {
|
|
196
|
+
words: [...words],
|
|
197
|
+
ok: grounded?.ok === true,
|
|
198
|
+
reason: grounded?.reason || "实际经历尚未核实",
|
|
199
|
+
};
|
|
200
|
+
if (grounded?.ok !== true)
|
|
201
|
+
return {
|
|
202
|
+
...raw,
|
|
203
|
+
choice: "silent",
|
|
204
|
+
bubbles: [],
|
|
205
|
+
appraisal: "",
|
|
206
|
+
feelings: [],
|
|
207
|
+
bonds: [],
|
|
208
|
+
contribution: {
|
|
209
|
+
kind: "none",
|
|
210
|
+
point: "旧草稿中的新行动尚无实际记录支持",
|
|
211
|
+
},
|
|
212
|
+
reason: "这段念头的经历依据还没有核实,先留在自己这里",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const exchange = botExchange(snapshot, raw);
|
|
216
|
+
if (
|
|
217
|
+
exchange &&
|
|
218
|
+
(raw.choice === "speak" || raw.choice === "react") &&
|
|
219
|
+
raw.contribution?.kind !== "none"
|
|
220
|
+
) {
|
|
221
|
+
try {
|
|
222
|
+
let review = await models.call(
|
|
223
|
+
profile,
|
|
224
|
+
"validation",
|
|
225
|
+
CONTRIBUTION_CHECK,
|
|
226
|
+
{
|
|
227
|
+
botExchange: exchange,
|
|
228
|
+
reply: raw.bubbles || [],
|
|
229
|
+
plannedPoint: raw.contribution?.point || "",
|
|
230
|
+
},
|
|
231
|
+
trace,
|
|
232
|
+
);
|
|
233
|
+
if (trace && typeof review.hasContribution === "boolean")
|
|
234
|
+
trace.contributionReview = review;
|
|
235
|
+
if (review.hasContribution === false && review.shouldRepair === true) {
|
|
236
|
+
trace?.steps?.push("新问题或新段落被上一稿漏接,按当前内容重新想一次");
|
|
237
|
+
try {
|
|
238
|
+
const repaired = await models.call(
|
|
239
|
+
profile,
|
|
240
|
+
"turn",
|
|
241
|
+
`${turnSystem}\n${CONTRIBUTION_REPAIR}`,
|
|
242
|
+
{
|
|
243
|
+
...turnInput,
|
|
244
|
+
contributionRepair: {
|
|
245
|
+
previousReply: raw.bubbles || [],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
trace,
|
|
249
|
+
images,
|
|
250
|
+
);
|
|
251
|
+
const repairedExchange = botExchange(snapshot, repaired);
|
|
252
|
+
if (
|
|
253
|
+
repairedExchange &&
|
|
254
|
+
["speak", "react"].includes(repaired.choice) &&
|
|
255
|
+
repaired.contribution?.kind !== "none"
|
|
256
|
+
) {
|
|
257
|
+
const revisedReview = await models.call(
|
|
258
|
+
profile,
|
|
259
|
+
"validation",
|
|
260
|
+
CONTRIBUTION_CHECK,
|
|
261
|
+
{
|
|
262
|
+
botExchange: repairedExchange,
|
|
263
|
+
reply: repaired.bubbles || [],
|
|
264
|
+
plannedPoint: repaired.contribution?.point || "",
|
|
265
|
+
repairAttempt: true,
|
|
266
|
+
},
|
|
267
|
+
trace,
|
|
268
|
+
);
|
|
269
|
+
review = revisedReview;
|
|
270
|
+
}
|
|
271
|
+
if (trace) {
|
|
272
|
+
trace.contributionReview = review;
|
|
273
|
+
trace.contributionRepair = {
|
|
274
|
+
attempted: true,
|
|
275
|
+
succeeded: review.hasContribution === true,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (review.hasContribution === true) return repaired;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
trace?.steps?.push(`按新内容重答失败,本轮先不发:${error.message}`);
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
...raw,
|
|
284
|
+
choice: "silent",
|
|
285
|
+
bubbles: [],
|
|
286
|
+
feelings: [],
|
|
287
|
+
bonds: [],
|
|
288
|
+
contribution: { kind: "none", point: "新内容没有形成可发送的回复" },
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
if (review.hasContribution === false)
|
|
292
|
+
return {
|
|
293
|
+
...raw,
|
|
294
|
+
choice: "silent",
|
|
295
|
+
bubbles: [],
|
|
296
|
+
feelings: [],
|
|
297
|
+
bonds: [],
|
|
298
|
+
contribution: { kind: "none", point: "没有新的内容,不再换词确认" },
|
|
299
|
+
};
|
|
300
|
+
} catch (error) {
|
|
301
|
+
trace?.steps?.push(
|
|
302
|
+
`接话内容核对暂不可用,继续原有校验:${error.message}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return raw;
|
|
124
307
|
}
|
|
125
308
|
|
|
126
309
|
const ids = (value) =>
|
|
@@ -128,11 +311,44 @@ const ids = (value) =>
|
|
|
128
311
|
.map((id) => (typeof id === "string" && /^\d+$/.test(id) ? Number(id) : id))
|
|
129
312
|
.filter((id) => Number.isSafeInteger(id));
|
|
130
313
|
|
|
314
|
+
// A model sometimes closes an object too early or too late, leaving a
|
|
315
|
+
// top-level field inside understanding or contribution, or the reading
|
|
316
|
+
// fields outside. Put them back where the contract says they belong.
|
|
317
|
+
function liftMisplaced(raw, trace) {
|
|
318
|
+
const out = { ...raw };
|
|
319
|
+
let moved = false;
|
|
320
|
+
for (const key of ["understanding", "contribution"]) {
|
|
321
|
+
const inner = out[key];
|
|
322
|
+
if (!inner || typeof inner !== "object" || Array.isArray(inner)) continue;
|
|
323
|
+
for (const field of [
|
|
324
|
+
"choice",
|
|
325
|
+
"targetMessageIds",
|
|
326
|
+
"topic",
|
|
327
|
+
"bubbles",
|
|
328
|
+
"appraisal",
|
|
329
|
+
"reason",
|
|
330
|
+
])
|
|
331
|
+
if (out[field] === undefined && inner[field] !== undefined) {
|
|
332
|
+
out[field] = inner[field];
|
|
333
|
+
moved = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (out.understanding && typeof out.understanding === "object")
|
|
337
|
+
for (const field of ["tone", "stance"])
|
|
338
|
+
if (out[field] !== undefined && out.understanding[field] === undefined) {
|
|
339
|
+
out.understanding = { ...out.understanding, [field]: out[field] };
|
|
340
|
+
moved = true;
|
|
341
|
+
}
|
|
342
|
+
if (moved) trace?.steps?.push("回合输出有字段放错了层级,已按约定归位");
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
|
|
131
346
|
export function normalizeTurn(raw, snapshot, trace) {
|
|
132
347
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
133
348
|
throw SyntaxError("回合输出不是 JSON 对象");
|
|
134
349
|
const known = new Set((snapshot.messages || []).map((m) => m.id));
|
|
135
350
|
const batchIds = new Set(snapshot.batchIds || []);
|
|
351
|
+
raw = liftMisplaced(raw, trace);
|
|
136
352
|
let choice = CHOICES.includes(raw.choice)
|
|
137
353
|
? raw.choice
|
|
138
354
|
: LEGACY[raw.action] || null;
|
|
@@ -149,7 +365,11 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
149
365
|
}
|
|
150
366
|
if (
|
|
151
367
|
choice === "react" &&
|
|
152
|
-
readerQuestions(snapshot, {
|
|
368
|
+
readerQuestions(snapshot, {
|
|
369
|
+
choice,
|
|
370
|
+
targetMessageIds: targets,
|
|
371
|
+
understanding: raw.understanding,
|
|
372
|
+
}).some((q) => targets.includes(q.id))
|
|
153
373
|
) {
|
|
154
374
|
choice = "speak";
|
|
155
375
|
trace?.steps?.push("对方在要求解释,保留完整回答空间,不按极短反应截断");
|
|
@@ -173,6 +393,20 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
173
393
|
"这是一句完整回应,按正常发言保留,不为短反应标签改写原意",
|
|
174
394
|
);
|
|
175
395
|
}
|
|
396
|
+
const bubbleLimit = maxBubbles(choice, snapshot);
|
|
397
|
+
if (bubbles.length > bubbleLimit) {
|
|
398
|
+
if (choice === "speak" && bubbleLimit === 1) {
|
|
399
|
+
bubbles = [joinBubbleParts(bubbles)];
|
|
400
|
+
trace?.steps?.push("根据对方的偏好,把连续补充合成一条完整消息");
|
|
401
|
+
} else {
|
|
402
|
+
bubbles = bubbles.slice(0, bubbleLimit);
|
|
403
|
+
trace?.steps?.push(
|
|
404
|
+
choice === "react"
|
|
405
|
+
? "简短反应只留第一条气泡"
|
|
406
|
+
: "回复超过当前对方的分段偏好,已收束到允许的条数",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
176
410
|
if (choice === "silent") bubbles = [];
|
|
177
411
|
const crisisIds = ids(raw.crisis?.messageIds).filter((id) =>
|
|
178
412
|
batchIds.has(id),
|
|
@@ -189,10 +423,13 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
189
423
|
bubbles = [];
|
|
190
424
|
trace?.steps?.push("有人可能处在危机里,底线要求认真回应");
|
|
191
425
|
}
|
|
426
|
+
const grounding = conversationGrounding(snapshot);
|
|
427
|
+
const botFollowupLimit =
|
|
428
|
+
!crisis && !snapshot.initiative && grounding.botFollowupLimit;
|
|
192
429
|
const botLoop =
|
|
193
430
|
!crisis &&
|
|
194
431
|
!snapshot.initiative &&
|
|
195
|
-
(
|
|
432
|
+
(grounding.botLoop ||
|
|
196
433
|
(raw.contribution?.kind === "none" &&
|
|
197
434
|
!raw.act &&
|
|
198
435
|
!raw.share &&
|
|
@@ -204,10 +441,15 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
204
441
|
.every((m) =>
|
|
205
442
|
conversationGrounding(snapshot).botMessageIds.includes(m.id),
|
|
206
443
|
)));
|
|
207
|
-
|
|
444
|
+
const botYield = botLoop || botFollowupLimit;
|
|
445
|
+
if (botYield) {
|
|
208
446
|
choice = "silent";
|
|
209
447
|
bubbles = [];
|
|
210
|
-
trace?.steps?.push(
|
|
448
|
+
trace?.steps?.push(
|
|
449
|
+
botFollowupLimit
|
|
450
|
+
? "机器人之间已经连续说过两轮,这次没有新的直接提问或纠正,先留出空间"
|
|
451
|
+
: "机器人连续确认没有新内容,这段对话在此收住",
|
|
452
|
+
);
|
|
211
453
|
}
|
|
212
454
|
const stopped = !crisis && explicitStop(snapshot);
|
|
213
455
|
if (stopped) {
|
|
@@ -222,7 +464,7 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
222
464
|
return {
|
|
223
465
|
choice,
|
|
224
466
|
share:
|
|
225
|
-
!
|
|
467
|
+
!botYield &&
|
|
226
468
|
!stopped &&
|
|
227
469
|
raw.share &&
|
|
228
470
|
["send", "later", "decline"].includes(raw.share.choice)
|
|
@@ -233,7 +475,7 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
233
475
|
}
|
|
234
476
|
: null,
|
|
235
477
|
attention:
|
|
236
|
-
!
|
|
478
|
+
!botYield &&
|
|
237
479
|
raw.attention &&
|
|
238
480
|
["continue", "chat", "rest"].includes(raw.attention.action)
|
|
239
481
|
? {
|
|
@@ -241,20 +483,33 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
241
483
|
reason: text(raw.attention.reason, 120),
|
|
242
484
|
}
|
|
243
485
|
: { action: "continue" },
|
|
244
|
-
appraisal:
|
|
245
|
-
?
|
|
486
|
+
appraisal: botYield
|
|
487
|
+
? botFollowupLimit
|
|
488
|
+
? "这段互答已经有来有回,先给聊天留点空间。"
|
|
489
|
+
: "对方在确认先前的话,没有新的内容。"
|
|
246
490
|
: text(raw.appraisal, 200),
|
|
247
|
-
reason:
|
|
248
|
-
?
|
|
491
|
+
reason: botYield
|
|
492
|
+
? botFollowupLimit
|
|
493
|
+
? "连续互答两轮,眼下没有直接问题或纠正,先给群里留点空间。"
|
|
494
|
+
: "这段交流已经结束,先收住。"
|
|
249
495
|
: text(raw.reason || raw.appraisal, 300) || "此刻的判断",
|
|
250
|
-
topic:
|
|
496
|
+
topic: botYield ? "暂时停一下" : text(raw.topic, 40),
|
|
251
497
|
...(raw.understanding && typeof raw.understanding === "object"
|
|
252
498
|
? {
|
|
253
499
|
understanding: {
|
|
500
|
+
...(INTENT_KINDS.includes(raw.understanding.kind)
|
|
501
|
+
? { kind: raw.understanding.kind }
|
|
502
|
+
: {}),
|
|
254
503
|
messageIds: ids(raw.understanding.messageIds).filter((id) =>
|
|
255
504
|
batchIds.has(id),
|
|
256
505
|
),
|
|
257
506
|
point: text(raw.understanding.point, 180),
|
|
507
|
+
...(TONES.includes(raw.understanding.tone)
|
|
508
|
+
? { tone: raw.understanding.tone }
|
|
509
|
+
: {}),
|
|
510
|
+
...(STANCES.includes(raw.understanding.stance)
|
|
511
|
+
? { stance: raw.understanding.stance }
|
|
512
|
+
: {}),
|
|
258
513
|
},
|
|
259
514
|
}
|
|
260
515
|
: {}),
|
|
@@ -275,17 +530,17 @@ export function normalizeTurn(raw, snapshot, trace) {
|
|
|
275
530
|
),
|
|
276
531
|
],
|
|
277
532
|
evidenceIds: ids(raw.evidenceIds).filter((id) => known.has(id)),
|
|
278
|
-
feelings: (Array.isArray(raw.feelings) && !
|
|
533
|
+
feelings: (Array.isArray(raw.feelings) && !botYield ? raw.feelings : [])
|
|
279
534
|
.filter((f) => f && typeof f.feeling === "string")
|
|
280
535
|
.slice(0, 3),
|
|
281
|
-
bonds: (Array.isArray(raw.bonds) && !
|
|
536
|
+
bonds: (Array.isArray(raw.bonds) && !botYield ? raw.bonds : [])
|
|
282
537
|
.filter((b) => b && b.userId != null && typeof b.change === "string")
|
|
283
538
|
.map((b) => ({ ...b, evidence: ids(b.evidence) }))
|
|
284
539
|
.slice(0, 4),
|
|
285
540
|
crisis: crisis ? { clear: true, messageIds: crisisIds } : { clear: false },
|
|
286
|
-
act:
|
|
287
|
-
bubbles: bubbles.slice(0,
|
|
288
|
-
maxBubbles:
|
|
541
|
+
act: botYield || stopped ? null : normalizeAct(raw.act, trace),
|
|
542
|
+
bubbles: bubbles.slice(0, bubbleLimit),
|
|
543
|
+
maxBubbles: bubbleLimit,
|
|
289
544
|
};
|
|
290
545
|
}
|
|
291
546
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Cheap readings of the batch that a model would otherwise have to work out
|
|
2
|
+
// from scratch: one person's run of messages read as one utterance, and
|
|
3
|
+
// surface signs of tone and certainty. They are hints with their reason, not
|
|
4
|
+
// verdicts; the turn decides what was meant.
|
|
5
|
+
const LAUGH =
|
|
6
|
+
/哈{3,}|h{3,}|233+|笑死|绷不住|乐死|xswl|\[doge\]|(狗头)|\(狗头\)|🐶|😂|🤣/i;
|
|
7
|
+
const TEASE = /开玩笑|逗你|骗你的|假的啦|就随便说说|嘴硬|你行你上|鉴定为/;
|
|
8
|
+
const IRONY =
|
|
9
|
+
/呵呵|真行|可真(?:行|会|厉害)|好家伙|太(?:厉害|聪明)了吧|厉害了我的|谢谢你啊|那可太|笑死个人|真是谢谢/;
|
|
10
|
+
const GUESS = /可能|应该是|估计|大概|好像|也许|说不定|我猜|不确定|感觉是|似乎/;
|
|
11
|
+
const OPINION = /我觉得|我认为|我感觉|在我看来|个人觉得|我更喜欢|还是.{0,6}好/;
|
|
12
|
+
const HEARSAY = /听说|据说|他说|她说|有人说|别人说|朋友说/;
|
|
13
|
+
const COMPLAIN = /烦死|好烦|气死|无语|崩溃|受不了|服了|累死|心累|难受/;
|
|
14
|
+
|
|
15
|
+
export function messageCues(text) {
|
|
16
|
+
const value = String(text || "");
|
|
17
|
+
const tone = [];
|
|
18
|
+
if (LAUGH.test(value)) tone.push("laughing");
|
|
19
|
+
if (TEASE.test(value)) tone.push("teasing");
|
|
20
|
+
if (IRONY.test(value)) tone.push("maybe_ironic");
|
|
21
|
+
if (COMPLAIN.test(value)) tone.push("complaining");
|
|
22
|
+
const stance = HEARSAY.test(value)
|
|
23
|
+
? "hearsay"
|
|
24
|
+
: GUESS.test(value)
|
|
25
|
+
? "guess"
|
|
26
|
+
: OPINION.test(value)
|
|
27
|
+
? "opinion"
|
|
28
|
+
: null;
|
|
29
|
+
return { tone, stance };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Consecutive messages from one person in the batch, read as one turn of
|
|
33
|
+
// speech: "我明天" + "要去面试" + "好紧张" is one sentence typed in pieces.
|
|
34
|
+
export function utterances(messages = [], batchIds = []) {
|
|
35
|
+
const fresh = new Set(batchIds);
|
|
36
|
+
const rows = messages
|
|
37
|
+
.filter((m) => fresh.has(m.id) && m.role === "user")
|
|
38
|
+
.sort((a, b) => a.id - b.id);
|
|
39
|
+
const groups = [];
|
|
40
|
+
for (const m of rows) {
|
|
41
|
+
const last = groups.at(-1);
|
|
42
|
+
if (
|
|
43
|
+
last &&
|
|
44
|
+
last.speaker === String(m.speaker) &&
|
|
45
|
+
m.time - last.until < 90000
|
|
46
|
+
) {
|
|
47
|
+
last.ids.push(m.id);
|
|
48
|
+
last.text += ` ${m.text || ""}`;
|
|
49
|
+
last.until = m.time;
|
|
50
|
+
} else
|
|
51
|
+
groups.push({
|
|
52
|
+
speaker: String(m.speaker),
|
|
53
|
+
name: m.name,
|
|
54
|
+
ids: [m.id],
|
|
55
|
+
text: m.text || "",
|
|
56
|
+
until: m.time,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return groups
|
|
60
|
+
.filter((g) => g.ids.length > 1)
|
|
61
|
+
.map(({ until: _until, ...g }) => ({
|
|
62
|
+
...g,
|
|
63
|
+
text: g.text.trim().slice(0, 160),
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function understandingHints(messages = [], batchIds = []) {
|
|
68
|
+
const fresh = new Set(batchIds);
|
|
69
|
+
const cues = messages
|
|
70
|
+
.filter((m) => fresh.has(m.id) && m.role === "user")
|
|
71
|
+
.map((m) => ({ messageId: m.id, ...messageCues(m.text) }))
|
|
72
|
+
.filter((c) => c.tone.length || c.stance)
|
|
73
|
+
.map((c) => ({
|
|
74
|
+
messageId: c.messageId,
|
|
75
|
+
...(c.tone.length ? { tone: c.tone } : {}),
|
|
76
|
+
...(c.stance ? { stance: c.stance } : {}),
|
|
77
|
+
}));
|
|
78
|
+
const groups = utterances(messages, batchIds);
|
|
79
|
+
if (!cues.length && !groups.length) return null;
|
|
80
|
+
return {
|
|
81
|
+
...(groups.length ? { utterances: groups } : {}),
|
|
82
|
+
...(cues.length ? { surfaceCues: cues } : {}),
|
|
83
|
+
note: "从字面看出的语气与把握程度提示,不是结论:反讽、玩笑要结合前后文判断;连续几条是同一个人分开打的一句话。",
|
|
84
|
+
};
|
|
85
|
+
}
|