luckytri 1.0.0 → 1.0.2

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.
Files changed (139) hide show
  1. package/.github/SECURITY.md +4 -4
  2. package/README.en.md +252 -250
  3. package/README.md +252 -250
  4. package/bin/luckytri.js +5 -1
  5. package/docs/en/CHANGELOG.md +8 -0
  6. package/docs/en/connect.md +18 -18
  7. package/docs/en/guide.md +202 -216
  8. package/docs/en/npm-release.md +25 -25
  9. package/docs/zh/CHANGELOG.md +8 -0
  10. package/docs/zh/connect.md +17 -17
  11. package/docs/zh/guide.md +202 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +110 -108
  14. package/plugins/sdk/index.d.ts +155 -155
  15. package/plugins/sdk/testing.js +58 -58
  16. package/plugins/weather/index.js +82 -82
  17. package/plugins/weather/luckytri-plugin.json +29 -29
  18. package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
  19. package/public/app/assets/Empty-DP6WMGHi.js +2 -0
  20. package/public/app/assets/Field-CtbdqaiB.js +1 -0
  21. package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
  22. package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
  23. package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
  24. package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
  25. package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
  26. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  27. package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
  29. package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
  30. package/public/app/assets/TimePage-7-awH5XL.js +3 -0
  31. package/public/app/assets/index-4rItIkD-.js +4 -0
  32. package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
  33. package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
  34. package/public/app/assets/media-SMfTT-yc.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +72 -99
  37. package/public/guide.html +44 -86
  38. package/public/tutorial.en.md +25 -39
  39. package/public/tutorial.md +25 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/import-data.js +128 -0
  43. package/scripts/launch.js +101 -94
  44. package/scripts/plugin.js +150 -150
  45. package/scripts/reset-password.js +22 -0
  46. package/scripts/runtime.js +25 -25
  47. package/scripts/service.js +56 -47
  48. package/scripts/setup.js +50 -50
  49. package/scripts/start-server.js +41 -41
  50. package/scripts/stop.js +36 -36
  51. package/server/app.js +112 -96
  52. package/server/auth.js +217 -0
  53. package/server/channels/contract.js +57 -57
  54. package/server/channels/hub.js +165 -165
  55. package/server/channels/index.js +28 -25
  56. package/server/channels/onebot/index.js +335 -336
  57. package/server/channels/onebot/token.js +22 -3
  58. package/server/core/capabilities.js +208 -208
  59. package/server/core/events.js +64 -60
  60. package/server/core/image-preprocessor.js +208 -208
  61. package/server/core/model-manager.js +1213 -1213
  62. package/server/core/net-guard.js +49 -49
  63. package/server/core/orchestrator.js +569 -569
  64. package/server/core/perceivers.js +91 -91
  65. package/server/core/persona-manager.js +243 -243
  66. package/server/core/repository.js +293 -293
  67. package/server/core/response-validator.js +508 -508
  68. package/server/core/turn-processor.js +630 -630
  69. package/server/core/turn.js +236 -236
  70. package/server/core/vision-manager.js +633 -633
  71. package/server/i18n/errors.js +548 -497
  72. package/server/i18n/labels.js +649 -653
  73. package/server/index.js +151 -102
  74. package/server/knowledge/manager.js +386 -386
  75. package/server/knowledge/schema.js +12 -8
  76. package/server/local-access.js +32 -0
  77. package/server/mind/anticipations.js +507 -507
  78. package/server/mind/api.js +703 -703
  79. package/server/mind/budget.js +152 -152
  80. package/server/mind/context-selection.js +61 -61
  81. package/server/mind/day-planner.js +324 -324
  82. package/server/mind/evidence.js +148 -148
  83. package/server/mind/index.js +445 -445
  84. package/server/mind/life/diary.js +786 -786
  85. package/server/mind/life/index.js +1049 -1049
  86. package/server/mind/observations.js +84 -84
  87. package/server/mind/own-day.js +324 -324
  88. package/server/mind/own-voice.js +318 -318
  89. package/server/mind/senses.js +53 -53
  90. package/server/mind/time/activity-clock.js +196 -196
  91. package/server/mind/time/availability.js +69 -69
  92. package/server/mind/time/commitment-review.js +198 -198
  93. package/server/mind/time/executor.js +512 -512
  94. package/server/mind/time/experiences.js +128 -128
  95. package/server/mind/time/games.js +562 -562
  96. package/server/mind/time/index.js +739 -739
  97. package/server/mind/time/intent.js +166 -166
  98. package/server/mind/time/kinds.js +171 -171
  99. package/server/mind/time/plugin-activity.js +207 -207
  100. package/server/mind/time/tasks.js +587 -587
  101. package/server/mind/util.js +110 -110
  102. package/server/mind/view.js +273 -273
  103. package/server/paths.js +102 -102
  104. package/server/plugins/api.js +204 -204
  105. package/server/plugins/host.js +967 -967
  106. package/server/plugins/install.js +295 -295
  107. package/server/plugins/manifest.js +201 -201
  108. package/server/plugins/permissions.js +42 -42
  109. package/server/plugins/runner/guard.mjs +36 -36
  110. package/server/plugins/runner/worker.mjs +250 -250
  111. package/server/plugins/schema.js +91 -91
  112. package/server/plugins/zip.js +112 -112
  113. package/server/startup-lock.js +118 -118
  114. package/server/storage/archive.js +242 -241
  115. package/server/storage/backup-cleanup-worker.js +8 -8
  116. package/server/storage/backup-cleanup.js +415 -414
  117. package/server/storage/backup-lock.js +76 -76
  118. package/server/storage/backup-scheduler.js +131 -131
  119. package/server/storage/data-package.js +298 -0
  120. package/server/storage/data-transfer-api.js +50 -0
  121. package/server/storage/data-transfer-worker.js +18 -0
  122. package/server/storage/data-transfer.js +415 -0
  123. package/server/storage/store.js +202 -199
  124. package/server/studio/readiness.js +181 -190
  125. package/server/studio/settings.js +236 -236
  126. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  127. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  128. package/public/app/assets/Field-D4gY-INU.js +0 -1
  129. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  130. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  131. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  132. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  133. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  134. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  135. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  136. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  137. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  138. package/public/app/assets/index-BheRW-1t.js +0 -4
  139. package/public/app/assets/media-BzPDBm7e.js +0 -1
