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,208 @@
1
+ import sharp from "sharp";
2
+
3
+ const MIB = 1024 * 1024;
4
+ const TYPES = {
5
+ jpeg: "image/jpeg",
6
+ png: "image/png",
7
+ webp: "image/webp",
8
+ gif: "image/gif",
9
+ heif: "image/avif",
10
+ };
11
+
12
+ function configured(value, fallback, min, max) {
13
+ const number = Number(value);
14
+ return value != null && value !== "" && Number.isFinite(number)
15
+ ? Math.min(max, Math.max(min, number))
16
+ : fallback;
17
+ }
18
+
19
+ export function visionImageLimits(env = process.env) {
20
+ return {
21
+ maxSourceBytes: Math.floor(
22
+ configured(env.VISION_SOURCE_MAX_MB, 32, 4, 64) * MIB,
23
+ ),
24
+ maxModelBytes: Math.floor(
25
+ configured(env.VISION_MODEL_MAX_MB, 4, 1, 4) * MIB,
26
+ ),
27
+ maxPixels: 80_000_000,
28
+ maxDimension: 8192,
29
+ maxOutputPixels: 16_000_000,
30
+ downloadTimeoutMs: Math.floor(
31
+ configured(env.VISION_DOWNLOAD_TIMEOUT_SECONDS, 30, 5, 120) * 1000,
32
+ ),
33
+ };
34
+ }
35
+
36
+ export function imageMime(bytes) {
37
+ if (
38
+ bytes.length >= 3 &&
39
+ bytes[0] === 0xff &&
40
+ bytes[1] === 0xd8 &&
41
+ bytes[2] === 0xff
42
+ )
43
+ return "image/jpeg";
44
+ if (
45
+ bytes.length >= 8 &&
46
+ bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
47
+ )
48
+ return "image/png";
49
+ if (
50
+ bytes.length >= 6 &&
51
+ /^(GIF87a|GIF89a)$/.test(bytes.toString("ascii", 0, 6))
52
+ )
53
+ return "image/gif";
54
+ if (
55
+ bytes.length >= 12 &&
56
+ bytes.toString("ascii", 0, 4) === "RIFF" &&
57
+ bytes.toString("ascii", 8, 12) === "WEBP"
58
+ )
59
+ return "image/webp";
60
+ if (bytes.length >= 16 && bytes.toString("ascii", 4, 8) === "ftyp") {
61
+ const length = bytes.readUInt32BE(0);
62
+ if (length >= 16 && length <= bytes.length) {
63
+ for (let offset = 8; offset < Math.min(length, 64); offset += 4) {
64
+ if (offset === 12) continue; // Minor version, not a compatible brand.
65
+ if (
66
+ ["avif", "avis"].includes(bytes.toString("ascii", offset, offset + 4))
67
+ )
68
+ return "image/avif";
69
+ }
70
+ }
71
+ }
72
+ return "";
73
+ }
74
+
75
+ // Bound work across all conversations, including downloads and decoded images.
76
+ let active = 0;
77
+ const waiting = [];
78
+ export async function withVisionImageSlot(run) {
79
+ if (active >= 2) await new Promise((resolve) => waiting.push(resolve));
80
+ else active++;
81
+ try {
82
+ return await run();
83
+ } finally {
84
+ const next = waiting.shift();
85
+ if (next) next();
86
+ else active--;
87
+ }
88
+ }
89
+
90
+ export async function prepareVisionImage(bytes, limits = visionImageLimits()) {
91
+ if (!bytes.length || bytes.length > limits.maxSourceBytes)
92
+ throw Error("图片超过大小限制");
93
+ const mime = imageMime(bytes);
94
+ if (!mime) throw Error("不是支持的图片格式");
95
+ const input = { limitInputPixels: limits.maxPixels, pages: 1 };
96
+ let metadata;
97
+ try {
98
+ metadata = await sharp(bytes, input).metadata();
99
+ } catch (error) {
100
+ if (/pixel limit/i.test(error.message)) throw Error("图片像素超过处理限制");
101
+ throw Error("图片读取失败");
102
+ }
103
+ if (
104
+ !TYPES[metadata.format] ||
105
+ (metadata.format === "heif" && metadata.compression !== "av1")
106
+ )
107
+ throw Error("不是支持的图片格式");
108
+ const oriented =
109
+ metadata.autoOrient ||
110
+ ([5, 6, 7, 8].includes(metadata.orientation)
111
+ ? { width: metadata.height, height: metadata.width }
112
+ : metadata);
113
+ const { width, height } = oriented;
114
+ if (!width || !height || width * height > limits.maxPixels)
115
+ throw Error("图片像素超过处理限制");
116
+ const scale = Math.min(
117
+ 1,
118
+ limits.maxDimension / width,
119
+ limits.maxDimension / height,
120
+ Math.sqrt(limits.maxOutputPixels / (width * height)),
121
+ );
122
+ let targetWidth = Math.max(1, Math.floor(width * scale));
123
+ let targetHeight = Math.max(1, Math.floor(height * scale));
124
+ const frameOnly = metadata.pages > 1;
125
+ if (
126
+ bytes.length <= limits.maxModelBytes &&
127
+ scale === 1 &&
128
+ !frameOnly &&
129
+ (!metadata.orientation || metadata.orientation === 1) &&
130
+ mime !== "image/avif" &&
131
+ mime !== "image/gif"
132
+ ) {
133
+ try {
134
+ // Decode before passing through: intact headers can hide a truncated
135
+ // image body. Discard the tiny probe and retain the original bytes.
136
+ await sharp(bytes, input)
137
+ .resize({ width: 1, height: 1, fit: "inside" })
138
+ .raw()
139
+ .timeout({ seconds: 10 })
140
+ .toBuffer();
141
+ } catch (error) {
142
+ if (/timeout|timed out/i.test(error.message)) throw Error("图片处理超时");
143
+ throw Error("图片读取失败");
144
+ }
145
+ return {
146
+ bytes,
147
+ mime,
148
+ width,
149
+ height,
150
+ originalBytes: bytes.length,
151
+ transformed: false,
152
+ frameOnly: false,
153
+ };
154
+ }
155
+
156
+ const pipeline = () =>
157
+ sharp(bytes, input)
158
+ .rotate()
159
+ .resize({
160
+ width: targetWidth,
161
+ height: targetHeight,
162
+ fit: "inside",
163
+ withoutEnlargement: true,
164
+ })
165
+ .timeout({ seconds: 10 });
166
+ const result = (output, format) => ({
167
+ bytes: output.data,
168
+ mime: format,
169
+ width: output.info.width,
170
+ height: output.info.height,
171
+ originalBytes: bytes.length,
172
+ transformed: true,
173
+ frameOnly,
174
+ });
175
+ try {
176
+ // Try lossless encoding first: large uncompressed screenshots often fit
177
+ // without shrinking text or introducing JPEG artefacts.
178
+ if (mime !== "image/jpeg") {
179
+ const png = await pipeline()
180
+ .png({ compressionLevel: 6 })
181
+ .toBuffer({ resolveWithObject: true });
182
+ if (png.data.length <= limits.maxModelBytes)
183
+ return result(png, "image/png");
184
+ }
185
+ for (let attempt = 0; attempt < 4; attempt++) {
186
+ const output = await pipeline()
187
+ .flatten({ background: "#ffffff" })
188
+ .jpeg({ quality: attempt === 0 ? 90 : 82, chromaSubsampling: "4:4:4" })
189
+ .toBuffer({ resolveWithObject: true });
190
+ if (output.data.length <= limits.maxModelBytes)
191
+ return result(output, "image/jpeg");
192
+ // First reduce encoding size while keeping the full dimensions. Only
193
+ // shrink if that still cannot fit the model's request budget.
194
+ if (attempt > 0) {
195
+ const shrink = Math.min(
196
+ 0.85,
197
+ Math.sqrt(limits.maxModelBytes / output.data.length) * 0.9,
198
+ );
199
+ targetWidth = Math.max(1, Math.floor(targetWidth * shrink));
200
+ targetHeight = Math.max(1, Math.floor(targetHeight * shrink));
201
+ }
202
+ }
203
+ } catch (error) {
204
+ if (/timeout|timed out/i.test(error.message)) throw Error("图片处理超时");
205
+ throw Error("图片读取失败");
206
+ }
207
+ throw Error("图片超过大小限制");
208
+ }
@@ -0,0 +1,66 @@
1
+ import { localClock } from "./conversation-cues.js";
2
+
3
+ // History has a different role when she starts a conversation. It can stop
4
+ // a repeated question or a false claim, but it must never pose a new request.
5
+ export function initiativeContext(snapshot, occasion = snapshot.initiative) {
6
+ const situation = occasion?.initiative || {};
7
+ const history = (snapshot.messages || []).slice(-12).map((m) => ({
8
+ id: m.id,
9
+ speaker: m.speaker,
10
+ name: m.name,
11
+ role: m.role,
12
+ text: m.text,
13
+ time: m.time,
14
+ localTime: m.localTime,
15
+ referenceOnly: true,
16
+ }));
17
+ return {
18
+ sessionId: snapshot.sessionId,
19
+ conversation: snapshot.conversation,
20
+ batchIds: [],
21
+ addressed: [],
22
+ newMessages: [],
23
+ messages: [],
24
+ initiative: {
25
+ type: occasion?.type,
26
+ quietMinutes: situation.quietMinutes,
27
+ sinceUserMinutes: situation.sinceUserMinutes,
28
+ awaitingReply: !!situation.awaitingReply,
29
+ lastInitiatedAt: situation.lastInitiatedAt,
30
+ returned: occasion?.returned || [],
31
+ },
32
+ expression: occasion?.expression || {
33
+ origin: "earlier_wish",
34
+ thought: occasion?.thought,
35
+ words: occasion?.planned ? [occasion.planned] : [],
36
+ reason: occasion?.wantedBecause,
37
+ },
38
+ ...(snapshot.self ? { self: snapshot.self } : {}),
39
+ ...(snapshot.inner ? { inner: snapshot.inner } : {}),
40
+ history: {
41
+ use: "只核对过去、避免重复和捏造;这里没有本轮收到的消息,不是待回复的问题",
42
+ messages: history,
43
+ },
44
+ };
45
+ }
46
+ export function initiativeSnapshot(session, rows, nature, now, timeZone) {
47
+ return {
48
+ sessionId: session,
49
+ batchIds: [],
50
+ batch: [],
51
+ persona: nature,
52
+ sourceRows: [],
53
+ messages: rows.slice(-24).map((m) => ({
54
+ id: m.seq,
55
+ speaker: m.userId,
56
+ name: m.name,
57
+ role: m.role,
58
+ text: m.text,
59
+ time: m.time,
60
+ localTime: localClock(m.time, timeZone).local,
61
+ attachments: [],
62
+ referenceOnly: true,
63
+ })),
64
+ conversation: { clock: localClock(now, timeZone) },
65
+ };
66
+ }
@@ -0,0 +1,32 @@
1
+ // Trim only optional historical messages, keeping current targets and quote chains intact.
2
+ export function fitInput(data, build, estimate, limit) {
3
+ const payload = structuredClone(data);
4
+ const context = payload.context || payload;
5
+ const protectedIds = new Set([
6
+ ...(context.batchIds || []),
7
+ ...(payload.decision?.targetMessageIds || []),
8
+ ...(payload.decision?.evidenceIds || []),
9
+ ]);
10
+ for (const m of context.messages || [])
11
+ if (protectedIds.has(m.id))
12
+ for (const id of m.replyChain || []) protectedIds.add(id);
13
+ // A standalone memory/vision batch without batchIds must not be silently truncated.
14
+ const canTrim = Array.isArray(context.batchIds);
15
+ let messages = build(payload),
16
+ tokens = estimate(messages),
17
+ removed = 0;
18
+ while (tokens > limit && canTrim) {
19
+ const index =
20
+ context.messages?.findIndex((m) => !protectedIds.has(m.id)) ?? -1;
21
+ if (index < 0) break;
22
+ context.messages.splice(index, 1);
23
+ removed++;
24
+ messages = build(payload);
25
+ tokens = estimate(messages);
26
+ }
27
+ if (tokens > limit)
28
+ throw Error(
29
+ "当前批次、引用与人格超过模型输入预算,请增加 Max Input Tokens",
30
+ );
31
+ return { messages, inputEstimate: tokens, removed };
32
+ }
@@ -0,0 +1,98 @@
1
+ // Offline sample replies for simulation mode without an API key. Rule based
2
+ // on purpose: it never pretends to be the model or to know her mind.
3
+ const serious = (text) =>
4
+ /去世|过世|葬礼|重病|真的.{0,4}(?:难过|崩溃)|最近.{0,8}(?:难过|孤独)|没人.{0,4}(?:在乎|理我)|抑郁|撑不下去/.test(
5
+ text,
6
+ );
7
+ const danger = (text) =>
8
+ /想自杀|打算自杀|准备自杀|想死|不想活|伤害自己|结束生命/.test(text) &&
9
+ !/笑死|社死|想死你/.test(text);
10
+
11
+ export function sceneOf(text) {
12
+ if (/你.{0,5}(?:真人|机器人|AI|ai)|你是人吗|你到底是谁/.test(text))
13
+ return "identity";
14
+ if (danger(text)) return "danger";
15
+ if (/去世|过世|葬礼/.test(text)) return "grief";
16
+ if (serious(text)) return "sad";
17
+ if (
18
+ /别(?:说|回|给建议)|不用回|不想聊|让我.{0,3}(?:骂|吐槽)|只想吐槽/.test(text)
19
+ )
20
+ return /别回|不用回|不想聊/.test(text) ? "stop" : "vent";
21
+ if (/晚安|睡了|去睡|去洗澡|先撤|晚点聊|不聊了/.test(text)) return "bye";
22
+ if (
23
+ /怎么办|怎么.{0,5}(?:办|准备|选)|建议|该.{0,4}(?:做|准备)|先准备什么/.test(
24
+ text,
25
+ )
26
+ )
27
+ return "advice";
28
+ if (
29
+ /offer|录取|上岸|面试过|过了.{0,3}(?:面试|考试)|考过|拿奖|涨工资/.test(text)
30
+ )
31
+ return "win";
32
+ if (/加班|老板|下班|需求|打工/.test(text)) return "work";
33
+ if (/考试|复习|挂科|早八|作业|论文/.test(text)) return "study";
34
+ if (/排位|连跪|队友|挂机|晋级赛/.test(text)) return "game";
35
+ if (/奶茶|外卖|火锅|好吃|吃到|吃了/.test(text)) return "food";
36
+ if (/难过|想哭|崩溃|孤独|委屈/.test(text)) return "sad";
37
+ if (/烦|累|吐槽/.test(text)) return "vent";
38
+ if (/哈哈|笑死|hh|绷不住|离谱/.test(text)) return "joke";
39
+ if (/^(?:嗯+|哦+|好[的吧]?|行|ok|收到|谢谢)[。!!\s]*$/i.test(text))
40
+ return "ack";
41
+ if (/^\[(?:叫了你一声|回复你)\]$/.test(text) || text.trim() === "@我")
42
+ return "called";
43
+ if (/\[表情\]/.test(text)) return "sticker";
44
+ if (/\[图片\]/.test(text)) return "image";
45
+ return "share";
46
+ }
47
+
48
+ const LINES = {
49
+ identity: ["不是啦,我是 AI,可以一起唠", "AI 群友,真人那部分就不冒充了"],
50
+ danger: [
51
+ "你现在身边有人吗?先找个能陪着你的人,别一个人扛",
52
+ "先离可能伤到自己的东西远一点,联系身边的人陪着你",
53
+ ],
54
+ grief: ["这一下真的很难缓过来", "想说的时候再说,不急"],
55
+ sad: ["唉,这会儿确实不好受", "先不用急着把自己哄好"],
56
+ vent: ["今天这破事是有点多", "这事确实够烦的"],
57
+ bye: ["去吧", "好,晚点聊", "嗯,回头聊"],
58
+ win: ["可以啊!!", "这不得好好开心一下", "好消息,终于轮到你了"],
59
+ advice: ["先挑最要紧的一件,别一下全压过来", "先把能确定的那一步做了"],
60
+ work: ["偏偏挑快下班的时候", "怎么又是这个点来活"],
61
+ study: ["这复习没对上题", "这题是一点面子不给"],
62
+ game: ["连跪还碰上挂机,够烦的", "队友走了,血压还在"],
63
+ food: ["有点馋了", "这顿听着就很满足"],
64
+ joke: ["hh 这也行", "?怎么还有这一出", "离谱但好笑"],
65
+ ack: ["嗯嗯", "好", "行"],
66
+ image: ["收到", "嗯嗯"],
67
+ sticker: ["哈哈", "懂了"],
68
+ called: ["咋啦", "在,怎么了", "嗯?"],
69
+ share: ["原来是这么回事", "这事还挺有意思"],
70
+ };
71
+
72
+ export function demoReply(settings, input) {
73
+ const { message, context = [] } = input;
74
+ const said = String(message?.text || "");
75
+ const scene = sceneOf(said);
76
+ if (
77
+ scene === "stop" ||
78
+ (scene === "ack" && message?.kind === "group" && !input.direct)
79
+ )
80
+ return { speak: false, reason: "模拟:对方收口,留白" };
81
+ const recent = new Set(
82
+ context
83
+ .filter((m) => m.role === "assistant")
84
+ .slice(-4)
85
+ .map((m) => m.text),
86
+ );
87
+ const options = LINES[scene].filter((line) => !recent.has(line));
88
+ if (!options.length) return { speak: false, reason: "模拟:避免复读" };
89
+ const seed = [...said].reduce(
90
+ (n, ch) => (n * 31 + ch.codePointAt(0)) >>> 0,
91
+ 7,
92
+ );
93
+ return {
94
+ speak: true,
95
+ reply: options[seed % options.length],
96
+ reason: `模拟样例 · ${scene}`,
97
+ };
98
+ }
@@ -0,0 +1,118 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { bindSessionId, sessionNativeId } from "../channels/session-key.js";
3
+ import { segmentFacts } from "../channels/segments.js";
4
+ // The neutral message every channel hands to the core. An adapter fills
5
+ // platformId, accountId, time, mentions, replyId and attachments itself; the
6
+ // segment parsing is only the fallback for events saved before it did.
7
+ export function messageEnvelope(m) {
8
+ const raw = m.raw || {},
9
+ segments = m.segments || (Array.isArray(raw.message) ? raw.message : []),
10
+ facts = segmentFacts(segments),
11
+ given = (value) => (Array.isArray(value) && value.length ? value : null);
12
+ return {
13
+ ...m,
14
+ role: m.role || "user",
15
+ platformId: String(m.platformId || raw.message_id || ""),
16
+ accountId: String(m.accountId || raw.self_id || ""),
17
+ time:
18
+ Number.isFinite(m.time) && m.time > 0
19
+ ? m.time
20
+ : raw.time
21
+ ? raw.time * 1000
22
+ : Date.now(),
23
+ receivedAt: Date.now(),
24
+ mentions: given(m.mentions)?.map(String) ?? facts.mentions,
25
+ replyId: String(m.replyId || facts.replyId || ""),
26
+ attachments: given(m.attachments) ?? facts.attachments,
27
+ segments,
28
+ raw: undefined,
29
+ };
30
+ }
31
+ export function persistIncoming(repo, m) {
32
+ let sessionId = m.sessionId;
33
+ try {
34
+ sessionId = bindSessionId(repo.db, m.sessionId);
35
+ } catch {
36
+ sessionId = m.sessionId;
37
+ }
38
+ const env = messageEnvelope({ ...m, sessionId });
39
+ const seq = repo.append(env);
40
+ if (!seq) return null;
41
+ const native = String(m.nativeId || sessionNativeId(sessionId) || "");
42
+ const groupName = String(m.raw?.group_name || "").trim();
43
+ const personName = String(m.name || "").trim();
44
+ const readable =
45
+ m.kind === "group"
46
+ ? groupName && groupName !== native && !/^\d{4,20}$/.test(groupName)
47
+ ? groupName.slice(0, 100)
48
+ : ""
49
+ : personName && personName !== native && !/^\d{4,20}$/.test(personName)
50
+ ? personName.slice(0, 100)
51
+ : "";
52
+ const placeholder =
53
+ m.kind === "group" ? "未命名的群" : personName || "未命名的人";
54
+ repo.db
55
+ .prepare(
56
+ `INSERT INTO sessions(id,name,kind) VALUES (?,?,?)
57
+ ON CONFLICT(id) DO UPDATE SET name=excluded.name
58
+ WHERE excluded.name NOT IN ('未命名的群','未命名的人')
59
+ AND (
60
+ sessions.name LIKE '群聊 %'
61
+ OR sessions.name GLOB '[0-9]*'
62
+ OR sessions.name IN ('未命名的群','未命名的人','')
63
+ )`,
64
+ )
65
+ .run(sessionId, readable || placeholder, m.kind);
66
+ repo.db
67
+ .prepare(
68
+ "INSERT OR IGNORE INTO messages(event_id,session_id,user_id,name,text,time,role,is_demo) VALUES (?,?,?,?,?,?,?,?)",
69
+ )
70
+ .run(
71
+ m.eventId,
72
+ sessionId,
73
+ m.userId,
74
+ m.name,
75
+ m.text,
76
+ env.time,
77
+ "user",
78
+ Number(!!m.simulated),
79
+ );
80
+ return { ...env, seq, sessionId };
81
+ }
82
+ export function persistReply(repo, m, text, platformId, time = Date.now()) {
83
+ const msg = {
84
+ sessionId: m.sessionId,
85
+ kind: m.kind,
86
+ userId: "bot",
87
+ name: repo.store.settings().name,
88
+ text,
89
+ role: "assistant",
90
+ eventId: randomUUID(),
91
+ platformId: String(platformId || ""),
92
+ accountId: m.accountId,
93
+ time,
94
+ mentions: [],
95
+ attachments: [],
96
+ simulated: !!m.simulated,
97
+ ...(m.artifact ? { artifact: m.artifact } : {}),
98
+ ...(m.traceId ? { traceId: m.traceId } : {}),
99
+ ...(Array.isArray(m.replyTargetIds)
100
+ ? { replyTargetIds: m.replyTargetIds }
101
+ : {}),
102
+ };
103
+ repo.append(msg);
104
+ repo.db
105
+ .prepare(
106
+ "INSERT INTO messages(event_id,session_id,user_id,name,text,time,role,is_demo) VALUES (?,?,?,?,?,?,?,?)",
107
+ )
108
+ .run(
109
+ msg.eventId,
110
+ msg.sessionId,
111
+ "bot",
112
+ msg.name,
113
+ text,
114
+ msg.time,
115
+ "assistant",
116
+ Number(!!m.simulated),
117
+ );
118
+ }
@@ -0,0 +1,78 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { persistReply } from "./message-manager.js";
3
+ export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
4
+ export async function deliver(
5
+ repo,
6
+ message,
7
+ bubbles,
8
+ trace,
9
+ send,
10
+ isCurrent,
11
+ {
12
+ wait = sleep,
13
+ random = Math.random,
14
+ now = Date.now,
15
+ prepareBubble = (line) => line,
16
+ } = {},
17
+ ) {
18
+ const ids = bubbles.map((text, i) => {
19
+ const id = randomUUID();
20
+ repo.db
21
+ .prepare(
22
+ "INSERT INTO core_outbox(id,trace_id,session_id,position,text,status,time) VALUES (?,?,?,?,?,?,?)",
23
+ )
24
+ .run(id, trace.id, message.sessionId, i, text, "pending", now());
25
+ return id;
26
+ });
27
+ const sent = [];
28
+ trace.sent = sent;
29
+ try {
30
+ for (let i = 0; i < bubbles.length; i++) {
31
+ // Typing pace between bubbles; physical, not a decision.
32
+ if (i)
33
+ await wait(
34
+ Math.min(1200, 300 + bubbles[i].length * 15 + random() * 350),
35
+ );
36
+ if (!isCurrent()) {
37
+ trace.steps.push("发送前配置或语境变化,取消剩余气泡");
38
+ break;
39
+ }
40
+ const outgoing = prepareBubble(bubbles[i]);
41
+ repo.db
42
+ .prepare("UPDATE core_outbox SET status='sending',text=? WHERE id=?")
43
+ .run(outgoing, ids[i]);
44
+ try {
45
+ const result = await send(message, outgoing);
46
+ repo.db
47
+ .prepare(
48
+ "UPDATE core_outbox SET status='confirmed',platform_id=? WHERE id=?",
49
+ )
50
+ .run(String(result?.message_id || ""), ids[i]);
51
+ persistReply(
52
+ repo,
53
+ {
54
+ ...message,
55
+ traceId: trace.id,
56
+ replyTargetIds: trace.decision?.targetMessageIds || [],
57
+ },
58
+ outgoing,
59
+ result?.message_id,
60
+ now(),
61
+ );
62
+ sent.push(outgoing);
63
+ } catch (e) {
64
+ repo.db
65
+ .prepare("UPDATE core_outbox SET status='uncertain' WHERE id=?")
66
+ .run(ids[i]);
67
+ throw e;
68
+ }
69
+ }
70
+ } finally {
71
+ repo.db
72
+ .prepare(
73
+ "UPDATE core_outbox SET status='cancelled' WHERE trace_id=? AND status='pending'",
74
+ )
75
+ .run(trace.id);
76
+ }
77
+ return sent;
78
+ }