luckytri 1.0.6 → 1.0.7

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 (155) hide show
  1. package/LICENSE +21 -21
  2. package/docs/en/CHANGELOG.md +25 -16
  3. package/docs/en/chat-style.md +28 -9
  4. package/docs/en/guide.md +5 -5
  5. package/docs/en/her-life.md +2 -2
  6. package/docs/en/webui.md +1 -1
  7. package/docs/zh/CHANGELOG.md +25 -16
  8. package/docs/zh/chat-style.md +30 -9
  9. package/docs/zh/guide.md +5 -5
  10. package/docs/zh/her-life.md +2 -2
  11. package/docs/zh/webui.md +1 -1
  12. package/package.json +1 -1
  13. package/public/app/assets/{ChatsPage-DCYMkJ91.js → ChatsPage-CshdtTzV.js} +2 -2
  14. package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
  15. package/public/app/assets/{Empty-CLyZkxR5.js → Empty-C7yzOBzy.js} +1 -1
  16. package/public/app/assets/{Empty-DIobYG3x.css → Empty-DmUB2-Bk.css} +1 -1
  17. package/public/app/assets/{Field-BpaRwLI_.js → Field-BjFv4Rfn.js} +1 -1
  18. package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
  19. package/public/app/assets/{HeartPage-BIOMTiwr.js → HeartPage-eqo6ru16.js} +1 -1
  20. package/public/app/assets/LifePage-BUi5euBF.css +1 -0
  21. package/public/app/assets/{LifePage-BCgM4JkR.js → LifePage-CN8GE5NK.js} +1 -1
  22. package/public/app/assets/{MemoryPage-vShvRqMt.js → MemoryPage-CCpMN4Wg.js} +1 -1
  23. package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
  24. package/public/app/assets/{NaturePage-Cy9ptr4G.js → NaturePage-C0JqdBTR.js} +2 -2
  25. package/public/app/assets/NaturePage-CHkpb5NK.css +1 -0
  26. package/public/app/assets/PeoplePage-BTU0H1pM.css +1 -0
  27. package/public/app/assets/{PeoplePage-CUZekB2E.js → PeoplePage-BXGBbO4u.js} +1 -1
  28. package/public/app/assets/{PluginsPage-1RM4rmJP.js → PluginsPage-Dksp5AV5.js} +1 -1
  29. package/public/app/assets/{SystemPage-BI2CB5Ig.css → SystemPage-BenIyTvk.css} +1 -1
  30. package/public/app/assets/{SystemPage-BeT5TmIV.js → SystemPage-Cd7aa8eE.js} +1 -1
  31. package/public/app/assets/TimePage-BOms_n4I.css +1 -0
  32. package/public/app/assets/{TimePage-taTmPMM8.js → TimePage-Bs_zImb4.js} +2 -2
  33. package/public/app/assets/{index-DY5AM0Hz.css → index-BHZcHUMh.css} +1 -1
  34. package/public/app/assets/{index-DdYwC5ZU.js → index-CyRtYxP3.js} +3 -3
  35. package/public/app/assets/{knowledge-DJmfu2KN.js → knowledge-BJOIyuJo.js} +1 -1
  36. package/public/app/assets/{media-BEtnE0Gv.js → media-Bwtx8blz.js} +1 -1
  37. package/public/app/assets/{mind-Dtiw0-QK.js → mind-DtTVrW6Z.js} +1 -1
  38. package/public/app/index.html +5 -6
  39. package/public/guide.en.html +3 -3
  40. package/public/guide.html +3 -3
  41. package/scripts/import-data.js +128 -128
  42. package/scripts/proxy-config.js +167 -167
  43. package/scripts/update-instance.js +201 -201
  44. package/server/channels/adapters.js +27 -27
  45. package/server/channels/onebot/adapter.js +288 -288
  46. package/server/channels/qqbot/adapter.js +253 -253
  47. package/server/channels/qqbot/api.js +199 -199
  48. package/server/channels/qqbot/credentials.js +11 -11
  49. package/server/channels/qqbot/gateway.js +274 -274
  50. package/server/channels/qqbot/index.js +389 -389
  51. package/server/channels/segments.js +40 -40
  52. package/server/channels/session-key.js +153 -153
  53. package/server/channels/state.js +42 -42
  54. package/server/core/api.js +564 -564
  55. package/server/core/context-builder.js +288 -288
  56. package/server/core/context-compactor.js +454 -454
  57. package/server/core/conversation-cues.js +365 -325
  58. package/server/core/conversation-grounding.js +178 -0
  59. package/server/core/conversation-manager.js +87 -87
  60. package/server/core/dialogue-context.js +52 -0
  61. package/server/core/feedback.js +8 -8
  62. package/server/core/group-style.js +72 -72
  63. package/server/core/initiative-context.js +66 -66
  64. package/server/core/input-budget.js +32 -32
  65. package/server/core/local-demo.js +98 -98
  66. package/server/core/message-manager.js +118 -118
  67. package/server/core/message-scheduler.js +78 -78
  68. package/server/core/model-json.js +190 -190
  69. package/server/core/model-presets.js +900 -900
  70. package/server/core/name-call.js +29 -29
  71. package/server/core/network.js +157 -157
  72. package/server/core/person-name.js +22 -22
  73. package/server/core/persona-manager.js +324 -269
  74. package/server/core/reply-delivery.js +472 -404
  75. package/server/core/reply-target-resolver.js +50 -50
  76. package/server/core/response-generator.js +58 -50
  77. package/server/core/response-validator.js +30 -27
  78. package/server/core/retired-prompts.js +51 -51
  79. package/server/core/sessions.js +109 -109
  80. package/server/core/speaker-names.js +92 -92
  81. package/server/core/topic-tracker.js +65 -65
  82. package/server/core/turn.js +42 -8
  83. package/server/http.js +18 -18
  84. package/server/i18n/decisions.js +209 -209
  85. package/server/i18n/index.js +144 -144
  86. package/server/i18n/labels.js +0 -5
  87. package/server/i18n/models.js +111 -111
  88. package/server/knowledge/api.js +342 -342
  89. package/server/knowledge/embedding-profile.js +114 -114
  90. package/server/knowledge/index.js +13 -13
  91. package/server/knowledge/retrieval.js +93 -93
  92. package/server/knowledge/schema.js +158 -158
  93. package/server/knowledge/vector-index.js +90 -90
  94. package/server/mind/affect.js +265 -265
  95. package/server/mind/attention.js +200 -200
  96. package/server/mind/bonds.js +565 -565
  97. package/server/mind/clock.js +83 -83
  98. package/server/mind/context-selection.js +16 -4
  99. package/server/mind/continuity.js +562 -562
  100. package/server/mind/conversation-origin.js +175 -175
  101. package/server/mind/days.js +210 -210
  102. package/server/mind/evidence-read.js +45 -45
  103. package/server/mind/faces.js +373 -373
  104. package/server/mind/guard.js +68 -60
  105. package/server/mind/initiative.js +338 -338
  106. package/server/mind/life/activities.js +21 -21
  107. package/server/mind/life/context.js +330 -330
  108. package/server/mind/life/growth.js +261 -261
  109. package/server/mind/life/presence.js +305 -305
  110. package/server/mind/meetings.js +1069 -1069
  111. package/server/mind/memory-policy.js +48 -48
  112. package/server/mind/memory.js +888 -888
  113. package/server/mind/nature.js +358 -358
  114. package/server/mind/novelty.js +19 -19
  115. package/server/mind/periods.js +118 -118
  116. package/server/mind/reading.js +120 -120
  117. package/server/mind/relationship-context.js +71 -71
  118. package/server/mind/relationships.js +390 -390
  119. package/server/mind/salience.js +201 -201
  120. package/server/mind/schema.js +567 -567
  121. package/server/mind/self.js +496 -484
  122. package/server/mind/thoughts.js +274 -274
  123. package/server/mind/time/agenda.js +242 -242
  124. package/server/mind/time/api.js +200 -200
  125. package/server/mind/time/attention.js +34 -34
  126. package/server/mind/time/delivery-links.js +130 -130
  127. package/server/mind/time/games.js +7 -3
  128. package/server/mind/time/index.js +3 -0
  129. package/server/mind/time/model-material.js +77 -77
  130. package/server/mind/time/presentation.js +35 -51
  131. package/server/mind/time/schedule.js +83 -83
  132. package/server/mind/time/schema.js +84 -84
  133. package/server/mind/time/search.js +383 -383
  134. package/server/mind/time/sharing.js +472 -472
  135. package/server/mind/time/task-links.js +311 -311
  136. package/server/mind/time/works.js +438 -432
  137. package/server/mind/traits.js +191 -191
  138. package/server/mind/view.js +43 -6
  139. package/server/runtime/installation.js +116 -116
  140. package/server/runtime/update-manager.js +140 -140
  141. package/server/storage/data-package.js +298 -298
  142. package/server/storage/data-transfer-api.js +50 -50
  143. package/server/storage/data-transfer-worker.js +18 -18
  144. package/server/storage/data-transfer.js +416 -416
  145. package/server/studio/display-names.js +139 -139
  146. package/server/studio/management.js +102 -102
  147. package/server/studio/updates.js +144 -144
  148. package/server/version.js +10 -10
  149. package/public/app/assets/ChatsPage-CFMVozoG.css +0 -1
  150. package/public/app/assets/HeartPage-C-2jvgzF.css +0 -1
  151. package/public/app/assets/LifePage-CG1OHbRK.css +0 -1
  152. package/public/app/assets/MemoryPage-C_Bu4rfb.css +0 -1
  153. package/public/app/assets/NaturePage-CPZlImT0.css +0 -1
  154. package/public/app/assets/PeoplePage-3RRUITDz.css +0 -1
  155. package/public/app/assets/TimePage-C-guwPk2.css +0 -1
