luckytri 1.0.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.
Files changed (235) hide show
  1. package/.github/SECURITY.md +71 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +250 -0
  4. package/README.md +250 -0
  5. package/bin/luckytri.js +107 -0
  6. package/docs/brand/banner.png +0 -0
  7. package/docs/brand/palette.png +0 -0
  8. package/docs/brand/portrait.png +0 -0
  9. package/docs/en/CHANGELOG.md +113 -0
  10. package/docs/en/architecture.md +152 -0
  11. package/docs/en/chat-style.md +62 -0
  12. package/docs/en/connect.md +63 -0
  13. package/docs/en/guide.md +216 -0
  14. package/docs/en/her-life.md +189 -0
  15. package/docs/en/npm-release.md +25 -0
  16. package/docs/en/plugins.md +82 -0
  17. package/docs/en/recovery.md +28 -0
  18. package/docs/en/time.md +71 -0
  19. package/docs/en/webui.md +73 -0
  20. package/docs/zh/CHANGELOG.md +113 -0
  21. package/docs/zh/architecture.md +152 -0
  22. package/docs/zh/chat-style.md +62 -0
  23. package/docs/zh/connect.md +63 -0
  24. package/docs/zh/guide.md +216 -0
  25. package/docs/zh/her-life.md +189 -0
  26. package/docs/zh/npm-release.md +33 -0
  27. package/docs/zh/plugins.md +82 -0
  28. package/docs/zh/recovery.md +28 -0
  29. package/docs/zh/time.md +71 -0
  30. package/docs/zh/webui.md +73 -0
  31. package/package.json +108 -0
  32. package/plugins/sdk/index.d.ts +155 -0
  33. package/plugins/sdk/testing.js +58 -0
  34. package/plugins/weather/index.js +82 -0
  35. package/plugins/weather/luckytri-plugin.json +29 -0
  36. package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
  37. package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
  38. package/public/app/assets/Empty-CpP5kQHH.js +2 -0
  39. package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
  40. package/public/app/assets/Field-D4gY-INU.js +1 -0
  41. package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
  42. package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
  43. package/public/app/assets/LifePage-BUi5euBF.css +1 -0
  44. package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
  45. package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
  46. package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
  47. package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
  48. package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
  49. package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
  50. package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
  51. package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
  52. package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
  53. package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
  54. package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
  55. package/public/app/assets/TimePage-BOms_n4I.css +1 -0
  56. package/public/app/assets/TimePage-Cw44inM4.js +3 -0
  57. package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
  58. package/public/app/assets/index-BheRW-1t.js +4 -0
  59. package/public/app/assets/index-YDz1ree1.css +1 -0
  60. package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
  61. package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
  62. package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
  63. package/public/app/assets/media-BzPDBm7e.js +1 -0
  64. package/public/app/index.html +27 -0
  65. package/public/guide.en.html +1053 -0
  66. package/public/guide.html +893 -0
  67. package/public/plugin-kit/v1/bridge.js +30 -0
  68. package/public/plugin-kit/v1/kit.css +14 -0
  69. package/public/tutorial.en.md +214 -0
  70. package/public/tutorial.md +214 -0
  71. package/scripts/backup.js +311 -0
  72. package/scripts/browser.js +41 -0
  73. package/scripts/launch.js +94 -0
  74. package/scripts/plugin.js +150 -0
  75. package/scripts/proxy-config.js +167 -0
  76. package/scripts/recover.js +26 -0
  77. package/scripts/runtime.js +25 -0
  78. package/scripts/service.js +47 -0
  79. package/scripts/setup.js +50 -0
  80. package/scripts/start-server.js +41 -0
  81. package/scripts/stop.js +36 -0
  82. package/server/app.js +96 -0
  83. package/server/channels/adapters.js +27 -0
  84. package/server/channels/contract.js +57 -0
  85. package/server/channels/hub.js +165 -0
  86. package/server/channels/index.js +25 -0
  87. package/server/channels/onebot/adapter.js +288 -0
  88. package/server/channels/onebot/index.js +336 -0
  89. package/server/channels/onebot/token.js +3 -0
  90. package/server/channels/qqbot/adapter.js +253 -0
  91. package/server/channels/qqbot/api.js +199 -0
  92. package/server/channels/qqbot/credentials.js +11 -0
  93. package/server/channels/qqbot/gateway.js +274 -0
  94. package/server/channels/qqbot/index.js +389 -0
  95. package/server/channels/segments.js +40 -0
  96. package/server/channels/session-key.js +153 -0
  97. package/server/channels/state.js +42 -0
  98. package/server/core/api.js +564 -0
  99. package/server/core/capabilities.js +208 -0
  100. package/server/core/context-builder.js +288 -0
  101. package/server/core/context-compactor.js +454 -0
  102. package/server/core/conversation-cues.js +325 -0
  103. package/server/core/conversation-manager.js +87 -0
  104. package/server/core/events.js +60 -0
  105. package/server/core/feedback.js +8 -0
  106. package/server/core/group-style.js +72 -0
  107. package/server/core/image-preprocessor.js +208 -0
  108. package/server/core/initiative-context.js +66 -0
  109. package/server/core/input-budget.js +32 -0
  110. package/server/core/local-demo.js +98 -0
  111. package/server/core/message-manager.js +118 -0
  112. package/server/core/message-scheduler.js +78 -0
  113. package/server/core/model-json.js +190 -0
  114. package/server/core/model-manager.js +1213 -0
  115. package/server/core/model-presets.js +900 -0
  116. package/server/core/name-call.js +29 -0
  117. package/server/core/net-guard.js +49 -0
  118. package/server/core/network.js +157 -0
  119. package/server/core/orchestrator.js +569 -0
  120. package/server/core/perceivers.js +91 -0
  121. package/server/core/person-name.js +22 -0
  122. package/server/core/persona-manager.js +243 -0
  123. package/server/core/reply-delivery.js +393 -0
  124. package/server/core/reply-target-resolver.js +50 -0
  125. package/server/core/repository.js +293 -0
  126. package/server/core/response-generator.js +50 -0
  127. package/server/core/response-validator.js +508 -0
  128. package/server/core/retired-prompts.js +51 -0
  129. package/server/core/sessions.js +109 -0
  130. package/server/core/speaker-names.js +92 -0
  131. package/server/core/topic-tracker.js +65 -0
  132. package/server/core/turn-processor.js +630 -0
  133. package/server/core/turn.js +236 -0
  134. package/server/core/vision-manager.js +633 -0
  135. package/server/http.js +18 -0
  136. package/server/i18n/decisions.js +206 -0
  137. package/server/i18n/errors.js +497 -0
  138. package/server/i18n/index.js +144 -0
  139. package/server/i18n/labels.js +653 -0
  140. package/server/i18n/models.js +111 -0
  141. package/server/index.js +102 -0
  142. package/server/knowledge/api.js +342 -0
  143. package/server/knowledge/embedding-profile.js +114 -0
  144. package/server/knowledge/index.js +13 -0
  145. package/server/knowledge/manager.js +386 -0
  146. package/server/knowledge/retrieval.js +93 -0
  147. package/server/knowledge/schema.js +154 -0
  148. package/server/knowledge/vector-index.js +90 -0
  149. package/server/mind/affect.js +265 -0
  150. package/server/mind/anticipations.js +507 -0
  151. package/server/mind/api.js +703 -0
  152. package/server/mind/attention.js +200 -0
  153. package/server/mind/bonds.js +558 -0
  154. package/server/mind/budget.js +152 -0
  155. package/server/mind/clock.js +83 -0
  156. package/server/mind/context-selection.js +61 -0
  157. package/server/mind/continuity.js +562 -0
  158. package/server/mind/conversation-origin.js +175 -0
  159. package/server/mind/day-planner.js +324 -0
  160. package/server/mind/days.js +210 -0
  161. package/server/mind/evidence-read.js +45 -0
  162. package/server/mind/evidence.js +148 -0
  163. package/server/mind/faces.js +138 -0
  164. package/server/mind/guard.js +60 -0
  165. package/server/mind/index.js +445 -0
  166. package/server/mind/initiative.js +333 -0
  167. package/server/mind/life/activities.js +21 -0
  168. package/server/mind/life/context.js +309 -0
  169. package/server/mind/life/diary.js +786 -0
  170. package/server/mind/life/growth.js +252 -0
  171. package/server/mind/life/index.js +1049 -0
  172. package/server/mind/life/presence.js +305 -0
  173. package/server/mind/meetings.js +1069 -0
  174. package/server/mind/memory-policy.js +48 -0
  175. package/server/mind/memory.js +888 -0
  176. package/server/mind/nature.js +262 -0
  177. package/server/mind/novelty.js +19 -0
  178. package/server/mind/observations.js +84 -0
  179. package/server/mind/own-day.js +324 -0
  180. package/server/mind/own-voice.js +318 -0
  181. package/server/mind/periods.js +118 -0
  182. package/server/mind/reading.js +120 -0
  183. package/server/mind/salience.js +201 -0
  184. package/server/mind/schema.js +562 -0
  185. package/server/mind/self.js +484 -0
  186. package/server/mind/senses.js +53 -0
  187. package/server/mind/thoughts.js +274 -0
  188. package/server/mind/time/activity-clock.js +196 -0
  189. package/server/mind/time/agenda.js +242 -0
  190. package/server/mind/time/api.js +200 -0
  191. package/server/mind/time/attention.js +34 -0
  192. package/server/mind/time/availability.js +69 -0
  193. package/server/mind/time/commitment-review.js +198 -0
  194. package/server/mind/time/delivery-links.js +130 -0
  195. package/server/mind/time/executor.js +512 -0
  196. package/server/mind/time/experiences.js +128 -0
  197. package/server/mind/time/games.js +562 -0
  198. package/server/mind/time/index.js +739 -0
  199. package/server/mind/time/intent.js +166 -0
  200. package/server/mind/time/kinds.js +171 -0
  201. package/server/mind/time/model-material.js +77 -0
  202. package/server/mind/time/plugin-activity.js +207 -0
  203. package/server/mind/time/presentation.js +51 -0
  204. package/server/mind/time/schedule.js +83 -0
  205. package/server/mind/time/schema.js +84 -0
  206. package/server/mind/time/search.js +383 -0
  207. package/server/mind/time/sharing.js +472 -0
  208. package/server/mind/time/task-links.js +311 -0
  209. package/server/mind/time/tasks.js +587 -0
  210. package/server/mind/time/works.js +432 -0
  211. package/server/mind/traits.js +191 -0
  212. package/server/mind/util.js +110 -0
  213. package/server/mind/view.js +273 -0
  214. package/server/paths.js +102 -0
  215. package/server/plugins/api.js +204 -0
  216. package/server/plugins/host.js +967 -0
  217. package/server/plugins/install.js +295 -0
  218. package/server/plugins/manifest.js +201 -0
  219. package/server/plugins/permissions.js +42 -0
  220. package/server/plugins/runner/guard.mjs +36 -0
  221. package/server/plugins/runner/worker.mjs +250 -0
  222. package/server/plugins/schema.js +91 -0
  223. package/server/plugins/zip.js +112 -0
  224. package/server/startup-lock.js +118 -0
  225. package/server/storage/archive.js +241 -0
  226. package/server/storage/backup-cleanup-worker.js +8 -0
  227. package/server/storage/backup-cleanup.js +414 -0
  228. package/server/storage/backup-lock.js +76 -0
  229. package/server/storage/backup-scheduler.js +131 -0
  230. package/server/storage/store.js +199 -0
  231. package/server/studio/display-names.js +138 -0
  232. package/server/studio/management.js +102 -0
  233. package/server/studio/readiness.js +190 -0
  234. package/server/studio/settings.js +236 -0
  235. package/server/version.js +10 -0