@@ -1,508 +1,508 @@
1
- import {
2
- conversationalIssues,
3
- openedUnprompted,
4
- replyFocus,
5
- } from "./conversation-cues.js";
6
- import { gentlePersona } from "./persona-manager.js";
7
- import { initiativeContext } from "./initiative-context.js";
8
- import { interestTerms } from "../mind/attention.js";
9
- const norm = (s) => s.toLowerCase().replace(/[\s\p{P}\p{S}]/gu, "");
10
- export function contradictoryOwnWords(snapshot, decision = {}) {
11
- const targets = (snapshot.messages || []).filter((message) =>
12
- decision.targetMessageIds?.includes(message.id),
13
- );
14
- const words = (snapshot.inner?.continuity?.people || [])
15
- .filter((person) =>
16
- targets.some((message) => String(message.speaker) === String(person.id)),
17
- )
18
- .flatMap((person) =>
19
- (person.myElsewhereWords || []).map((line) => line.text || ""),
20
- );
21
- return (
22
- words.some((line) => /不是|没说|并非|不指/.test(line)) &&
23
- words.some((line) => /就是|确实|说的就是|指的就是/.test(line))
24
- );
25
- }
26
- // "你刚说……" is a claim about the last few minutes, so the room can settle it.
27
- // The one that went wrong was her own joke handed back as the other person's
28
- // words. Only a claim that shares nothing with anything that person said in the
29
- // window is refused; "你刚说的那个" points at their words without repeating them.
30
- const IMMEDIATE_CLAIM =
31
- /你(?:自己|才)?(?:刚(?:刚|才)?|方才)(?:自己)?(?:不是)?(?:明明)?(?:说|讲|提(?:到|过)?|发了?|打了?)(?:过)?(的|了)?[,,::\s]*["“「]?([^,。!?!?;;"”」\n]{2,30})/g;
32
- const POINTS_AT = /^(?:那|这|哪|什么|啥|话|事|东西|内容|意思|一句|一堆|一些)/;
33
- // "因为我看到你那条消息发过来" — a source for a line she opened on her own.
34
- const SEEN_HIS_MESSAGE =
35
- /(?<![没不未]有?)(?:看到|看见|收到|瞧见)了?你(?:这条|那条|刚才|刚刚)?(?:发(?:来|过来|出来)?)?的?(?:那条)?(?:消息|信息)|你(?:这条|那条)?(?:消息|信息)(?:发过来|发来|刚发)/;
36
- export function unsupportedImmediateClaim(text, snapshot, targetMessages) {
37
- const speakers = new Set(targetMessages.map((m) => String(m.speaker)));
38
- if (!speakers.size) return "";
39
- const said = interestTerms(
40
- (snapshot.messages || [])
41
- .filter((m) => m.role !== "assistant" && speakers.has(String(m.speaker)))
42
- .slice(-60)
43
- .map((m) => m.text || ""),
44
- );
45
- for (const match of String(text).matchAll(IMMEDIATE_CLAIM)) {
46
- const claim = match[2].trim();
47
- if (match[1] === "的" && POINTS_AT.test(claim)) continue;
48
- const terms = interestTerms([claim]);
49
- if (terms.size < 2) continue;
50
- if (![...terms].some((term) => said.has(term))) return claim;
51
- }
52
- return "";
53
- }
54
- // The other half of the same slip: "你自己说的……" followed by a line that was
55
- // her own earlier message and that nobody else said in the window.
56
- const THEY_SAID = /你(?:自己)?(?:刚|才|明明)?说(?:过|的)/;
57
- export function attributesOwnWordsToThem(text, snapshot) {
58
- if (!THEY_SAID.test(text)) return false;
59
- const said = norm(String(text).replace(THEY_SAID, ""));
60
- const recent = (snapshot.messages || []).slice(-40);
61
- const mine = recent
62
- .filter((m) => m.role === "assistant")
63
- .map((m) => norm(m.text || ""));
64
- const theirs = recent
65
- .filter((m) => m.role !== "assistant")
66
- .map((m) => norm(m.text || ""));
67
- let echoed = false;
68
- for (let i = 0; i + 6 <= said.length; i += 2) {
69
- const phrase = said.slice(i, i + 6);
70
- if (theirs.some((line) => line.includes(phrase))) return false;
71
- if (mine.some((line) => line.includes(phrase))) echoed = true;
72
- }
73
- return echoed;
74
- }
75
- // The reviewer judges one reply; it needs the turn and its recent lead-in,
76
- // not the summaries, recall or the rest of the transcript.
77
- export function reviewContext(snapshot, decision = {}) {
78
- if (snapshot.initiative) return initiativeContext(snapshot);
79
- const {
80
- persona: _persona,
81
- sourceRows: _sourceRows,
82
- batch: _batch,
83
- summaries: _summaries,
84
- summaryInstruction: _summaryInstruction,
85
- stages: _stages,
86
- recalled: _recalled,
87
- budget: _budget,
88
- historyStart: _historyStart,
89
- self: _self,
90
- inner: _inner,
91
- ...rest
92
- } = snapshot;
93
- const messages = snapshot.messages || [];
94
- const focus = new Set([
95
- ...(snapshot.batchIds || []),
96
- ...(decision.targetMessageIds || []),
97
- ...(decision.evidenceIds || []),
98
- ]);
99
- for (const m of messages)
100
- if (focus.has(m.id)) for (const id of m.replyChain || []) focus.add(id);
101
- const recent = new Set(messages.slice(-30).map((m) => m.id));
102
- return {
103
- ...rest,
104
- ...(snapshot.inner?.currentLife
105
- ? { currentLife: snapshot.inner.currentLife }
106
- : {}),
107
- ...(snapshot.inner?.continuity
108
- ? { continuity: snapshot.inner.continuity }
109
- : {}),
110
- messages: messages.filter((m) => focus.has(m.id) || recent.has(m.id)),
111
- };
112
- }
113
- const bubbleLimit = (decision) =>
114
- decision.maxBubbles ?? (decision.action === "MULTI_MESSAGE" ? 3 : 2);
115
- export function normalizeResponse(result, decision, fallback = "嗯") {
116
- const maxBubbles = bubbleLimit(decision);
117
- let bubbles = [];
118
- if (Array.isArray(result?.bubbles)) bubbles = result.bubbles;
119
- else if (typeof result?.bubbles === "string") bubbles = [result.bubbles];
120
- else
121
- for (const key of ["reply", "text", "content"])
122
- if (typeof result?.[key] === "string" && result[key].trim()) {
123
- bubbles = [result[key]];
124
- break;
125
- }
126
- bubbles = bubbles
127
- .filter((x) => typeof x === "string")
128
- .map((x) => x.trim())
129
- .filter(Boolean)
130
- .slice(0, maxBubbles);
131
- if (!bubbles.length) bubbles = [fallback];
132
- return {
133
- ...(result && typeof result === "object" ? result : {}),
134
- bubbles,
135
- reason:
136
- typeof result?.reason === "string" && result.reason.trim()
137
- ? result.reason
138
- : "短句回复",
139
- };
140
- }
141
- export function validateResponse(result, snapshot, decision, maxChars = 180) {
142
- const issues = [];
143
- const maxBubbles = bubbleLimit(decision);
144
- if (
145
- !Array.isArray(result.bubbles) ||
146
- result.bubbles.length < 1 ||
147
- result.bubbles.length > maxBubbles ||
148
- result.bubbles.some((x) => typeof x !== "string" || !x.trim())
149
- )
150
- return ["气泡格式无效"];
151
- if (result.bubbles.reduce((n, s) => n + Array.from(s).length, 0) > maxChars)
152
- issues.push("回复超过当前会话总字数");
153
- if (decision.choice === "react" && Array.from(result.bubbles[0]).length > 20)
154
- issues.push("选择了简短反应,只写一句不超过20字的短话");
155
- const recent = snapshot.messages
156
- .filter((m) => m.role === "assistant")
157
- .slice(-12)
158
- .map((m) => m.text);
159
- const continuity = snapshot.inner?.continuity;
160
- const sharedElsewhere =
161
- continuity?.requested &&
162
- continuity.people.some((person) =>
163
- person.places.some((place) => !place.current),
164
- );
165
- const pledgedTo = new Set(
166
- (continuity?.people || [])
167
- .filter(
168
- (person) =>
169
- person.myPrivateIntentions?.length ||
170
- person.recentShared?.some(
171
- (line) =>
172
- line.role === "assistant" &&
173
- /答应|说好|下次|以后|我会|我就|记住/.test(line.text),
174
- ) ||
175
- person.sharedMoments?.some(
176
- (moment) =>
177
- moment.privateOrigin &&
178
- moment.iSaid?.some((line) =>
179
- /答应|说好|下次|以后|我会|我就|记住/.test(line),
180
- ),
181
- ),
182
- )
183
- .map((person) => String(person.id)),
184
- );
185
- const targetMessages = snapshot.messages.filter((m) =>
186
- decision.targetMessageIds?.includes(m.id),
187
- );
188
- const pledgedTurn = targetMessages.some((m) =>
189
- pledgedTo.has(String(m.speaker)),
190
- );
191
- const affectionTurn = targetMessages.some(
192
- (m) =>
193
- (m.relation === "direct" ||
194
- /(?:^|:)private:/.test(snapshot.sessionId || "")) &&
195
- /(?:想你|喜欢你|爱你)/.test(m.text || ""),
196
- );
197
- const privateLines = (continuity?.people || []).flatMap((person) => [
198
- ...(person.recentShared || []).map((line) => line.text),
199
- ...(person.myPrivateIntentions || []).map((line) => line.content),
200
- ...(person.myElsewhereWords || [])
201
- .filter((line) => line.privateOrigin)
202
- .map((line) => line.text),
203
- ...(person.sharedMoments || [])
204
- .filter((moment) => moment.privateOrigin)
205
- .flatMap((moment) => [
206
- ...(moment.theySaid || []).map((line) => line.text),
207
- ...(moment.iSaid || []),
208
- ]),
209
- ]);
210
- const ownWordsConflict = contradictoryOwnWords(snapshot, decision);
211
- if (
212
- ownWordsConflict &&
213
- !/前后|说乱|不一致|矛盾|改口|两种说法|先.{0,24}后/.test(
214
- result.bubbles.join(" "),
215
- )
216
- )
217
- issues.push(
218
- "本人对同一件事留下相反说法,这次必须承认前后不一致,不能只挑一条旧话解释",
219
- );
220
- const publicWords = norm(
221
- snapshot.messages
222
- .filter((message) => message.role === "user")
223
- .slice(-12)
224
- .map((message) => message.text || "")
225
- .join(""),
226
- );
227
- for (const text of result.bubbles) {
228
- const spoken = norm(text);
229
- if (
230
- ownWordsConflict &&
231
- /从头到尾|始终|一直|只指|唯一|没在别处乱/.test(text)
232
- )
233
- issues.push(
234
- "本人在别处对同一称呼或关系有相反说法,不能声称从头到尾一致;承认前后说乱了",
235
- );
236
- if (
237
- privateLines.some((line) => {
238
- const privateWords = norm(line || "");
239
- for (let i = 0; i + 12 <= privateWords.length; i += 3) {
240
- const phrase = privateWords.slice(i, i + 12);
241
- if (spoken.includes(phrase) && !publicWords.includes(phrase))
242
- return true;
243
- }
244
- return false;
245
- })
246
- )
247
- issues.push("这句话照搬了只在私下说过的原话,不能在群里公开复述");
248
- if (
249
- pledgedTurn &&
250
- /(?:没|没有|不记得|从未).{0,8}(?:答应|承诺|说过)|(?:答应|承诺).{0,8}(?:没有|不存在)|(?:有|拿出).{0,4}证据|你记错了/.test(
251
- text,
252
- )
253
- )
254
- issues.push(
255
- "已有核实的本人承诺;不能否认、反问证据或把失约推给对方,简短承认并回应当下",
256
- );
257
- if (
258
- targetMessages.some((m) =>
259
- (continuity?.people || []).some(
260
- (person) =>
261
- String(person.id) === String(m.speaker) &&
262
- person.myElsewhereWords?.length,
263
- ),
264
- ) &&
265
- /(?:我)?(?:没|没有|从未|从来没|不记得).{0,10}(?:说过|说|喊过|喊|叫过|叫)/.test(
266
- text,
267
- )
268
- )
269
- issues.push(
270
- "别处已有本人真实发出的相关原话,不能否认自己说过;先核对当时对象再修正",
271
- );
272
- const claimed = unsupportedImmediateClaim(text, snapshot, targetMessages);
273
- if (claimed)
274
- issues.push(
275
- `说对方刚说过“${claimed}”,但本轮记录里对方没有这样说;不能把自己先说的话或别人的话安到对方头上,承认是自己顺口带出来的,或直接问他`,
276
- );
277
- if (attributesOwnWordsToThem(text, snapshot))
278
- issues.push(
279
- "你说是对方说的那句话,其实是你自己先说的;不能把自己的话安到对方头上,承认是自己先说的",
280
- );
281
- if (
282
- affectionTurn &&
283
- /(?:才见过|刚见过|不是才见|别闹到我|先别闹)/.test(text)
284
- )
285
- issues.push(
286
- "对方在直接表达想念;不要用见面时间或划水玩笑驳回这份表达,按当下关系自然回应",
287
- );
288
- if (
289
- sharedElsewhere &&
290
- /(?:不能|不算|不能因此).{0,24}(?:共同经历|共同相处)|(?:从来|从没|完全).{0,8}(?:没聊过|不记得|没相处过|没有共同经历)|(?:没在|没有在).{0,8}(?:别的|其他|其它).{0,8}群.{0,8}(?:聊|相处)|只有.{0,8}(?:这段|这次)私聊.{0,8}(?:经历|记录)/.test(
291
- text,
292
- )
293
- )
294
- issues.push(
295
- "已核实和同一个人在其他地方真正说过话,换到私聊不会抹掉共同经历;依据 continuity 中具体的旧事回应,不把公开相处说成别的实例或不能算共同经历",
296
- );
297
- if (
298
- snapshot.initiative &&
299
- /(?:收到|看到|看见)你(?:们)?(?:刚刚?|刚才|又|发来|来找我)|你(?:们)?(?:刚刚?|刚才|这会儿).{0,8}(?:说|问|发|提|找我)|(?:刚刚?|刚才|这会儿)你(?:们)?.{0,8}(?:说|问|发|提|找我)|你(?:又来|来找我|发来(?:了)?消息)/.test(
300
- text,
301
- )
302
- )
303
- issues.push(
304
- "本轮没有收到新消息,不能捏造对方刚说过、发过消息或来找你;若确实回忆旧事要用过去的时间",
305
- );
306
- if ((snapshot.persona?.forbidden || []).some((w) => w && text.includes(w)))
307
- issues.push("使用人格禁用表达");
308
- if (
309
- /[hH]{2,}[。!!~~]*$/.test(text) &&
310
- norm(text).length > 6 &&
311
- recent
312
- .slice(-5)
313
- .filter((r) => norm(r).length > 6 && /[hH]{2,}[。!!~~]*$/.test(r))
314
- .length >= 2
315
- )
316
- issues.push("不要把hh反复当句尾装饰;删掉装饰,保留具体回应");
317
- const briefReaction =
318
- /^(?:h{2,6}|哈{1,6}|嗯{1,3}|好|行|哦{1,3}|啊|对|是的|晚安)$/i.test(
319
- norm(text),
320
- );
321
- if (!briefReaction && recent.some((r) => norm(r) === norm(text)))
322
- issues.push("重复近期回复");
323
- if (
324
- gentlePersona(snapshot.persona) &&
325
- /你们?[^。!?]{0,10}(?:出糗|怎么这么|脑子|太菜|有病)|我怎么知道|现在又嫌我|你倒是|累傻了|甩锅|难伺候|你除了.*还会啥|你倒是说|被我说中了|活该|自找的|赚了个教训/.test(
326
- text,
327
- )
328
- )
329
- issues.push("低毒舌人格不质问、挖苦或揣测群友恶意");
330
- if (/又一张表情包|怎么连续几张|都不带字的|你们.*刷屏/.test(text))
331
- issues.push("不要把发图行为本身当话题点评");
332
- if (
333
- /你们是复读机|你真蠢|你(?:们)?(?:就是|真是|是个)?(?:弱智|废物)/.test(text)
334
- )
335
- issues.push("评价或攻击群友,过于刻薄");
336
- if (
337
- /我理解你的感受|作为一个AI|我只是(?:一个|个)?(?:AI|助手)|有什么需要帮助|你并不孤单|(?:骂吧|慢慢说|你继续)[,,。\s]*我听着/.test(
338
- text,
339
- )
340
- )
341
- issues.push("客服式套话");
342
- for (const phrase of ["笑死", "我服了", "不是哥们", "你们怎么回事", "经典"])
343
- if (
344
- text.includes(phrase) &&
345
- recent.filter((r) => r.includes(phrase)).length >= 2
346
- )
347
- issues.push(`近期反复使用“${phrase}”`);
348
- }
349
- if (new Set(result.bubbles.map(norm)).size !== result.bubbles.length)
350
- issues.push("气泡之间重复");
351
- const plans = (snapshot.inner?.expecting || [])
352
- .map((line) => String(line).match(/计划:(.+?)(/)?.[1])
353
- .filter(Boolean);
354
- const current = targetMessages.map((m) => m.text || "").join(" ");
355
- if (
356
- plans.length &&
357
- !/(?:已经|刚刚?|刚才).{0,12}(?:去|做|完成|回来)|(?:去过|做完|结束了)/.test(
358
- current,
359
- ) &&
360
- result.bubbles.some((line) => {
361
- if (!/(?:你|他|她).{0,12}(?:不是|已经).{0,18}(?:了|完|回来)/.test(line))
362
- return false;
363
- const said = interestTerms([line]);
364
- return plans.some(
365
- (plan) =>
366
- [...interestTerms([plan])].filter((term) => said.has(term)).length >=
367
- 2,
368
- );
369
- })
370
- )
371
- issues.push(
372
- "这件事只记录为对方的安排,尚未确认已经发生;不要用‘你不是已经……了吗’当成事实",
373
- );
374
- const focus = replyFocus(snapshot, decision);
375
- const questioned = targetMessages.at(-1);
376
- if (focus.kind === "clarify_claim" && questioned) {
377
- const cue = interestTerms([questioned.text || ""]);
378
- const earlierToSomeoneElse = (snapshot.messages || []).filter(
379
- (message) =>
380
- message.role === "assistant" &&
381
- message.id < questioned.id &&
382
- (message.replyTargets || []).length &&
383
- (message.replyTargets || []).every(
384
- (target) => String(target.speaker) !== String(questioned.speaker),
385
- ) &&
386
- [...interestTerms([message.text || ""])].some((term) => cue.has(term)),
387
- );
388
- if (
389
- earlierToSomeoneElse.length &&
390
- result.bubbles.some((line) =>
391
- /从头到尾.{0,16}(?:你|都是)|一直.{0,12}(?:你|只)|你自己.{0,8}(?:听岔|记错)/.test(
392
- line,
393
- ),
394
- )
395
- )
396
- issues.push(
397
- "相关旧话有明确的另一位回复对象,不能说从头到尾只对眼前人说或责怪对方听错",
398
- );
399
- if (
400
- /主人/.test(questioned.text || "") &&
401
- earlierToSomeoneElse.some((message) =>
402
- /你主人|他的主人/.test(message.text || ""),
403
- )
404
- ) {
405
- const line = result.bubbles.join(" ");
406
- if (
407
- /指的就是.{0,10}你|说的就是.{0,10}你|你.{0,8}是.{0,8}(?:他|别人).{0,4}主人|(?:他|别人).{0,4}主人.{0,6}是你/.test(
408
- line,
409
- )
410
- )
411
- issues.push(
412
- "旧话是对第三人说的,但没有证据证明眼前人是第三人的主人;承认当时说错,不能继续补造归属",
413
- );
414
- if (/你.{0,8}(?:对号入座|误会|听岔|理解错)/.test(line))
415
- issues.push("自己把无依据的人际关系说出口,不能让追问者承担误解责任");
416
- const claimedEarlierQuestion =
417
- /(?:是|因为|顺着).{0,20}(?:问我|问的是).{0,12}(?:养我|谁.{0,2}养|主人)/.test(
418
- line,
419
- );
420
- if (claimedEarlierQuestion) {
421
- const targets = new Set(
422
- earlierToSomeoneElse.flatMap((message) =>
423
- (message.replyTargets || []).map((target) => target.messageId),
424
- ),
425
- );
426
- const actuallyAsked = (snapshot.messages || []).some(
427
- (message) =>
428
- targets.has(message.id) &&
429
- /养你|养我|主人/.test(message.text || ""),
430
- );
431
- if (!actuallyAsked)
432
- issues.push(
433
- "对方是在旧话之后才追问主人,不能倒置时间说那句旧话是在回答这个问题",
434
- );
435
- }
436
- }
437
- }
438
- if (focus.kind === "basis_check" && questioned) {
439
- const opener = (snapshot.messages || [])
440
- .filter((m) => m.role === "assistant" && m.id < questioned.id)
441
- .at(-1);
442
- if (
443
- openedUnprompted(snapshot.messages, opener) &&
444
- result.bubbles.some((line) => SEEN_HIS_MESSAGE.test(line))
445
- )
446
- issues.push(
447
- "那句是你自己先开口的,在那之前他没有新发来消息;依据只能是你的推测或记得的事,不能说成是看到了他的消息",
448
- );
449
- }
450
- if (
451
- [
452
- "repair",
453
- "acknowledge",
454
- "promise_check",
455
- "clarify_claim",
456
- "basis_check",
457
- ].includes(focus.kind) &&
458
- result.bubbles.length > 1
459
- )
460
- issues.push("这一轮只是纠正或确认,一句收住,不要追加原话题或解释");
461
- if (focus.kind === "acknowledge" && result.bubbles.join("").length > 8)
462
- issues.push("对方只确认了一下,不需要再评论或劝慰,用很短的回应收住");
463
- if (
464
- focus.kind === "acknowledge" &&
465
- result.bubbles.some((line) => /早点|休息|歇着|去睡|注意身体/.test(line))
466
- )
467
- issues.push("对方只是确认,不要无请求地安排休息或继续给建议");
468
- const selfName = String(snapshot.persona?.name || "").trim();
469
- if (
470
- focus.kind === "repair" &&
471
- selfName &&
472
- result.bubbles.some((line) =>
473
- ["复读的是", "重复的是", "说错的是"].some((prefix) =>
474
- line.includes(prefix + selfName),
475
- ),
476
- )
477
- )
478
- issues.push(
479
- "历史 assistant 是自己已发的原话;被指出重复时不能说成另一个 bot 的错",
480
- );
481
- if (
482
- focus.kind === "acknowledge" &&
483
- result.bubbles.some((line) =>
484
- /你(?:自己|刚才|之前|前面).{0,6}(?:说|提)|你明明/.test(line),
485
- )
486
- )
487
- issues.push(
488
- "对方只是确认,不要突然把别处的话题和未经核实的说法安到对方头上",
489
- );
490
- issues.push(...conversationalIssues(result, snapshot, decision));
491
- const undone = claimedUndoneAction(result.bubbles, snapshot);
492
- if (undone) issues.push(undone);
493
- return [...new Set(issues)];
494
- }
495
-
496
- // She may only say an action is done when the ledger says it is.
497
- export function claimedUndoneAction(bubbles, snapshot) {
498
- const action = snapshot?.actionResult;
499
- if (!action || action.state === "done") return "";
500
- const said = (Array.isArray(bubbles) ? bubbles : []).join("");
501
- if (
502
- /已经(?:帮你)?(?:做好|做完|办好|发出去|发过去|打开|关掉|设好|记下|完成)/.test(
503
- said,
504
- )
505
- )
506
- return "这句话说已经做完了,但这件事还没有完成";
507
- return "";
508
- }
1
+ import {
2
+ conversationalIssues,
3
+ openedUnprompted,
4
+ replyFocus,
5
+ } from "./conversation-cues.js";
6
+ import { gentlePersona } from "./persona-manager.js";
7
+ import { initiativeContext } from "./initiative-context.js";
8
+ import { interestTerms } from "../mind/attention.js";
9
+ const norm = (s) => s.toLowerCase().replace(/[\s\p{P}\p{S}]/gu, "");
10
+ export function contradictoryOwnWords(snapshot, decision = {}) {
11
+ const targets = (snapshot.messages || []).filter((message) =>
12
+ decision.targetMessageIds?.includes(message.id),
13
+ );
14
+ const words = (snapshot.inner?.continuity?.people || [])
15
+ .filter((person) =>
16
+ targets.some((message) => String(message.speaker) === String(person.id)),
17
+ )
18
+ .flatMap((person) =>
19
+ (person.myElsewhereWords || []).map((line) => line.text || ""),
20
+ );
21
+ return (
22
+ words.some((line) => /不是|没说|并非|不指/.test(line)) &&
23
+ words.some((line) => /就是|确实|说的就是|指的就是/.test(line))
24
+ );
25
+ }
26
+ // "你刚说……" is a claim about the last few minutes, so the room can settle it.
27
+ // The one that went wrong was her own joke handed back as the other person's
28
+ // words. Only a claim that shares nothing with anything that person said in the
29
+ // window is refused; "你刚说的那个" points at their words without repeating them.
30
+ const IMMEDIATE_CLAIM =
31
+ /你(?:自己|才)?(?:刚(?:刚|才)?|方才)(?:自己)?(?:不是)?(?:明明)?(?:说|讲|提(?:到|过)?|发了?|打了?)(?:过)?(的|了)?[,,::\s]*["“「]?([^,。!?!?;;"”」\n]{2,30})/g;
32
+ const POINTS_AT = /^(?:那|这|哪|什么|啥|话|事|东西|内容|意思|一句|一堆|一些)/;
33
+ // "因为我看到你那条消息发过来" — a source for a line she opened on her own.
34
+ const SEEN_HIS_MESSAGE =
35
+ /(?<![没不未]有?)(?:看到|看见|收到|瞧见)了?你(?:这条|那条|刚才|刚刚)?(?:发(?:来|过来|出来)?)?的?(?:那条)?(?:消息|信息)|你(?:这条|那条)?(?:消息|信息)(?:发过来|发来|刚发)/;
36
+ export function unsupportedImmediateClaim(text, snapshot, targetMessages) {
37
+ const speakers = new Set(targetMessages.map((m) => String(m.speaker)));
38
+ if (!speakers.size) return "";
39
+ const said = interestTerms(
40
+ (snapshot.messages || [])
41
+ .filter((m) => m.role !== "assistant" && speakers.has(String(m.speaker)))
42
+ .slice(-60)
43
+ .map((m) => m.text || ""),
44
+ );
45
+ for (const match of String(text).matchAll(IMMEDIATE_CLAIM)) {
46
+ const claim = match[2].trim();
47
+ if (match[1] === "的" && POINTS_AT.test(claim)) continue;
48
+ const terms = interestTerms([claim]);
49
+ if (terms.size < 2) continue;
50
+ if (![...terms].some((term) => said.has(term))) return claim;
51
+ }
52
+ return "";
53
+ }
54
+ // The other half of the same slip: "你自己说的……" followed by a line that was
55
+ // her own earlier message and that nobody else said in the window.
56
+ const THEY_SAID = /你(?:自己)?(?:刚|才|明明)?说(?:过|的)/;
57
+ export function attributesOwnWordsToThem(text, snapshot) {
58
+ if (!THEY_SAID.test(text)) return false;
59
+ const said = norm(String(text).replace(THEY_SAID, ""));
60
+ const recent = (snapshot.messages || []).slice(-40);
61
+ const mine = recent
62
+ .filter((m) => m.role === "assistant")
63
+ .map((m) => norm(m.text || ""));
64
+ const theirs = recent
65
+ .filter((m) => m.role !== "assistant")
66
+ .map((m) => norm(m.text || ""));
67
+ let echoed = false;
68
+ for (let i = 0; i + 6 <= said.length; i += 2) {
69
+ const phrase = said.slice(i, i + 6);
70
+ if (theirs.some((line) => line.includes(phrase))) return false;
71
+ if (mine.some((line) => line.includes(phrase))) echoed = true;
72
+ }
73
+ return echoed;
74
+ }
75
+ // The reviewer judges one reply; it needs the turn and its recent lead-in,
76
+ // not the summaries, recall or the rest of the transcript.
77
+ export function reviewContext(snapshot, decision = {}) {
78
+ if (snapshot.initiative) return initiativeContext(snapshot);
79
+ const {
80
+ persona: _persona,
81
+ sourceRows: _sourceRows,
82
+ batch: _batch,
83
+ summaries: _summaries,
84
+ summaryInstruction: _summaryInstruction,
85
+ stages: _stages,
86
+ recalled: _recalled,
87
+ budget: _budget,
88
+ historyStart: _historyStart,
89
+ self: _self,
90
+ inner: _inner,
91
+ ...rest
92
+ } = snapshot;
93
+ const messages = snapshot.messages || [];
94
+ const focus = new Set([
95
+ ...(snapshot.batchIds || []),
96
+ ...(decision.targetMessageIds || []),
97
+ ...(decision.evidenceIds || []),
98
+ ]);
99
+ for (const m of messages)
100
+ if (focus.has(m.id)) for (const id of m.replyChain || []) focus.add(id);
101
+ const recent = new Set(messages.slice(-30).map((m) => m.id));
102
+ return {
103
+ ...rest,
104
+ ...(snapshot.inner?.currentLife
105
+ ? { currentLife: snapshot.inner.currentLife }
106
+ : {}),
107
+ ...(snapshot.inner?.continuity
108
+ ? { continuity: snapshot.inner.continuity }
109
+ : {}),
110
+ messages: messages.filter((m) => focus.has(m.id) || recent.has(m.id)),
111
+ };
112
+ }
113
+ const bubbleLimit = (decision) =>
114
+ decision.maxBubbles ?? (decision.action === "MULTI_MESSAGE" ? 3 : 2);
115
+ export function normalizeResponse(result, decision, fallback = "嗯") {
116
+ const maxBubbles = bubbleLimit(decision);
117
+ let bubbles = [];
118
+ if (Array.isArray(result?.bubbles)) bubbles = result.bubbles;
119
+ else if (typeof result?.bubbles === "string") bubbles = [result.bubbles];
120
+ else
121
+ for (const key of ["reply", "text", "content"])
122
+ if (typeof result?.[key] === "string" && result[key].trim()) {
123
+ bubbles = [result[key]];
124
+ break;
125
+ }
126
+ bubbles = bubbles
127
+ .filter((x) => typeof x === "string")
128
+ .map((x) => x.trim())
129
+ .filter(Boolean)
130
+ .slice(0, maxBubbles);
131
+ if (!bubbles.length) bubbles = [fallback];
132
+ return {
133
+ ...(result && typeof result === "object" ? result : {}),
134
+ bubbles,
135
+ reason:
136
+ typeof result?.reason === "string" && result.reason.trim()
137
+ ? result.reason
138
+ : "短句回复",
139
+ };
140
+ }
141
+ export function validateResponse(result, snapshot, decision, maxChars = 180) {
142
+ const issues = [];
143
+ const maxBubbles = bubbleLimit(decision);
144
+ if (
145
+ !Array.isArray(result.bubbles) ||
146
+ result.bubbles.length < 1 ||
147
+ result.bubbles.length > maxBubbles ||
148
+ result.bubbles.some((x) => typeof x !== "string" || !x.trim())
149
+ )
150
+ return ["气泡格式无效"];
151
+ if (result.bubbles.reduce((n, s) => n + Array.from(s).length, 0) > maxChars)
152
+ issues.push("回复超过当前会话总字数");
153
+ if (decision.choice === "react" && Array.from(result.bubbles[0]).length > 20)
154
+ issues.push("选择了简短反应,只写一句不超过20字的短话");
155
+ const recent = snapshot.messages
156
+ .filter((m) => m.role === "assistant")
157
+ .slice(-12)
158
+ .map((m) => m.text);
159
+ const continuity = snapshot.inner?.continuity;
160
+ const sharedElsewhere =
161
+ continuity?.requested &&
162
+ continuity.people.some((person) =>
163
+ person.places.some((place) => !place.current),
164
+ );
165
+ const pledgedTo = new Set(
166
+ (continuity?.people || [])
167
+ .filter(
168
+ (person) =>
169
+ person.myPrivateIntentions?.length ||
170
+ person.recentShared?.some(
171
+ (line) =>
172
+ line.role === "assistant" &&
173
+ /答应|说好|下次|以后|我会|我就|记住/.test(line.text),
174
+ ) ||
175
+ person.sharedMoments?.some(
176
+ (moment) =>
177
+ moment.privateOrigin &&
178
+ moment.iSaid?.some((line) =>
179
+ /答应|说好|下次|以后|我会|我就|记住/.test(line),
180
+ ),
181
+ ),
182
+ )
183
+ .map((person) => String(person.id)),
184
+ );
185
+ const targetMessages = snapshot.messages.filter((m) =>
186
+ decision.targetMessageIds?.includes(m.id),
187
+ );
188
+ const pledgedTurn = targetMessages.some((m) =>
189
+ pledgedTo.has(String(m.speaker)),
190
+ );
191
+ const affectionTurn = targetMessages.some(
192
+ (m) =>
193
+ (m.relation === "direct" ||
194
+ /(?:^|:)private:/.test(snapshot.sessionId || "")) &&
195
+ /(?:想你|喜欢你|爱你)/.test(m.text || ""),
196
+ );
197
+ const privateLines = (continuity?.people || []).flatMap((person) => [
198
+ ...(person.recentShared || []).map((line) => line.text),
199
+ ...(person.myPrivateIntentions || []).map((line) => line.content),
200
+ ...(person.myElsewhereWords || [])
201
+ .filter((line) => line.privateOrigin)
202
+ .map((line) => line.text),
203
+ ...(person.sharedMoments || [])
204
+ .filter((moment) => moment.privateOrigin)
205
+ .flatMap((moment) => [
206
+ ...(moment.theySaid || []).map((line) => line.text),
207
+ ...(moment.iSaid || []),
208
+ ]),
209
+ ]);
210
+ const ownWordsConflict = contradictoryOwnWords(snapshot, decision);
211
+ if (
212
+ ownWordsConflict &&
213
+ !/前后|说乱|不一致|矛盾|改口|两种说法|先.{0,24}后/.test(
214
+ result.bubbles.join(" "),
215
+ )
216
+ )
217
+ issues.push(
218
+ "本人对同一件事留下相反说法,这次必须承认前后不一致,不能只挑一条旧话解释",
219
+ );
220
+ const publicWords = norm(
221
+ snapshot.messages
222
+ .filter((message) => message.role === "user")
223
+ .slice(-12)
224
+ .map((message) => message.text || "")
225
+ .join(""),
226
+ );
227
+ for (const text of result.bubbles) {
228
+ const spoken = norm(text);
229
+ if (
230
+ ownWordsConflict &&
231
+ /从头到尾|始终|一直|只指|唯一|没在别处乱/.test(text)
232
+ )
233
+ issues.push(
234
+ "本人在别处对同一称呼或关系有相反说法,不能声称从头到尾一致;承认前后说乱了",
235
+ );
236
+ if (
237
+ privateLines.some((line) => {
238
+ const privateWords = norm(line || "");
239
+ for (let i = 0; i + 12 <= privateWords.length; i += 3) {
240
+ const phrase = privateWords.slice(i, i + 12);
241
+ if (spoken.includes(phrase) && !publicWords.includes(phrase))
242
+ return true;
243
+ }
244
+ return false;
245
+ })
246
+ )
247
+ issues.push("这句话照搬了只在私下说过的原话,不能在群里公开复述");
248
+ if (
249
+ pledgedTurn &&
250
+ /(?:没|没有|不记得|从未).{0,8}(?:答应|承诺|说过)|(?:答应|承诺).{0,8}(?:没有|不存在)|(?:有|拿出).{0,4}证据|你记错了/.test(
251
+ text,
252
+ )
253
+ )
254
+ issues.push(
255
+ "已有核实的本人承诺;不能否认、反问证据或把失约推给对方,简短承认并回应当下",
256
+ );
257
+ if (
258
+ targetMessages.some((m) =>
259
+ (continuity?.people || []).some(
260
+ (person) =>
261
+ String(person.id) === String(m.speaker) &&
262
+ person.myElsewhereWords?.length,
263
+ ),
264
+ ) &&
265
+ /(?:我)?(?:没|没有|从未|从来没|不记得).{0,10}(?:说过|说|喊过|喊|叫过|叫)/.test(
266
+ text,
267
+ )
268
+ )
269
+ issues.push(
270
+ "别处已有本人真实发出的相关原话,不能否认自己说过;先核对当时对象再修正",
271
+ );
272
+ const claimed = unsupportedImmediateClaim(text, snapshot, targetMessages);
273
+ if (claimed)
274
+ issues.push(
275
+ `说对方刚说过“${claimed}”,但本轮记录里对方没有这样说;不能把自己先说的话或别人的话安到对方头上,承认是自己顺口带出来的,或直接问他`,
276
+ );
277
+ if (attributesOwnWordsToThem(text, snapshot))
278
+ issues.push(
279
+ "你说是对方说的那句话,其实是你自己先说的;不能把自己的话安到对方头上,承认是自己先说的",
280
+ );
281
+ if (
282
+ affectionTurn &&
283
+ /(?:才见过|刚见过|不是才见|别闹到我|先别闹)/.test(text)
284
+ )
285
+ issues.push(
286
+ "对方在直接表达想念;不要用见面时间或划水玩笑驳回这份表达,按当下关系自然回应",
287
+ );
288
+ if (
289
+ sharedElsewhere &&
290
+ /(?:不能|不算|不能因此).{0,24}(?:共同经历|共同相处)|(?:从来|从没|完全).{0,8}(?:没聊过|不记得|没相处过|没有共同经历)|(?:没在|没有在).{0,8}(?:别的|其他|其它).{0,8}群.{0,8}(?:聊|相处)|只有.{0,8}(?:这段|这次)私聊.{0,8}(?:经历|记录)/.test(
291
+ text,
292
+ )
293
+ )
294
+ issues.push(
295
+ "已核实和同一个人在其他地方真正说过话,换到私聊不会抹掉共同经历;依据 continuity 中具体的旧事回应,不把公开相处说成别的实例或不能算共同经历",
296
+ );
297
+ if (
298
+ snapshot.initiative &&
299
+ /(?:收到|看到|看见)你(?:们)?(?:刚刚?|刚才|又|发来|来找我)|你(?:们)?(?:刚刚?|刚才|这会儿).{0,8}(?:说|问|发|提|找我)|(?:刚刚?|刚才|这会儿)你(?:们)?.{0,8}(?:说|问|发|提|找我)|你(?:又来|来找我|发来(?:了)?消息)/.test(
300
+ text,
301
+ )
302
+ )
303
+ issues.push(
304
+ "本轮没有收到新消息,不能捏造对方刚说过、发过消息或来找你;若确实回忆旧事要用过去的时间",
305
+ );
306
+ if ((snapshot.persona?.forbidden || []).some((w) => w && text.includes(w)))
307
+ issues.push("使用人格禁用表达");
308
+ if (
309
+ /[hH]{2,}[。!!~~]*$/.test(text) &&
310
+ norm(text).length > 6 &&
311
+ recent
312
+ .slice(-5)
313
+ .filter((r) => norm(r).length > 6 && /[hH]{2,}[。!!~~]*$/.test(r))
314
+ .length >= 2
315
+ )
316
+ issues.push("不要把hh反复当句尾装饰;删掉装饰,保留具体回应");
317
+ const briefReaction =
318
+ /^(?:h{2,6}|哈{1,6}|嗯{1,3}|好|行|哦{1,3}|啊|对|是的|晚安)$/i.test(
319
+ norm(text),
320
+ );
321
+ if (!briefReaction && recent.some((r) => norm(r) === norm(text)))
322
+ issues.push("重复近期回复");
323
+ if (
324
+ gentlePersona(snapshot.persona) &&
325
+ /你们?[^。!?]{0,10}(?:出糗|怎么这么|脑子|太菜|有病)|我怎么知道|现在又嫌我|你倒是|累傻了|甩锅|难伺候|你除了.*还会啥|你倒是说|被我说中了|活该|自找的|赚了个教训/.test(
326
+ text,
327
+ )
328
+ )
329
+ issues.push("低毒舌人格不质问、挖苦或揣测群友恶意");
330
+ if (/又一张表情包|怎么连续几张|都不带字的|你们.*刷屏/.test(text))
331
+ issues.push("不要把发图行为本身当话题点评");
332
+ if (
333
+ /你们是复读机|你真蠢|你(?:们)?(?:就是|真是|是个)?(?:弱智|废物)/.test(text)
334
+ )
335
+ issues.push("评价或攻击群友,过于刻薄");
336
+ if (
337
+ /我理解你的感受|作为一个AI|我只是(?:一个|个)?(?:AI|助手)|有什么需要帮助|你并不孤单|(?:骂吧|慢慢说|你继续)[,,。\s]*我听着/.test(
338
+ text,
339
+ )
340
+ )
341
+ issues.push("客服式套话");
342
+ for (const phrase of ["笑死", "我服了", "不是哥们", "你们怎么回事", "经典"])
343
+ if (
344
+ text.includes(phrase) &&
345
+ recent.filter((r) => r.includes(phrase)).length >= 2
346
+ )
347
+ issues.push(`近期反复使用“${phrase}”`);
348
+ }
349
+ if (new Set(result.bubbles.map(norm)).size !== result.bubbles.length)
350
+ issues.push("气泡之间重复");
351
+ const plans = (snapshot.inner?.expecting || [])
352
+ .map((line) => String(line).match(/计划:(.+?)(/)?.[1])
353
+ .filter(Boolean);
354
+ const current = targetMessages.map((m) => m.text || "").join(" ");
355
+ if (
356
+ plans.length &&
357
+ !/(?:已经|刚刚?|刚才).{0,12}(?:去|做|完成|回来)|(?:去过|做完|结束了)/.test(
358
+ current,
359
+ ) &&
360
+ result.bubbles.some((line) => {
361
+ if (!/(?:你|他|她).{0,12}(?:不是|已经).{0,18}(?:了|完|回来)/.test(line))
362
+ return false;
363
+ const said = interestTerms([line]);
364
+ return plans.some(
365
+ (plan) =>
366
+ [...interestTerms([plan])].filter((term) => said.has(term)).length >=
367
+ 2,
368
+ );
369
+ })
370
+ )
371
+ issues.push(
372
+ "这件事只记录为对方的安排,尚未确认已经发生;不要用‘你不是已经……了吗’当成事实",
373
+ );
374
+ const focus = replyFocus(snapshot, decision);
375
+ const questioned = targetMessages.at(-1);
376
+ if (focus.kind === "clarify_claim" && questioned) {
377
+ const cue = interestTerms([questioned.text || ""]);
378
+ const earlierToSomeoneElse = (snapshot.messages || []).filter(
379
+ (message) =>
380
+ message.role === "assistant" &&
381
+ message.id < questioned.id &&
382
+ (message.replyTargets || []).length &&
383
+ (message.replyTargets || []).every(
384
+ (target) => String(target.speaker) !== String(questioned.speaker),
385
+ ) &&
386
+ [...interestTerms([message.text || ""])].some((term) => cue.has(term)),
387
+ );
388
+ if (
389
+ earlierToSomeoneElse.length &&
390
+ result.bubbles.some((line) =>
391
+ /从头到尾.{0,16}(?:你|都是)|一直.{0,12}(?:你|只)|你自己.{0,8}(?:听岔|记错)/.test(
392
+ line,
393
+ ),
394
+ )
395
+ )
396
+ issues.push(
397
+ "相关旧话有明确的另一位回复对象,不能说从头到尾只对眼前人说或责怪对方听错",
398
+ );
399
+ if (
400
+ /主人/.test(questioned.text || "") &&
401
+ earlierToSomeoneElse.some((message) =>
402
+ /你主人|他的主人/.test(message.text || ""),
403
+ )
404
+ ) {
405
+ const line = result.bubbles.join(" ");
406
+ if (
407
+ /指的就是.{0,10}你|说的就是.{0,10}你|你.{0,8}是.{0,8}(?:他|别人).{0,4}主人|(?:他|别人).{0,4}主人.{0,6}是你/.test(
408
+ line,
409
+ )
410
+ )
411
+ issues.push(
412
+ "旧话是对第三人说的,但没有证据证明眼前人是第三人的主人;承认当时说错,不能继续补造归属",
413
+ );
414
+ if (/你.{0,8}(?:对号入座|误会|听岔|理解错)/.test(line))
415
+ issues.push("自己把无依据的人际关系说出口,不能让追问者承担误解责任");
416
+ const claimedEarlierQuestion =
417
+ /(?:是|因为|顺着).{0,20}(?:问我|问的是).{0,12}(?:养我|谁.{0,2}养|主人)/.test(
418
+ line,
419
+ );
420
+ if (claimedEarlierQuestion) {
421
+ const targets = new Set(
422
+ earlierToSomeoneElse.flatMap((message) =>
423
+ (message.replyTargets || []).map((target) => target.messageId),
424
+ ),
425
+ );
426
+ const actuallyAsked = (snapshot.messages || []).some(
427
+ (message) =>
428
+ targets.has(message.id) &&
429
+ /养你|养我|主人/.test(message.text || ""),
430
+ );
431
+ if (!actuallyAsked)
432
+ issues.push(
433
+ "对方是在旧话之后才追问主人,不能倒置时间说那句旧话是在回答这个问题",
434
+ );
435
+ }
436
+ }
437
+ }
438
+ if (focus.kind === "basis_check" && questioned) {
439
+ const opener = (snapshot.messages || [])
440
+ .filter((m) => m.role === "assistant" && m.id < questioned.id)
441
+ .at(-1);
442
+ if (
443
+ openedUnprompted(snapshot.messages, opener) &&
444
+ result.bubbles.some((line) => SEEN_HIS_MESSAGE.test(line))
445
+ )
446
+ issues.push(
447
+ "那句是你自己先开口的,在那之前他没有新发来消息;依据只能是你的推测或记得的事,不能说成是看到了他的消息",
448
+ );
449
+ }
450
+ if (
451
+ [
452
+ "repair",
453
+ "acknowledge",
454
+ "promise_check",
455
+ "clarify_claim",
456
+ "basis_check",
457
+ ].includes(focus.kind) &&
458
+ result.bubbles.length > 1
459
+ )
460
+ issues.push("这一轮只是纠正或确认,一句收住,不要追加原话题或解释");
461
+ if (focus.kind === "acknowledge" && result.bubbles.join("").length > 8)
462
+ issues.push("对方只确认了一下,不需要再评论或劝慰,用很短的回应收住");
463
+ if (
464
+ focus.kind === "acknowledge" &&
465
+ result.bubbles.some((line) => /早点|休息|歇着|去睡|注意身体/.test(line))
466
+ )
467
+ issues.push("对方只是确认,不要无请求地安排休息或继续给建议");
468
+ const selfName = String(snapshot.persona?.name || "").trim();
469
+ if (
470
+ focus.kind === "repair" &&
471
+ selfName &&
472
+ result.bubbles.some((line) =>
473
+ ["复读的是", "重复的是", "说错的是"].some((prefix) =>
474
+ line.includes(prefix + selfName),
475
+ ),
476
+ )
477
+ )
478
+ issues.push(
479
+ "历史 assistant 是自己已发的原话;被指出重复时不能说成另一个 bot 的错",
480
+ );
481
+ if (
482
+ focus.kind === "acknowledge" &&
483
+ result.bubbles.some((line) =>
484
+ /你(?:自己|刚才|之前|前面).{0,6}(?:说|提)|你明明/.test(line),
485
+ )
486
+ )
487
+ issues.push(
488
+ "对方只是确认,不要突然把别处的话题和未经核实的说法安到对方头上",
489
+ );
490
+ issues.push(...conversationalIssues(result, snapshot, decision));
491
+ const undone = claimedUndoneAction(result.bubbles, snapshot);
492
+ if (undone) issues.push(undone);
493
+ return [...new Set(issues)];
494
+ }
495
+
496
+ // She may only say an action is done when the ledger says it is.
497
+ export function claimedUndoneAction(bubbles, snapshot) {
498
+ const action = snapshot?.actionResult;
499
+ if (!action || action.state === "done") return "";
500
+ const said = (Array.isArray(bubbles) ? bubbles : []).join("");
501
+ if (
502
+ /已经(?:帮你)?(?:做好|做完|办好|发出去|发过去|打开|关掉|设好|记下|完成)/.test(
503
+ said,
504
+ )
505
+ )
506
+ return "这句话说已经做完了,但这件事还没有完成";
507
+ return "";
508
+ }