@@ -1,338 +1,338 @@
1
- import { HOUR, parse, text } from "./util.js";
2
- import { identityAliases } from "../core/persona-manager.js";
3
- import { isPrivateSession } from "./memory.js";
4
- import { parseSessionKey, isGroupSession } from "../channels/session-key.js";
5
- import { sameRecentTheme } from "./novelty.js";
6
- import { initiativeAudience, originSummary } from "./conversation-origin.js";
7
-
8
- // A chance to start something is different from a request to answer someone.
9
- // Time changes what she can notice; it never increments a loneliness score or
10
- // obliges her to speak. The model receives the situation and makes the choice.
11
- export const INITIATIVE_PROMPT = `【这一轮是你自己发起,不是补答历史消息】
12
- occasion 中的 initiative 是现在的实际情况,历史 messages 只是回忆。没有新消息、没人叫你、别人已经得到回答,都不是禁止你开新话题的理由。
13
- 先看自己的兴趣、正在想做的事、留下的念头、对人的感觉,再决定此刻想不想和这里的人说话。可以只是想分享一个念头、闲聊、问一个自己好奇的问题,或者想找熟悉的人一起聊点什么;不需要重大事件或待处理的问题。安静了一阵也可能让你无聊、想念或想有人一起说说话,也可能正好想独处,按你此刻真正的倾向选择,不为完成主动指标发言。
14
- 想说就 speak,自然的一两句即可;没有想说的就 silent,说明自己的实际倾向。主动程度影响你有多愿意先开口,不是必须有人先问的门槛。
15
- 主动是给对方一个可接也可不接的邀请,不是催促。底线里的“不索取陪伴”指不施压、不制造亏欠,不禁止你主动邀请、表达无聊或想聊。不装作刚看过、做过或经历过没有证据的事。
16
- initiative.awaitingReply 表示上一句主动消息尚未得到回应:不重问、不追责、不换句话催同一件事;隔了一段时间,有另一个自己确实想说的内容仍可以开口。对方明确不想被联系时尊重它。
17
- outreach 的 planned 是过去想说的话,不是必须照抄的命令;结合现在的消息和经过的时间重新判断。不要机械接最后一条旧消息,不把私聊里的信息拿到群里。
18
- expression.audience 核实这段话题的来源和实际参与者。记忆相通不等于对方参与过:不能把别人的项目、约定、群聊问题说成“咱们的事”,也不能因亲近就认定对方知道上下文。共同经历可以换到私聊接着说。
19
- 未参与者也可以成为分享对象。shared:false / mode:new_topic 只表示这是对方的新话题,不是禁止联系。先结合自己的分享动机、对方的兴趣、关系与最近的交流,判断这件事是否值得和他聊;值得就 speak,自然给必要背景,再说自己想分享的想法或问题,不值得或只是在搬运别人聊天就 silent。reason 留下自己的具体判断,不因亲近就把技术建议误认成对方的需求。不要求重大事件,也不机械介绍每个话题的出处;自己的新想法、有意思的细节、想听他的看法都可以是理由。来源只证明当时发生过什么,不证明当前对象已经知道或想继续聊。`;
20
-
21
- export const SOLITUDE_INITIATIVE_PROMPT = `【独处也可以产生向外的愿望】
22
- initiative.contacts 是你曾来往、现在允许参与的地方,带有最后交流的时间和少量旧话。新消息很少不代表你停止存在:可以重新注意自己的兴趣、想做的事、想接近的人,也可以无聊或想聊一点普通的东西。没有新事实时不要强化旧猜测。
23
- 想联系谁就写 outreach;它独立于 thought,不必先写出一篇新总结。允许 skip:true(没有新理解)同时给出 outreach(有想说的话)。没有重大事件也可以提出一个具体的闲聊邀请或自己的问题。text 是想说的实际内容,reason 是此刻为什么想说,sources 引用这次提供的来源;纯粹属于你此刻的愿望可为空,不能借此捏造外部事实。
24
- outreach.session 必须来自 contacts 或其他本轮给出的会话。afterHours:0 表示现在就想说,正数才是你自愿留到以后;不用为了克制而一律推迟。可输出 {"outreach":{"session":"会话ID","text":"想说的话","reason":"自己的动机","sources":[],"afterHours":0}}。是否真的合适会在发送前结合最新对话再由你决定。
25
- 你可以想陪伴,也可以邀请别人陪你做点什么;不施压、不催回复、不说对方欠你陪伴。无聊和想念是可表达的倾向,不是必须表演的词。也完全可以什么都不说。`;
26
-
27
- export class Initiative {
28
- constructor(life) {
29
- this.life = life;
30
- this.db = life.db;
31
- }
32
- lastSent(session) {
33
- return (
34
- this.db
35
- .prepare(
36
- "SELECT o.time FROM core_outbox o JOIN core_traces tr ON tr.id=o.trace_id WHERE o.session_id=? AND o.status IN ('sending','confirmed','uncertain') AND json_extract(tr.data,'$.path') IN ('outreach','presence') ORDER BY o.time DESC LIMIT 1",
37
- )
38
- .get(session)?.time || null
39
- );
40
- }
41
- context(session, now = this.life.now()) {
42
- const { mind } = this.life;
43
- const messages = this.db
44
- .prepare(
45
- "SELECT seq,time,role,payload FROM core_events WHERE session_id=? AND seq NOT IN (SELECT seq FROM mind_unlived) AND COALESCE(json_extract(payload,'$.simulated'),0)=0 ORDER BY seq DESC LIMIT 6",
46
- )
47
- .all(session)
48
- .reverse();
49
- const last = messages.at(-1);
50
- const lastUser = this.db
51
- .prepare(
52
- "SELECT time t FROM core_events WHERE session_id=? AND role='user' AND seq NOT IN (SELECT seq FROM mind_unlived) AND COALESCE(json_extract(payload,'$.simulated'),0)=0 ORDER BY seq DESC LIMIT 1",
53
- )
54
- .get(session)?.t;
55
- const sent = this.lastSent(session);
56
- return {
57
- session,
58
- quietMinutes: last
59
- ? Math.max(0, Math.floor((now - last.time) / 60000))
60
- : null,
61
- sinceUserMinutes: lastUser
62
- ? Math.max(0, Math.floor((now - lastUser) / 60000))
63
- : null,
64
- lastInitiatedAt: sent || null,
65
- awaitingReply: !!(sent && (!lastUser || lastUser <= sent)),
66
- recent: messages.map((m) => {
67
- const p = parse(m.payload, {});
68
- return {
69
- ref: `m:${m.seq}`,
70
- role: m.role,
71
- name: p.name,
72
- time: m.time,
73
- text: text(p.text, 160),
74
- };
75
- }),
76
- thoughts: mind.thoughts
77
- .open({ session, now, limit: 3 })
78
- .map((t) => ({ ref: `t:${t.id}`, content: text(t.content, 160) })),
79
- };
80
- }
81
- contacts(now = this.life.now(), { ready = false, limit = 5 } = {}) {
82
- const tried = this.life.repo.config("presence", {});
83
- const names = [
84
- this.life.mind.nature.current(now).name,
85
- ...identityAliases(
86
- this.life.mind.nature.current(now).name,
87
- String(this.life.repo.store.settings().aliases || ""),
88
- ).split(/[,,]/),
89
- ];
90
- const rooms = this.life
91
- .living({ memory: false })
92
- .filter((s) => this.life.wasHere(s.id, now, names))
93
- .sort((a, b) => (tried[a.id] || 0) - (tried[b.id] || 0));
94
- const settings = this.life.settings();
95
- const contacts = [];
96
- for (const s of rooms) {
97
- if (
98
- ready &&
99
- (this.life.presenceCooling(s.id, now, settings) ||
100
- this.blocked(s.id, now, settings))
101
- )
102
- continue;
103
- const contact = {
104
- name: s.name,
105
- kind: s.kind,
106
- ...this.context(s.id, now),
107
- };
108
- if (
109
- ready &&
110
- (contact.quietMinutes === null ||
111
- contact.quietMinutes < Math.max(settings.idleMinutes, 1))
112
- )
113
- continue;
114
- contacts.push(contact);
115
- if (contacts.length >= limit) break;
116
- }
117
- return contacts;
118
- }
119
- // One formed thought gets one place. The choice follows where it has roots
120
- // and whom she actually knows, rather than rotating through idle rooms.
121
- chooseContact(note, contacts, now = this.life.now()) {
122
- const kind = note?.audience;
123
- const thought = note?.id ? this.life.mind.thoughts.get(note.id) : null;
124
- const candidates = contacts.filter(
125
- (contact) =>
126
- (kind === "either" || contact.kind === kind) &&
127
- this.audience(thought, contact.session, now).allowed,
128
- );
129
- if (!candidates.length) return null;
130
- const roots = new Set(
131
- this.audience(thought, candidates[0].session, now).origin.rooms.map(
132
- (room) => room.sessionId,
133
- ),
134
- );
135
- const score = (contact) => {
136
- let bond = null;
137
- if (contact.kind === "private") {
138
- try {
139
- bond = this.life.mind.bonds.person(
140
- parseSessionKey(contact.session).nativeId,
141
- now,
142
- );
143
- } catch {
144
- return -Infinity;
145
- }
146
- } else bond = this.life.mind.bonds.group(contact.session, now);
147
- const closeness = Number(bond?.closeness || 0);
148
- const familiarity = Number(bond?.familiarity || 0);
149
- const tension = Number(bond?.tension || 0);
150
- const talked = Number(bond?.interactions || 0);
151
- const rooted = roots.has(contact.session) ? 2.5 : 0;
152
- const fit = (contact.recent || []).some((line) =>
153
- sameRecentTheme(line.text, note.words?.join("\n")),
154
- )
155
- ? 0.5
156
- : 0;
157
- return (
158
- rooted +
159
- fit +
160
- closeness * 2 +
161
- Number(bond?.relationalWeight || 0) * 2 +
162
- familiarity +
163
- Math.min(5, talked) * 0.08 -
164
- tension * 2 -
165
- (contact.awaitingReply ? 0.35 : 0)
166
- );
167
- };
168
- return candidates
169
- .map((contact) => ({ contact, score: score(contact) }))
170
- .sort(
171
- (a, b) =>
172
- b.score - a.score ||
173
- a.contact.session.localeCompare(b.contact.session),
174
- )[0]?.contact;
175
- }
176
- audience(thought, session, now = this.life.now()) {
177
- const result = initiativeAudience(this.life.mind, thought, session, now);
178
- return { ...result, origin: originSummary(result.origin) };
179
- }
180
- recentlyToldAnotherGroup(thought, now = this.life.now()) {
181
- if (!isGroupSession(thought?.outreach_session)) return false;
182
- const earlier = this.db
183
- .prepare(
184
- "SELECT id,outreach,sources,outreach_session FROM mind_thoughts WHERE outreach_status='sent' AND created>=? AND created<? ORDER BY created DESC LIMIT 40",
185
- )
186
- .all(now - 12 * HOUR, now);
187
- return earlier.some(
188
- (row) =>
189
- row.id !== thought.id &&
190
- isGroupSession(row.outreach_session) &&
191
- row.outreach_session !== thought.outreach_session &&
192
- (sameRecentTheme(row.outreach, thought.outreach) ||
193
- (thought.sources || []).includes(`t:${row.id}`) ||
194
- parse(row.sources, []).includes(`t:${thought.id}`)),
195
- );
196
- }
197
- sourceMaterial(thought, now = this.life.now()) {
198
- const self = this.life.mind.self.latest(now);
199
- const material = [];
200
- for (const ref of (thought.sources || []).slice(0, 10)) {
201
- const id = ref.slice(2);
202
- if (ref.startsWith("m:")) {
203
- const row = this.db
204
- .prepare(
205
- "SELECT time,role,payload,session_id FROM core_events WHERE seq=?",
206
- )
207
- .get(Number(id));
208
- if (
209
- !row ||
210
- (isPrivateSession(row.session_id) &&
211
- row.session_id !== thought.outreach_session)
212
- )
213
- continue;
214
- const message = parse(row.payload, {});
215
- material.push({
216
- ref,
217
- kind: "past_message",
218
- role: row.role,
219
- name: message.name,
220
- time: row.time,
221
- sourceSession: row.session_id,
222
- content: text(message.text, 300),
223
- referenceOnly: true,
224
- });
225
- } else if (ref.startsWith("s:")) {
226
- const thread = self.find((s) => s.thread === id);
227
- if (
228
- thread &&
229
- this.life.mind.meetings.stays(thread, thought.outreach_session)
230
- )
231
- material.push({ ref, kind: "self", content: thread.content });
232
- } else if (ref.startsWith("t:")) {
233
- const note = this.life.mind.thoughts.get(id);
234
- if (
235
- note &&
236
- this.life.mind.meetings.stays(
237
- {
238
- ...note,
239
- session_id: note.sessions.find(isPrivateSession) || "",
240
- },
241
- thought.outreach_session,
242
- )
243
- )
244
- material.push({
245
- ref,
246
- kind: "earlier_thought",
247
- content: text(note.content, 300),
248
- time: note.created,
249
- certainty: "当时的想法,不自动成为外部事实",
250
- });
251
- } else if (ref.startsWith("a:")) {
252
- const event = this.life.mind.anticipations.get(id);
253
- if (
254
- event &&
255
- (!isPrivateSession(event.session_id) ||
256
- event.session_id === thought.outreach_session) &&
257
- (event.discretion === "open" ||
258
- event.session_id === thought.outreach_session)
259
- )
260
- material.push({
261
- ref,
262
- kind: "anticipation",
263
- content: event.content,
264
- due: event.due_at,
265
- status: event.status,
266
- recordedAt: event.created,
267
- });
268
- } else if (ref.startsWith("g:")) {
269
- const meeting = this.db
270
- .prepare(
271
- "SELECT created,session_id,appraisal,discretion FROM mind_meetings WHERE id=?",
272
- )
273
- .get(id);
274
- if (
275
- meeting &&
276
- (meeting.discretion === "open" ||
277
- meeting.session_id === thought.outreach_session) &&
278
- (!isPrivateSession(meeting.session_id) ||
279
- meeting.session_id === thought.outreach_session)
280
- )
281
- material.push({
282
- ref,
283
- kind: "past_understanding",
284
- content: text(meeting.appraisal, 300),
285
- time: meeting.created,
286
- certainty: "当时对相遇的理解,不是新收到的消息",
287
- });
288
- } else if (ref.startsWith("r:")) {
289
- const reading = this.db
290
- .prepare(
291
- "SELECT created,title,note,ordinal FROM mind_readings WHERE chunk_id=? AND created<=? ORDER BY created DESC LIMIT 1",
292
- )
293
- .get(id, now);
294
- if (reading)
295
- material.push({
296
- ref,
297
- kind: "past_reading",
298
- title: reading.title,
299
- note: text(reading.note, 300),
300
- part: reading.ordinal + 1,
301
- time: reading.created,
302
- });
303
- }
304
- }
305
- return material;
306
- }
307
- blocked(session, now, settings) {
308
- if (!session || !this.life.chat.enabled(session, { simulated: false }))
309
- return { reason: "会话不可用", permanent: true };
310
- // A room that has switched proactive messages off is not a place she can
311
- // knock on: she waits to be sought out, and looks again later.
312
- if (!this.life.canReach(session))
313
- return {
314
- reason: "对方没有开启主动消息,等对方来找她",
315
- retryAt: now + 6 * HOUR,
316
- };
317
- const lastUser = this.db
318
- .prepare(
319
- "SELECT time,payload FROM core_events WHERE session_id=? AND role='user' AND seq NOT IN (SELECT seq FROM mind_unlived) AND COALESCE(json_extract(payload,'$.simulated'),0)=0 ORDER BY seq DESC LIMIT 1",
320
- )
321
- .get(session);
322
- const words = parse(lastUser?.payload, {}).text || "";
323
- // Explicit contact boundaries are different from “this message needs no reply”.
324
- if (
325
- /(?:别|不要|不用)再?(?:主动)?(?:给我发消息|找我|联系我|私聊我)|别再打扰我/.test(
326
- words,
327
- )
328
- )
329
- return { reason: "对方明确不想被主动联系", permanent: true };
330
- if (this.life.chat.queue.lanes.has(session))
331
- return { reason: "这段对话正在处理,稍后重新看看", retryAt: now + 60000 };
332
- const sent = this.lastSent(session);
333
- const interval = settings.proactiveIntervalHours * HOUR;
334
- if (sent && now - sent < interval)
335
- return { reason: "刚主动说过,愿望仍保留", retryAt: sent + interval };
336
- return null;
337
- }
338
- }
1
+ import { HOUR, parse, text } from "./util.js";
2
+ import { identityAliases } from "../core/persona-manager.js";
3
+ import { isPrivateSession } from "./memory.js";
4
+ import { parseSessionKey, isGroupSession } from "../channels/session-key.js";
5
+ import { sameRecentTheme } from "./novelty.js";
6
+ import { initiativeAudience, originSummary } from "./conversation-origin.js";
7
+
8
+ // A chance to start something is different from a request to answer someone.
9
+ // Time changes what she can notice; it never increments a loneliness score or
10
+ // obliges her to speak. The model receives the situation and makes the choice.
11
+ export const INITIATIVE_PROMPT = `【这一轮是你自己发起,不是补答历史消息】
12
+ occasion 中的 initiative 是现在的实际情况,历史 messages 只是回忆。没有新消息、没人叫你、别人已经得到回答,都不是禁止你开新话题的理由。
13
+ 先看自己的兴趣、正在想做的事、留下的念头、对人的感觉,再决定此刻想不想和这里的人说话。可以只是想分享一个念头、闲聊、问一个自己好奇的问题,或者想找熟悉的人一起聊点什么;不需要重大事件或待处理的问题。安静了一阵也可能让你无聊、想念或想有人一起说说话,也可能正好想独处,按你此刻真正的倾向选择,不为完成主动指标发言。
14
+ 想说就 speak,自然的一两句即可;没有想说的就 silent,说明自己的实际倾向。主动程度影响你有多愿意先开口,不是必须有人先问的门槛。
15
+ 主动是给对方一个可接也可不接的邀请,不是催促。底线里的“不索取陪伴”指不施压、不制造亏欠,不禁止你主动邀请、表达无聊或想聊。不装作刚看过、做过或经历过没有证据的事。
16
+ initiative.awaitingReply 表示上一句主动消息尚未得到回应:不重问、不追责、不换句话催同一件事;隔了一段时间,有另一个自己确实想说的内容仍可以开口。对方明确不想被联系时尊重它。
17
+ outreach 的 planned 是过去想说的话,不是必须照抄的命令;结合现在的消息和经过的时间重新判断。不要机械接最后一条旧消息,不把私聊里的信息拿到群里。
18
+ expression.audience 核实这段话题的来源和实际参与者。记忆相通不等于对方参与过:不能把别人的项目、约定、群聊问题说成“咱们的事”,也不能因亲近就认定对方知道上下文。共同经历可以换到私聊接着说。
19
+ 未参与者也可以成为分享对象。shared:false / mode:new_topic 只表示这是对方的新话题,不是禁止联系。先结合自己的分享动机、对方的兴趣、关系与最近的交流,判断这件事是否值得和他聊;值得就 speak,自然给必要背景,再说自己想分享的想法或问题,不值得或只是在搬运别人聊天就 silent。reason 留下自己的具体判断,不因亲近就把技术建议误认成对方的需求。不要求重大事件,也不机械介绍每个话题的出处;自己的新想法、有意思的细节、想听他的看法都可以是理由。来源只证明当时发生过什么,不证明当前对象已经知道或想继续聊。`;
20
+
21
+ export const SOLITUDE_INITIATIVE_PROMPT = `【独处也可以产生向外的愿望】
22
+ initiative.contacts 是你曾来往、现在允许参与的地方,带有最后交流的时间和少量旧话。新消息很少不代表你停止存在:可以重新注意自己的兴趣、想做的事、想接近的人,也可以无聊或想聊一点普通的东西。没有新事实时不要强化旧猜测。
23
+ 想联系谁就写 outreach;它独立于 thought,不必先写出一篇新总结。允许 skip:true(没有新理解)同时给出 outreach(有想说的话)。没有重大事件也可以提出一个具体的闲聊邀请或自己的问题。text 是想说的实际内容,reason 是此刻为什么想说,sources 引用这次提供的来源;纯粹属于你此刻的愿望可为空,不能借此捏造外部事实。
24
+ outreach.session 必须来自 contacts 或其他本轮给出的会话。afterHours:0 表示现在就想说,正数才是你自愿留到以后;不用为了克制而一律推迟。可输出 {"outreach":{"session":"会话ID","text":"想说的话","reason":"自己的动机","sources":[],"afterHours":0}}。是否真的合适会在发送前结合最新对话再由你决定。
25
+ 你可以想陪伴,也可以邀请别人陪你做点什么;不施压、不催回复、不说对方欠你陪伴。无聊和想念是可表达的倾向,不是必须表演的词。也完全可以什么都不说。`;
26
+
27
+ export class Initiative {
28
+ constructor(life) {
29
+ this.life = life;
30
+ this.db = life.db;
31
+ }
32
+ lastSent(session) {
33
+ return (
34
+ this.db
35
+ .prepare(
36
+ "SELECT o.time FROM core_outbox o JOIN core_traces tr ON tr.id=o.trace_id WHERE o.session_id=? AND o.status IN ('sending','confirmed','uncertain') AND json_extract(tr.data,'$.path') IN ('outreach','presence') ORDER BY o.time DESC LIMIT 1",
37
+ )
38
+ .get(session)?.time || null
39
+ );
40
+ }
41
+ context(session, now = this.life.now()) {
42
+ const { mind } = this.life;
43
+ const messages = this.db
44
+ .prepare(
45
+ "SELECT seq,time,role,payload FROM core_events WHERE session_id=? AND seq NOT IN (SELECT seq FROM mind_unlived) AND COALESCE(json_extract(payload,'$.simulated'),0)=0 ORDER BY seq DESC LIMIT 6",
46
+ )
47
+ .all(session)
48
+ .reverse();
49
+ const last = messages.at(-1);
50
+ const lastUser = this.db
51
+ .prepare(
52
+ "SELECT time t FROM core_events WHERE session_id=? AND role='user' AND seq NOT IN (SELECT seq FROM mind_unlived) AND COALESCE(json_extract(payload,'$.simulated'),0)=0 ORDER BY seq DESC LIMIT 1",
53
+ )
54
+ .get(session)?.t;
55
+ const sent = this.lastSent(session);
56
+ return {
57
+ session,
58
+ quietMinutes: last
59
+ ? Math.max(0, Math.floor((now - last.time) / 60000))
60
+ : null,
61
+ sinceUserMinutes: lastUser
62
+ ? Math.max(0, Math.floor((now - lastUser) / 60000))
63
+ : null,
64
+ lastInitiatedAt: sent || null,
65
+ awaitingReply: !!(sent && (!lastUser || lastUser <= sent)),
66
+ recent: messages.map((m) => {
67
+ const p = parse(m.payload, {});
68
+ return {
69
+ ref: `m:${m.seq}`,
70
+ role: m.role,
71
+ name: p.name,
72
+ time: m.time,
73
+ text: text(p.text, 160),
74
+ };
75
+ }),
76
+ thoughts: mind.thoughts
77
+ .open({ session, now, limit: 3 })
78
+ .map((t) => ({ ref: `t:${t.id}`, content: text(t.content, 160) })),
79
+ };
80
+ }
81
+ contacts(now = this.life.now(), { ready = false, limit = 5 } = {}) {
82
+ const tried = this.life.repo.config("presence", {});
83
+ const names = [
84
+ this.life.mind.nature.current(now).name,
85
+ ...identityAliases(
86
+ this.life.mind.nature.current(now).name,
87
+ String(this.life.repo.store.settings().aliases || ""),
88
+ ).split(/[,,]/),
89
+ ];
90
+ const rooms = this.life
91
+ .living({ memory: false })
92
+ .filter((s) => this.life.wasHere(s.id, now, names))
93
+ .sort((a, b) => (tried[a.id] || 0) - (tried[b.id] || 0));
94
+ const settings = this.life.settings();
95
+ const contacts = [];
96
+ for (const s of rooms) {
97
+ if (
98
+ ready &&
99
+ (this.life.presenceCooling(s.id, now, settings) ||
100
+ this.blocked(s.id, now, settings))
101
+ )
102
+ continue;
103
+ const contact = {
104
+ name: s.name,
105
+ kind: s.kind,
106
+ ...this.context(s.id, now),
107
+ };
108
+ if (
109
+ ready &&
110
+ (contact.quietMinutes === null ||
111
+ contact.quietMinutes < Math.max(settings.idleMinutes, 1))
112
+ )
113
+ continue;
114
+ contacts.push(contact);
115
+ if (contacts.length >= limit) break;
116
+ }
117
+ return contacts;
118
+ }
119
+ // One formed thought gets one place. The choice follows where it has roots
120
+ // and whom she actually knows, rather than rotating through idle rooms.
121
+ chooseContact(note, contacts, now = this.life.now()) {
122
+ const kind = note?.audience;
123
+ const thought = note?.id ? this.life.mind.thoughts.get(note.id) : null;
124
+ const candidates = contacts.filter(
125
+ (contact) =>
126
+ (kind === "either" || contact.kind === kind) &&
127
+ this.audience(thought, contact.session, now).allowed,
128
+ );
129
+ if (!candidates.length) return null;
130
+ const roots = new Set(
131
+ this.audience(thought, candidates[0].session, now).origin.rooms.map(
132
+ (room) => room.sessionId,
133
+ ),
134
+ );
135
+ const score = (contact) => {
136
+ let bond = null;
137
+ if (contact.kind === "private") {
138
+ try {
139
+ bond = this.life.mind.bonds.person(
140
+ parseSessionKey(contact.session).nativeId,
141
+ now,
142
+ );
143
+ } catch {
144
+ return -Infinity;
145
+ }
146
+ } else bond = this.life.mind.bonds.group(contact.session, now);
147
+ const closeness = Number(bond?.closeness || 0);
148
+ const familiarity = Number(bond?.familiarity || 0);
149
+ const tension = Number(bond?.tension || 0);
150
+ const talked = Number(bond?.interactions || 0);
151
+ const rooted = roots.has(contact.session) ? 2.5 : 0;
152
+ const fit = (contact.recent || []).some((line) =>
153
+ sameRecentTheme(line.text, note.words?.join("\n")),
154
+ )
155
+ ? 0.5
156
+ : 0;
157
+ return (
158
+ rooted +
159
+ fit +
160
+ closeness * 2 +
161
+ Number(bond?.relationalWeight || 0) * 2 +
162
+ familiarity +
163
+ Math.min(5, talked) * 0.08 -
164
+ tension * 2 -
165
+ (contact.awaitingReply ? 0.35 : 0)
166
+ );
167
+ };
168
+ return candidates
169
+ .map((contact) => ({ contact, score: score(contact) }))
170
+ .sort(
171
+ (a, b) =>
172
+ b.score - a.score ||
173
+ a.contact.session.localeCompare(b.contact.session),
174
+ )[0]?.contact;
175
+ }
176
+ audience(thought, session, now = this.life.now()) {
177
+ const result = initiativeAudience(this.life.mind, thought, session, now);
178
+ return { ...result, origin: originSummary(result.origin) };
179
+ }
180
+ recentlyToldAnotherGroup(thought, now = this.life.now()) {
181
+ if (!isGroupSession(thought?.outreach_session)) return false;
182
+ const earlier = this.db
183
+ .prepare(
184
+ "SELECT id,outreach,sources,outreach_session FROM mind_thoughts WHERE outreach_status='sent' AND created>=? AND created<? ORDER BY created DESC LIMIT 40",
185
+ )
186
+ .all(now - 12 * HOUR, now);
187
+ return earlier.some(
188
+ (row) =>
189
+ row.id !== thought.id &&
190
+ isGroupSession(row.outreach_session) &&
191
+ row.outreach_session !== thought.outreach_session &&
192
+ (sameRecentTheme(row.outreach, thought.outreach) ||
193
+ (thought.sources || []).includes(`t:${row.id}`) ||
194
+ parse(row.sources, []).includes(`t:${thought.id}`)),
195
+ );
196
+ }
197
+ sourceMaterial(thought, now = this.life.now()) {
198
+ const self = this.life.mind.self.latest(now);
199
+ const material = [];
200
+ for (const ref of (thought.sources || []).slice(0, 10)) {
201
+ const id = ref.slice(2);
202
+ if (ref.startsWith("m:")) {
203
+ const row = this.db
204
+ .prepare(
205
+ "SELECT time,role,payload,session_id FROM core_events WHERE seq=?",
206
+ )
207
+ .get(Number(id));
208
+ if (
209
+ !row ||
210
+ (isPrivateSession(row.session_id) &&
211
+ row.session_id !== thought.outreach_session)
212
+ )
213
+ continue;
214
+ const message = parse(row.payload, {});
215
+ material.push({
216
+ ref,
217
+ kind: "past_message",
218
+ role: row.role,
219
+ name: message.name,
220
+ time: row.time,
221
+ sourceSession: row.session_id,
222
+ content: text(message.text, 300),
223
+ referenceOnly: true,
224
+ });
225
+ } else if (ref.startsWith("s:")) {
226
+ const thread = self.find((s) => s.thread === id);
227
+ if (
228
+ thread &&
229
+ this.life.mind.meetings.stays(thread, thought.outreach_session)
230
+ )
231
+ material.push({ ref, kind: "self", content: thread.content });
232
+ } else if (ref.startsWith("t:")) {
233
+ const note = this.life.mind.thoughts.get(id);
234
+ if (
235
+ note &&
236
+ this.life.mind.meetings.stays(
237
+ {
238
+ ...note,
239
+ session_id: note.sessions.find(isPrivateSession) || "",
240
+ },
241
+ thought.outreach_session,
242
+ )
243
+ )
244
+ material.push({
245
+ ref,
246
+ kind: "earlier_thought",
247
+ content: text(note.content, 300),
248
+ time: note.created,
249
+ certainty: "当时的想法,不自动成为外部事实",
250
+ });
251
+ } else if (ref.startsWith("a:")) {
252
+ const event = this.life.mind.anticipations.get(id);
253
+ if (
254
+ event &&
255
+ (!isPrivateSession(event.session_id) ||
256
+ event.session_id === thought.outreach_session) &&
257
+ (event.discretion === "open" ||
258
+ event.session_id === thought.outreach_session)
259
+ )
260
+ material.push({
261
+ ref,
262
+ kind: "anticipation",
263
+ content: event.content,
264
+ due: event.due_at,
265
+ status: event.status,
266
+ recordedAt: event.created,
267
+ });
268
+ } else if (ref.startsWith("g:")) {
269
+ const meeting = this.db
270
+ .prepare(
271
+ "SELECT created,session_id,appraisal,discretion FROM mind_meetings WHERE id=?",
272
+ )
273
+ .get(id);
274
+ if (
275
+ meeting &&
276
+ (meeting.discretion === "open" ||
277
+ meeting.session_id === thought.outreach_session) &&
278
+ (!isPrivateSession(meeting.session_id) ||
279
+ meeting.session_id === thought.outreach_session)
280
+ )
281
+ material.push({
282
+ ref,
283
+ kind: "past_understanding",
284
+ content: text(meeting.appraisal, 300),
285
+ time: meeting.created,
286
+ certainty: "当时对相遇的理解,不是新收到的消息",
287
+ });
288
+ } else if (ref.startsWith("r:")) {
289
+ const reading = this.db
290
+ .prepare(
291
+ "SELECT created,title,note,ordinal FROM mind_readings WHERE chunk_id=? AND created<=? ORDER BY created DESC LIMIT 1",
292
+ )
293
+ .get(id, now);
294
+ if (reading)
295
+ material.push({
296
+ ref,
297
+ kind: "past_reading",
298
+ title: reading.title,
299
+ note: text(reading.note, 300),
300
+ part: reading.ordinal + 1,
301
+ time: reading.created,
302
+ });
303
+ }
304
+ }
305
+ return material;
306
+ }
307
+ blocked(session, now, settings) {
308
+ if (!session || !this.life.chat.enabled(session, { simulated: false }))
309
+ return { reason: "会话不可用", permanent: true };
310
+ // A room that has switched proactive messages off is not a place she can
311
+ // knock on: she waits to be sought out, and looks again later.
312
+ if (!this.life.canReach(session))
313
+ return {
314
+ reason: "对方没有开启主动消息,等对方来找她",
315
+ retryAt: now + 6 * HOUR,
316
+ };
317
+ const lastUser = this.db
318
+ .prepare(
319
+ "SELECT time,payload FROM core_events WHERE session_id=? AND role='user' AND seq NOT IN (SELECT seq FROM mind_unlived) AND COALESCE(json_extract(payload,'$.simulated'),0)=0 ORDER BY seq DESC LIMIT 1",
320
+ )
321
+ .get(session);
322
+ const words = parse(lastUser?.payload, {}).text || "";
323
+ // Explicit contact boundaries are different from “this message needs no reply”.
324
+ if (
325
+ /(?:别|不要|不用)再?(?:主动)?(?:给我发消息|找我|联系我|私聊我)|别再打扰我/.test(
326
+ words,
327
+ )
328
+ )
329
+ return { reason: "对方明确不想被主动联系", permanent: true };
330
+ if (this.life.chat.queue.lanes.has(session))
331
+ return { reason: "这段对话正在处理,稍后重新看看", retryAt: now + 60000 };
332
+ const sent = this.lastSent(session);
333
+ const interval = settings.proactiveIntervalHours * HOUR;
334
+ if (sent && now - sent < interval)
335
+ return { reason: "刚主动说过,愿望仍保留", retryAt: sent + interval };
336
+ return null;
337
+ }
338
+ }