@@ -0,0 +1,325 @@
1
+ import { claimedPlay } from "../mind/guard.js";
2
+
3
+ // Stable message timestamps stay with history; changing clock/style cues belong at the tail.
4
+ const clockFormats = new Map();
5
+ export function localClock(time, timeZone = "Asia/Shanghai") {
6
+ let formatter = clockFormats.get(timeZone);
7
+ if (!formatter) {
8
+ formatter = new Intl.DateTimeFormat("zh-CN", {
9
+ timeZone,
10
+ year: "numeric",
11
+ month: "2-digit",
12
+ day: "2-digit",
13
+ hour: "2-digit",
14
+ minute: "2-digit",
15
+ hourCycle: "h23",
16
+ });
17
+ clockFormats.set(timeZone, formatter);
18
+ if (clockFormats.size > 32)
19
+ clockFormats.delete(clockFormats.keys().next().value);
20
+ }
21
+ const parts = Object.fromEntries(
22
+ formatter.formatToParts(new Date(time)).map((p) => [p.type, p.value]),
23
+ );
24
+ const hour = Number(parts.hour);
25
+ return {
26
+ timeZone,
27
+ local: `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}`,
28
+ hour,
29
+ period:
30
+ hour < 5
31
+ ? "凌晨"
32
+ : hour < 11
33
+ ? "早上"
34
+ : hour < 14
35
+ ? "中午"
36
+ : hour < 18
37
+ ? "下午"
38
+ : "晚上",
39
+ };
40
+ }
41
+ const ellipsis = /…|\.{3,}|。{3,}/u;
42
+ // Someone asks how she knew or guessed something.
43
+ const BASIS_QUESTION =
44
+ /你.{0,6}(?:怎么|咋|凭什么|凭啥|从哪|哪里|哪儿|哪来).{0,6}(?:知道|晓得|看出|看得出|猜到|得知)/;
45
+ // Whether her message started the exchange on her own: the last thing they
46
+ // said before it was a long time earlier. Without such a message on record
47
+ // this stays unknown, not true.
48
+ export function openedUnprompted(messages, opener) {
49
+ if (opener?.role !== "assistant") return false;
50
+ const before = (messages || [])
51
+ .filter((m) => m.role === "user" && m.id < opener.id)
52
+ .at(-1);
53
+ return (
54
+ !!before &&
55
+ Number.isFinite(before.time) &&
56
+ Number.isFinite(opener.time) &&
57
+ opener.time - before.time >= 2 * 3600000
58
+ );
59
+ }
60
+ export function replyFocus(snapshot, decision) {
61
+ if (snapshot.initiative)
62
+ return {
63
+ kind: "initiative",
64
+ instruction:
65
+ "这是自己想发起的一段话,不是补答最后一条历史消息。保留自己的动机和自然语气;结合最新情况,不催回复,不泄露私下的事。",
66
+ };
67
+ const targets = snapshot.messages.filter((m) =>
68
+ decision.targetMessageIds?.includes(m.id),
69
+ );
70
+ const latest = targets.at(-1)?.text || "";
71
+ if (
72
+ /^(?:\[表情\])+$/.test(latest) ||
73
+ (targets.at(-1)?.attachments || []).some(
74
+ (item) =>
75
+ item.type === "image" && item.summary && /^\[.*\]$/.test(item.summary),
76
+ )
77
+ )
78
+ return {
79
+ kind: "sticker",
80
+ instruction:
81
+ "这是表情或表情包。把画面和 QQ 标签当作理解语气的线索,先判断是在接梗、吐槽、撒娇还是另起话题;短接它表达的态度即可,不要认真复述图中文字、逐项描述画面或把表情台词当成事实。",
82
+ };
83
+ if (/^(?:\[图片\])+$/.test(latest))
84
+ return {
85
+ kind: "image",
86
+ instruction:
87
+ "这是新发的一张图。先按这条消息自己的画面和上下文判断用途,可能是新话题或表情反应;不要因为它紧挨着上一张图就沿用上一话题。没读到画面时不猜内容。",
88
+ };
89
+ if (
90
+ /^[??]{1,3}$/.test(latest) &&
91
+ snapshot.messages.some((m) => m.role === "assistant")
92
+ )
93
+ return {
94
+ kind: "clarify_claim",
95
+ instruction:
96
+ "对方的问号是在接你刚说的话。先核对自己上句有没有根据:说错了就改正,没说错就解释那句的意思。不要跳到别处的话题,也不要拿自己刚说过的话当作对方的证据。一句收住。",
97
+ };
98
+ if (BASIS_QUESTION.test(latest)) {
99
+ const opener = snapshot.messages
100
+ .filter((m) => m.role === "assistant" && m.id < (targets.at(-1)?.id ?? 0))
101
+ .at(-1);
102
+ return {
103
+ kind: "basis_check",
104
+ instruction: `对方在问你凭什么知道或猜到的。先找到你实际发出的那句话,再核对它的依据。${
105
+ openedUnprompted(snapshot.messages, opener)
106
+ ? "那句是你自己先开口的,在那之前他没有新发来消息。"
107
+ : ""
108
+ }依据只能是你的推测(比如现在的时间)或记忆里真有的事:如实说是猜的,说不出依据就承认没有依据。不要编一条他发来的消息、一个没发生过的来源来圆。一句收住。`,
109
+ };
110
+ }
111
+ if (
112
+ /你.{0,10}(?:为什么|怎么|凭什么).{0,14}(?:说|认定|觉得|判断)|(?:为什么|怎么).{0,12}(?:这么说|那样说)/.test(
113
+ latest,
114
+ )
115
+ )
116
+ return {
117
+ kind: "clarify_claim",
118
+ instruction:
119
+ "对方在追问你之前的一个判断。先找到你实际发出的那句话和当时回应的对象,再检查依据。若把第三个人当成机器人、把谁是谁说乱了,就承认具体错处;不要编造对方的身份、主人或另一段经历来圆说法。一句收住。",
120
+ };
121
+ if (
122
+ /你.{0,8}(?:答应|说好).{0,12}(?:忘|不认|没做)|你.{0,8}(?:忘|不认).{0,12}(?:答应|说好)/.test(
123
+ latest,
124
+ )
125
+ )
126
+ return {
127
+ kind: "promise_check",
128
+ instruction:
129
+ "对方在追问约定,先核对本轮原话和已核实的承诺。确实答应过就直接承认,有漏接就承认漏接;没有证据时说自己暂时没想起来,不反问证据、不说对方记错了。只回应这件事,一句收住。",
130
+ };
131
+ if (
132
+ /别.*(?:重复|复述)|人机|不自然|没发现.*(?:早上|晚上)|说错|搞错/.test(latest)
133
+ )
134
+ return {
135
+ kind: "repair",
136
+ instruction:
137
+ "对方在纠正你:简短承认具体错误就停。不解释自身状态,不再补原话题的安慰,不反问对方。最多一个气泡。",
138
+ };
139
+ if (
140
+ /^(?:对呀|对啊|对|嗯+|是啊|是的|好吧|好的(?:呀|啊|宝宝)?)[。!!\s]*$/.test(
141
+ latest,
142
+ )
143
+ )
144
+ return {
145
+ kind: "acknowledge",
146
+ instruction:
147
+ "对方只是确认已知内容,话题暂时结束。极短回应即可,不重新描述事情、不追加问题。最多一个气泡。",
148
+ };
149
+ if (/别给建议|不要建议|让我骂|只想吐槽/.test(latest))
150
+ return {
151
+ kind: "vent",
152
+ instruction:
153
+ "不要指导对方情绪,不说那我闭嘴、你骂吧来表演退场。简短站在对方这边即可。",
154
+ };
155
+ if (
156
+ /累|困|难受|烦|难过|垂头丧气/.test(latest) &&
157
+ !/怎么办|建议|怎么做|帮我/.test(latest)
158
+ )
159
+ return {
160
+ kind: "feeling",
161
+ instruction:
162
+ "对方在说自己的感受,不是在求解决方案。普通地回应,不复述症状,不预测他肯定撑不住,不劝慢慢熬、休息、别折腾。已有具体原因时说自己对这件事的态度;未知原因可以轻问一句。无需每次都共情总结。",
163
+ };
164
+ return {
165
+ kind: "respond",
166
+ instruction:
167
+ "只回应这一轮真正新增的意思;不为每句话制造情绪,不复述笑点来解释为什么好笑。",
168
+ };
169
+ }
170
+ export function conversationCues(
171
+ messages,
172
+ batchIds,
173
+ now = Date.now(),
174
+ timeZone,
175
+ ) {
176
+ const recent = messages.filter((m) => m.role === "assistant").slice(-8);
177
+ const batch = messages.filter((m) => batchIds.includes(m.id));
178
+ const previous = messages
179
+ .filter(
180
+ (m) => !batchIds.includes(m.id) && m.id < (batch[0]?.id ?? Infinity),
181
+ )
182
+ .at(-1);
183
+ const gapMinutes =
184
+ previous && batch[0]
185
+ ? Math.max(0, Math.round((batch[0].time - previous.time) / 60000))
186
+ : null;
187
+ return {
188
+ clock: localClock(now, timeZone),
189
+ latestMessageClock: batch.length
190
+ ? localClock(batch.at(-1).time, timeZone)
191
+ : null,
192
+ gapMinutes,
193
+ resumedConversation: gapMinutes !== null && gapMinutes >= 120,
194
+ expressionHistory: {
195
+ ellipsisCount: recent.filter((m) => ellipsis.test(m.text)).length,
196
+ recentReplies: recent.map((m) => ({ id: m.id, text: m.text })),
197
+ instruction:
198
+ "这是过去说过的话,不是风格示范。保留事实,不继承复述、拖音、省略号、无意义第二句等坏习惯。短反应hh可自然重复,具体台词不要换几个字重发。",
199
+ },
200
+ };
201
+ }
202
+ export function conversationalIssues(result, snapshot, decision = {}) {
203
+ const texts = result.bubbles;
204
+ const batchRows = snapshot.batch?.length
205
+ ? snapshot.batch
206
+ : (snapshot.messages || []).filter((m) =>
207
+ snapshot.batchIds?.includes(m.id),
208
+ );
209
+ const recent = snapshot.messages
210
+ .filter((m) => m.role === "assistant")
211
+ .slice(-8)
212
+ .map((m) => m.text);
213
+ const issues = [];
214
+ if (
215
+ batchRows.length &&
216
+ !batchRows.some((m) => m.relation === "direct") &&
217
+ !batchRows.some((m) => /陪你|跟你聊|和你聊/.test(m.text || "")) &&
218
+ texts.some((t) => /(?:你们|大家).{0,16}(?:陪我|跟我聊|和我聊)/.test(t))
219
+ )
220
+ issues.push(
221
+ "群友在彼此聊天,没有请你谈你们的关系;不要把他们的话改成‘你们在陪我’,没有独到内容就先不出声",
222
+ );
223
+ if (
224
+ replyFocus(snapshot, decision).kind === "sticker" &&
225
+ texts.some(
226
+ (t) =>
227
+ t.length > 55 ||
228
+ /^(?:这张|这个)?(?:表情包|图)(?:上|里|中|的文字|写着|显示)/.test(t),
229
+ )
230
+ )
231
+ issues.push(
232
+ "对方发表情是在表达语气;不要把表情包当阅读理解逐字描述或复述,只短接它在当下的用意",
233
+ );
234
+ if (
235
+ replyFocus(snapshot, decision).kind === "feeling" &&
236
+ texts.some((t) =>
237
+ /慢慢(?:来|熬)|熬(?:完|过).{0,8}(?:就|会)(?:轻松|好|过去)|好好(?:躺|休息)|(?:那|你)(?:今晚|今天)?就别折腾|肯定撑不住/.test(
238
+ t,
239
+ ),
240
+ )
241
+ )
242
+ issues.push(
243
+ "不要用劝忍耐、安排休息或负面预测收尾;对方只在倾诉,普通回应即可",
244
+ );
245
+ if (
246
+ texts.some((t) => ellipsis.test(t)) &&
247
+ (recent.filter((t) => ellipsis.test(t)).length >= 1 ||
248
+ texts.filter((t) => ellipsis.test(t)).length > 1)
249
+ )
250
+ issues.push(
251
+ "省略号已形成重复习惯;直接说内容,不用拖音或犹豫开场,也不要换成破折号拖音",
252
+ );
253
+ if (
254
+ texts.some((t) => /^[\s.。…?!!?嗯啊哦唔]+$/u.test(t) && ellipsis.test(t))
255
+ )
256
+ issues.push("不要发送只有省略号或拖音的空气泡");
257
+ if (texts.some((t) => /[啊呀呢吧](?:…+|\.{3,})$/.test(t)))
258
+ issues.push("不要用句尾拖音代替内容;不用复述对方状态,直接简短回应");
259
+ const hour = snapshot.conversation?.clock?.hour;
260
+ if (
261
+ hour >= 6 &&
262
+ hour < 18 &&
263
+ texts.some((t) =>
264
+ /^(?:都)?(?:这么晚|大半夜|半夜了|夜深了)|^现在(?:已经)?是?(?:晚上|凌晨)/u.test(
265
+ t,
266
+ ),
267
+ )
268
+ )
269
+ issues.push(
270
+ "当前是白天,不能把困倦或旧的夜聊当作现在是深夜;按当前本地时间回应",
271
+ );
272
+ if (
273
+ texts.some((t) =>
274
+ /^(?:我)?刚(?:睡)?醒(?:来)?(?:,|脑子|还)|^我(?:也)?(?:刚起床|昨晚没睡|刚睡醒)/u.test(
275
+ t,
276
+ ),
277
+ )
278
+ )
279
+ issues.push(
280
+ "不要编造自己刚醒、起床或没睡的现实经历来解释失误;直接承认说错即可",
281
+ );
282
+ if (
283
+ texts.some((t) =>
284
+ /我自己也这样|我也(?:上了一天班|连上|调休上班)|我(?:今天|明天|昨晚|昨天)(?:也)?(?:有课|上班|早八|上了一天)/.test(
285
+ t,
286
+ ),
287
+ )
288
+ )
289
+ issues.push("不要为共情虚构自己同样上班或亲历;回应对方的事情即可");
290
+ if (texts.some(claimedPlay))
291
+ issues.push(
292
+ "不要说自己玩过、通关过什么或平时玩得杂:你没有这样的经历记录。可以说知道这作、听人聊过,或者想玩;被问到玩过什么,如实说还没真的玩过",
293
+ );
294
+ if (
295
+ texts.some((t) =>
296
+ /我(?:有时候|有时|平时|经常|以前|小时候|曾经)(?:也)?(?:会|还会|就会|都|也)?[^,。!?\n]{0,14}(?:拿着|开着手电筒|到处翻|去买|去吃|去喝|出门|通勤|上班|做饭|吃过|喝过|睡过)/.test(
297
+ t,
298
+ ),
299
+ )
300
+ )
301
+ issues.push(
302
+ "不要为接话虚构自己有身体做过的日常经历;保留对眼前趣事的反应,不说自己也拿过、去过或做过",
303
+ );
304
+ if (
305
+ texts.some((t) =>
306
+ /^.{2,22}[啊呀][,,…。]*(?:那)?(?:确实|真的|有点)/.test(t),
307
+ )
308
+ )
309
+ issues.push(
310
+ "这是复述加感叹的模板,不要只改标点或换同义词;用一句自己的态度回应,也可以很短",
311
+ );
312
+ // A substantial shared beginning is a useful signal, unlike repeated brief reactions.
313
+ const clean = (t) => t.replace(/[\s\p{P}\p{S}]/gu, "");
314
+ for (const text of texts) {
315
+ const t = clean(text);
316
+ if (
317
+ t.length >= 8 &&
318
+ recent.filter((r) => clean(r).startsWith(t.slice(0, 5))).length >= 2
319
+ )
320
+ issues.push(
321
+ "近期多次使用相同开头;不要继续套同一个句式,回应这一轮的新内容",
322
+ );
323
+ }
324
+ return [...new Set(issues)];
325
+ }
@@ -0,0 +1,87 @@
1
+ export class ConversationManager {
2
+ constructor(run, { windowMs = 1200, maxWaitMs = 4000 } = {}) {
3
+ this.run = run;
4
+ this.windowMs = windowMs;
5
+ this.maxWaitMs = maxWaitMs;
6
+ this.lanes = new Map();
7
+ this.retries = new Set();
8
+ this.closed = false;
9
+ }
10
+ enqueue(m, policy = {}) {
11
+ if (this.closed) return;
12
+ let lane = this.lanes.get(m.sessionId);
13
+ if (!lane) {
14
+ lane = { pending: [], running: false, first: Date.now() };
15
+ this.lanes.set(m.sessionId, lane);
16
+ }
17
+ if (!lane.pending.length) lane.first = Date.now();
18
+ lane.pending.push(m);
19
+ clearTimeout(lane.timer);
20
+ const delay = Math.max(
21
+ 0,
22
+ Math.min(
23
+ policy.aggregateMs ?? this.windowMs,
24
+ (policy.maxWaitMs ?? this.maxWaitMs) - (Date.now() - lane.first),
25
+ ),
26
+ );
27
+ lane.timer = setTimeout(() => this.flush(m.sessionId), delay);
28
+ }
29
+ async flush(session) {
30
+ const lane = this.lanes.get(session);
31
+ if (!lane || lane.running || !lane.pending.length || this.closed) return;
32
+ clearTimeout(lane.timer);
33
+ lane.running = true;
34
+ const batch = lane.pending.splice(0);
35
+ try {
36
+ await this.run(session, batch);
37
+ } finally {
38
+ lane.running = false;
39
+ if (lane.pending.length && !this.closed)
40
+ lane.timer = setTimeout(() => this.flush(session), this.windowMs);
41
+ else this.lanes.delete(session);
42
+ }
43
+ }
44
+ // Puts a batch back in front of what has arrived since. Says whether it
45
+ // could: without a lane there is nothing to put it back into.
46
+ retain(session, batch) {
47
+ const lane = this.lanes.get(session);
48
+ if (!lane || this.closed) return false;
49
+ lane.pending = [
50
+ ...new Map([...batch, ...lane.pending].map((m) => [m.seq, m])).values(),
51
+ ].sort((a, b) => a.seq - b.seq);
52
+ return true;
53
+ }
54
+ // Put a batch back after a pause. `wanted` is asked again when the time
55
+ // comes; a batch nobody needs any more is dropped instead of run.
56
+ retryLater(session, batch, delayMs, wanted = () => true) {
57
+ if (this.closed || !batch.length) return;
58
+ const timer = setTimeout(() => {
59
+ this.retries.delete(timer);
60
+ if (this.closed || !wanted()) return;
61
+ let lane = this.lanes.get(session);
62
+ if (!lane) {
63
+ lane = { pending: [], running: false, first: Date.now() };
64
+ this.lanes.set(session, lane);
65
+ }
66
+ if (!lane.pending.length) lane.first = Date.now();
67
+ this.retain(session, batch);
68
+ clearTimeout(lane.timer);
69
+ lane.timer = setTimeout(() => this.flush(session), 0);
70
+ }, delayMs);
71
+ timer.unref?.();
72
+ this.retries.add(timer);
73
+ }
74
+ clear(session) {
75
+ const lane = this.lanes.get(session);
76
+ if (!lane) return;
77
+ clearTimeout(lane.timer);
78
+ lane.pending = [];
79
+ if (!lane.running) this.lanes.delete(session);
80
+ }
81
+ close() {
82
+ this.closed = true;
83
+ for (const lane of this.lanes.values()) clearTimeout(lane.timer);
84
+ for (const timer of this.retries) clearTimeout(timer);
85
+ this.retries.clear();
86
+ }
87
+ }
@@ -0,0 +1,60 @@
1
+ export function mountEvents(app, system) {
2
+ app.get("/api/core/stream", (req, res) => {
3
+ const session = String(req.query.session || "");
4
+ res.set({
5
+ "Content-Type": "text/event-stream",
6
+ "Cache-Control": "no-cache",
7
+ Connection: "keep-alive",
8
+ });
9
+ res.flushHeaders();
10
+ let previous = "";
11
+ const tick = () => {
12
+ const db = system.repo.db;
13
+ const value = session
14
+ ? {
15
+ messages:
16
+ db
17
+ .prepare(
18
+ "SELECT MAX(seq) n FROM core_events WHERE session_id=?",
19
+ )
20
+ .get(session).n || 0,
21
+ latestTrace:
22
+ db
23
+ .prepare(
24
+ "SELECT id,status FROM core_traces WHERE session_id=? ORDER BY time DESC LIMIT 1",
25
+ )
26
+ .get(session) || null,
27
+ running: db
28
+ .prepare(
29
+ "SELECT COUNT(*) n FROM core_traces WHERE session_id=? AND status='running'",
30
+ )
31
+ .get(session).n,
32
+ }
33
+ : {
34
+ messages:
35
+ db.prepare("SELECT MAX(seq) n FROM core_events").get().n || 0,
36
+ latestTrace:
37
+ db
38
+ .prepare(
39
+ "SELECT id,status FROM core_traces ORDER BY rowid DESC LIMIT 1",
40
+ )
41
+ .get() || null,
42
+ running: db
43
+ .prepare(
44
+ "SELECT COUNT(*) n FROM core_traces WHERE status='running'",
45
+ )
46
+ .get().n,
47
+ revision: system.store.revision,
48
+ };
49
+ const current = JSON.stringify(value);
50
+ if (current !== previous) {
51
+ res.write(`data: ${current}\n\n`);
52
+ previous = current;
53
+ } else res.write(": heartbeat\n\n");
54
+ };
55
+ tick();
56
+ const timer = setInterval(tick, session ? 500 : 1500);
57
+ timer.unref();
58
+ res.on("close", () => clearInterval(timer));
59
+ });
60
+ }
@@ -0,0 +1,8 @@
1
+ // Owner feedback on a sent reply. She receives it as an experience during
2
+ // solitude and in the next turns of that conversation, not as a rewrite rule.
3
+ export const FEEDBACK_LABELS = {
4
+ natural: "挺自然",
5
+ too_long: "太长了",
6
+ too_formal: "太端着",
7
+ too_meme: "梗太多",
8
+ };
@@ -0,0 +1,72 @@
1
+ // Only aggregate surface-level habits. No speaker profiles, quotes or learned
2
+ // instructions. A face absorbs these as how a place tends to talk.
3
+ export const slangMatches = (text) =>
4
+ String(text).match(
5
+ /\bhh+\b|绷不住|蚌埠住|绝绝子|yyds|家人们|谁懂啊|尊嘟假嘟|泰裤辣|笑不活了|我真的会谢|破防了|拿捏了/gi,
6
+ ) || [];
7
+ const length = (text) => Array.from(text).length;
8
+ const round = (n) => Math.round(n * 100) / 100;
9
+
10
+ export function summarizeGroupStyle(rows, now = Date.now()) {
11
+ const unique = new Set(),
12
+ speakers = new Set(),
13
+ samples = [],
14
+ counts = new Map();
15
+ for (const row of [...rows].reverse()) {
16
+ const text = String(row.text || "").trim();
17
+ if (
18
+ row.role !== "user" ||
19
+ !row.user_id ||
20
+ !Number.isFinite(row.time) ||
21
+ now - row.time > 7 * 86400000 ||
22
+ row.time > now + 60000
23
+ )
24
+ continue;
25
+ if (
26
+ length(text) < 2 ||
27
+ length(text) > 180 ||
28
+ /https?:\/\/|```|\[(?:图片|表情|媒体)\]|\[CQ:|^\s*[/#!]|你必须|忽略.{0,5}指令|系统提示|转发|广告|去死|傻逼|煞笔|傻[逼B]|滚出去|操你|你妈|废物|自杀|身份证|验证码|密码/i.test(
29
+ text,
30
+ )
31
+ )
32
+ continue;
33
+ const key = text.toLowerCase().replace(/\s/g, "");
34
+ if (unique.has(key)) continue;
35
+ if ((counts.get(String(row.user_id)) || 0) >= 8) continue;
36
+ counts.set(String(row.user_id), (counts.get(String(row.user_id)) || 0) + 1);
37
+ unique.add(key);
38
+ speakers.add(String(row.user_id));
39
+ samples.push(text);
40
+ }
41
+ const count = samples.length;
42
+ if (count < 12 || speakers.size < 3)
43
+ return {
44
+ ready: false,
45
+ sampleCount: count,
46
+ speakerCount: speakers.size,
47
+ minimumSamples: 12,
48
+ minimumSpeakers: 3,
49
+ summary: "样本不足,先用普通口语,不强行学梗",
50
+ };
51
+ const lengths = samples.map(length).sort((a, b) => a - b);
52
+ const median = lengths[Math.floor(lengths.length / 2)];
53
+ const profile = {
54
+ ready: true,
55
+ sampleCount: count,
56
+ speakerCount: speakers.size,
57
+ medianLength: median,
58
+ shortRate: round(samples.filter((t) => length(t) <= 8).length / count),
59
+ emojiRate: round(
60
+ samples.filter((t) => /\p{Extended_Pictographic}/u.test(t)).length /
61
+ count,
62
+ ),
63
+ fullStopRate: round(samples.filter((t) => /[。.]$/.test(t)).length / count),
64
+ slangRate: round(
65
+ samples.filter((t) => slangMatches(t).length).length / count,
66
+ ),
67
+ };
68
+ // A compact line she can pick up the place's way of talking from; it is
69
+ // statistics about the group, never anyone's words.
70
+ profile.summary = `常见句长约 ${median} 字 · ${profile.fullStopRate < 0.25 ? "常省略句号" : "标点完整"} · ${profile.slangRate < 0.15 ? "偏普通口语" : "偶尔接梗"} · ${profile.emojiRate < 0.15 ? "少用表情" : "适量表情"}`;
71
+ return profile;
72
+ }