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,165 @@
1
+ import { parseSessionKey } from "./session-key.js";
2
+
3
+ export const CHANNEL_TYPES = ["onebot", "qqbot"];
4
+
5
+ // Which channel is wanted: the environment wins over the saved setting, the
6
+ // same way LLM_API_KEY wins over a key saved in the studio.
7
+ export function selectedChannel(store, env = process.env) {
8
+ const forced = String(env.LUCKYTRI_CHANNEL || "")
9
+ .trim()
10
+ .toLowerCase();
11
+ if (CHANNEL_TYPES.includes(forced)) return { type: forced, locked: true };
12
+ const saved = String(store.settings().channel || "");
13
+ return {
14
+ type: CHANNEL_TYPES.includes(saved) ? saved : "onebot",
15
+ locked: false,
16
+ };
17
+ }
18
+
19
+ // Exactly one channel is connected at a time. Everything above the hub talks
20
+ // to it as if it were the channel itself, so the core, her mind and her life
21
+ // are the same whichever platform she is on; switching is a setting, not a
22
+ // restart.
23
+ const QQ = new Set(CHANNEL_TYPES);
24
+
25
+ export function createChannelHub(store, channels, { env = process.env } = {}) {
26
+ const registry = new Map(channels.map((channel) => [channel.type, channel]));
27
+ if (!registry.size) throw new Error("channel hub needs at least one channel");
28
+ let active = null;
29
+ let locked = false;
30
+ let attached = null;
31
+ // Plugin channels stay connected beside the one QQ channel in use.
32
+ const started = new Set();
33
+
34
+ const current = () => registry.get(active) || registry.values().next().value;
35
+
36
+ function resolve(sessionId) {
37
+ let type = active;
38
+ if (sessionId) {
39
+ try {
40
+ type = parseSessionKey(sessionId).channel;
41
+ } catch {
42
+ /* A session without a channel key is spoken to over the active one. */
43
+ }
44
+ }
45
+ if (QQ.has(type) && type !== active)
46
+ throw new Error("这个会话属于另一种 QQ 接入方式,当前不能向它发送");
47
+ const channel = registry.get(type);
48
+ if (!channel || (!QQ.has(type) && !started.has(type)))
49
+ throw new Error("这个会话的通道没有接上");
50
+ return channel;
51
+ }
52
+
53
+ // Bring the connected channel in line with the settings. Cheap when nothing
54
+ // changed, so it is also what runs after the credentials are edited.
55
+ function sync() {
56
+ const wanted = selectedChannel(store, env);
57
+ const type = registry.has(wanted.type)
58
+ ? wanted.type
59
+ : registry.keys().next().value;
60
+ locked = wanted.locked;
61
+ if (active && active !== type) registry.get(active).stop();
62
+ const switched = active !== type;
63
+ active = type;
64
+ registry.get(type).start();
65
+ if (switched)
66
+ registry
67
+ .get(type)
68
+ .refreshDirectory()
69
+ .catch(() => {});
70
+ return type;
71
+ }
72
+
73
+ function attach(server, chat) {
74
+ attached = { server, chat };
75
+ for (const channel of registry.values()) channel.attach(server, chat);
76
+ for (const type of started) registry.get(type)?.start();
77
+ sync();
78
+ }
79
+
80
+ function register(channel) {
81
+ if (!channel?.type || QQ.has(channel.type) || registry.has(channel.type))
82
+ throw new Error("这个通道不能接上");
83
+ registry.set(channel.type, channel);
84
+ started.add(channel.type);
85
+ if (attached) {
86
+ channel.attach(attached.server, attached.chat);
87
+ channel.start();
88
+ }
89
+ return channel.type;
90
+ }
91
+
92
+ function unregister(type) {
93
+ if (QQ.has(type) || !registry.has(type)) return false;
94
+ started.delete(type);
95
+ const channel = registry.get(type);
96
+ try {
97
+ channel.stop();
98
+ channel.close();
99
+ } catch {
100
+ /* A plugin channel that already died should still leave the hub. */
101
+ }
102
+ registry.delete(type);
103
+ return true;
104
+ }
105
+
106
+ const send = async (message, text) => {
107
+ if (message?.simulated) return { message_id: "sim" };
108
+ return resolve(message.sessionId).send(message, text);
109
+ };
110
+
111
+ function canReach(sessionId) {
112
+ try {
113
+ return resolve(sessionId).canReach(sessionId);
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ function status() {
120
+ const channel = current();
121
+ const own = channel.status();
122
+ const all = {};
123
+ for (const item of registry.values()) all[item.type] = item.status();
124
+ return {
125
+ channel: channel.type,
126
+ locked,
127
+ online: !!own.online,
128
+ connectedAt: own.connectedAt ?? null,
129
+ lastEventAt: own.lastEventAt ?? null,
130
+ lastDisconnectAt: own.lastDisconnectAt ?? null,
131
+ adminProtected: !!env.ADMIN_TOKEN,
132
+ port: Number(env.PORT || 3210),
133
+ capabilities: channel.capabilities,
134
+ ...all,
135
+ };
136
+ }
137
+
138
+ return {
139
+ attach,
140
+ sync,
141
+ register,
142
+ unregister,
143
+ send,
144
+ canReach,
145
+ status,
146
+ type: () => current().type,
147
+ online: () =>
148
+ !!current().status().online ||
149
+ [...started].some((type) => registry.get(type)?.status().online),
150
+ fetchQuoted: (message) =>
151
+ (message?.sessionId ? resolve(message.sessionId) : current()).fetchQuoted(
152
+ message,
153
+ ),
154
+ fetchImage: (file, sessionId) =>
155
+ (sessionId ? resolve(sessionId) : current()).fetchImage(file),
156
+ fetchMedia: (file, sessionId, type) => {
157
+ const channel = sessionId ? resolve(sessionId) : current();
158
+ return channel.fetchMedia ? channel.fetchMedia(file, type) : null;
159
+ },
160
+ refreshDirectory: () => current().refreshDirectory(),
161
+ close() {
162
+ for (const channel of registry.values()) channel.close();
163
+ },
164
+ };
165
+ }
@@ -0,0 +1,25 @@
1
+ import { createOneBotChannel } from "./onebot/index.js";
2
+ import { createQqBotChannel } from "./qqbot/index.js";
3
+
4
+ // Adding a platform: write a folder that fulfils channels/contract.js, give its
5
+ // adapter an entry in adapters.js (so the core can ask what its media links
6
+ // look like) and its factory a line in createChannels. Nothing above the hub
7
+ // changes.
8
+ export function createChannels(store, options = {}) {
9
+ return [createOneBotChannel(store), createQqBotChannel(store, options.qqbot)];
10
+ }
11
+
12
+ export { createChannelHub, selectedChannel, CHANNEL_TYPES } from "./hub.js";
13
+ export { defineChannel } from "./contract.js";
14
+ export { adapterFor, registerChannel, onebot, qqbot } from "./adapters.js";
15
+ export {
16
+ bindSessionId,
17
+ formatSessionKey,
18
+ isGroupSession,
19
+ parseSessionKey,
20
+ publicSession,
21
+ sessionAliases,
22
+ sessionKind,
23
+ sessionNativeId,
24
+ } from "./session-key.js";
25
+ export { normalize } from "./onebot/adapter.js";
@@ -0,0 +1,288 @@
1
+ import { formatSessionKey, parseSessionKey } from "../session-key.js";
2
+ import { segmentFacts } from "../segments.js";
3
+
4
+ const CQ_ESCAPES = {
5
+ ",": ",",
6
+ "[": "[",
7
+ "]": "]",
8
+ "&": "&",
9
+ };
10
+ const decodeCq = (value) =>
11
+ String(value).replace(
12
+ /,|[|]|&/g,
13
+ (token) => CQ_ESCAPES[token],
14
+ );
15
+
16
+ function parseCqMessage(raw) {
17
+ const source = String(raw || ""),
18
+ segments = [],
19
+ pattern = /\[CQ:([^,\]]+)((?:,[^\]]*)?)\]/g;
20
+ let cursor = 0,
21
+ match;
22
+ while ((match = pattern.exec(source))) {
23
+ if (match.index > cursor)
24
+ segments.push({
25
+ type: "text",
26
+ data: { text: source.slice(cursor, match.index) },
27
+ });
28
+ const data = {};
29
+ for (const item of match[2].replace(/^,/, "").split(",")) {
30
+ if (!item) continue;
31
+ const [key, ...parts] = item.split("=");
32
+ if (key) data[key] = decodeCq(parts.join("="));
33
+ }
34
+ segments.push({ type: match[1], data });
35
+ cursor = pattern.lastIndex;
36
+ }
37
+ if (cursor < source.length)
38
+ segments.push({ type: "text", data: { text: source.slice(cursor) } });
39
+ return segments.length
40
+ ? segments
41
+ : [{ type: "text", data: { text: source } }];
42
+ }
43
+
44
+ const stickerTypes = new Set([
45
+ "face",
46
+ "mface",
47
+ "market_face",
48
+ "bface",
49
+ "sface",
50
+ "sticker",
51
+ "dice",
52
+ "rps",
53
+ "poke",
54
+ ]);
55
+ export function senderLabel(sender, userId) {
56
+ const id = String(userId ?? "");
57
+ const values = [sender?.card, sender?.nickname, sender?.nick]
58
+ .map((value) => String(value || "").trim())
59
+ .filter(Boolean);
60
+ const readable = values.find(
61
+ (value) => value !== id && !/^\d{4,20}$/.test(value),
62
+ );
63
+ return (readable || values.find((value) => value !== id) || id).slice(0, 100);
64
+ }
65
+
66
+ export const isSticker = (segment) =>
67
+ stickerTypes.has(String(segment?.type || "").toLowerCase()) ||
68
+ [segment?.data?.sub_type, segment?.data?.type]
69
+ .map((value) => String(value || "").toLowerCase())
70
+ .includes("sticker") ||
71
+ (String(segment?.type || "").toLowerCase() === "image" &&
72
+ Boolean(
73
+ segment?.data?.emoji_id ||
74
+ segment?.data?.emoji_package_id ||
75
+ (segment?.data?.summary && segment?.data?.key),
76
+ ));
77
+
78
+ function replyTarget(event, safeSegments, botMessageIds) {
79
+ const replies = safeSegments.filter((segment) => segment.type === "reply"),
80
+ ids = replies
81
+ .map((segment) => segment.data.id)
82
+ .filter((id) => id != null)
83
+ .map(String),
84
+ senderIds = [
85
+ event.reply?.user_id,
86
+ event.reply?.sender?.user_id,
87
+ event.reply?.sender?.id,
88
+ ...replies.map((segment) => segment.data.qq),
89
+ ...replies.map((segment) => segment.data.user_id),
90
+ ...replies.map((segment) => segment.data.sender?.user_id),
91
+ ...replies.map((segment) => segment.data.sender?.id),
92
+ ]
93
+ .filter((id) => id != null)
94
+ .map(String),
95
+ known =
96
+ typeof botMessageIds?.has === "function" &&
97
+ ids.some((id) => botMessageIds.has(id));
98
+ return known || senderIds.includes(String(event.self_id));
99
+ }
100
+
101
+ export const onebot = {
102
+ type: "onebot",
103
+ capabilities: {
104
+ mention: true,
105
+ quote: true,
106
+ image: true,
107
+ thread: false,
108
+ sticker: true,
109
+ },
110
+ mediaHosts: /(^|\.)(qpic\.cn|gtimg\.cn|qq\.com\.cn|qq\.com|myqcloud\.com)$/i,
111
+ usableMediaUrl(url) {
112
+ try {
113
+ const u = new URL(url);
114
+ return (
115
+ u.protocol === "https:" &&
116
+ !u.username &&
117
+ !u.password &&
118
+ this.mediaHosts.test(u.hostname)
119
+ );
120
+ } catch {
121
+ return false;
122
+ }
123
+ },
124
+ sendAction(message) {
125
+ return parseSessionKey(message.sessionId).kind === "group"
126
+ ? "send_group_msg"
127
+ : "send_private_msg";
128
+ },
129
+ sendParams(message, text) {
130
+ const parsed = parseSessionKey(message.sessionId);
131
+ return {
132
+ message: [{ type: "text", data: { text } }],
133
+ [parsed.kind === "group" ? "group_id" : "user_id"]:
134
+ parsed.kind === "group"
135
+ ? Number(message.raw?.group_id ?? parsed.nativeId)
136
+ : Number(message.raw?.user_id ?? message.userId ?? parsed.nativeId),
137
+ };
138
+ },
139
+ nativeChatId(sessionId) {
140
+ return Number(parseSessionKey(sessionId).nativeId);
141
+ },
142
+ quotedMessage(data, current) {
143
+ if (!data || typeof data !== "object" || !Array.isArray(data.message))
144
+ return null;
145
+ try {
146
+ const parsed = parseSessionKey(current.sessionId);
147
+ if (data.group_id && String(data.group_id) !== parsed.nativeId)
148
+ return null;
149
+ } catch {
150
+ return null;
151
+ }
152
+ const sender = String(data.sender?.user_id || data.user_id || "unknown");
153
+ return {
154
+ sessionId: current.sessionId,
155
+ kind: current.kind,
156
+ userId: sender === current.accountId ? "bot" : sender,
157
+ name: senderLabel(data.sender, sender),
158
+ text: data.message
159
+ .filter((s) => s.type === "text")
160
+ .map((s) => s.data?.text || "")
161
+ .join(""),
162
+ role: sender === current.accountId ? "assistant" : "user",
163
+ raw: {
164
+ ...data,
165
+ self_id: current.accountId,
166
+ message_id: current.replyId,
167
+ },
168
+ };
169
+ },
170
+ };
171
+
172
+ export function normalize(event, { botMessageIds } = {}) {
173
+ if (
174
+ !event ||
175
+ typeof event !== "object" ||
176
+ event.post_type !== "message" ||
177
+ !["group", "private"].includes(event.message_type) ||
178
+ String(event.user_id) === String(event.self_id)
179
+ )
180
+ return null;
181
+ if (
182
+ !/^\d+$/.test(String(event.user_id)) ||
183
+ !/^\d+$/.test(String(event.self_id)) ||
184
+ event.message_id == null ||
185
+ (event.message_type === "group" && !/^\d+$/.test(String(event.group_id)))
186
+ )
187
+ return null;
188
+ const segments = Array.isArray(event.message)
189
+ ? event.message
190
+ : parseCqMessage(event.raw_message || event.message || "");
191
+ const safeSegments = segments.filter(
192
+ (s) => s && typeof s === "object" && s.data && typeof s.data === "object",
193
+ );
194
+ const atId = (segment) => {
195
+ const data = segment?.data || {};
196
+ const id = data.qq ?? data.user_id;
197
+ return id == null || id === "" ? "" : String(id);
198
+ };
199
+ const atSelf = safeSegments.some(
200
+ (s) =>
201
+ String(s.type || "").toLowerCase() === "at" &&
202
+ atId(s) === String(event.self_id),
203
+ ),
204
+ replyToBot = replyTarget(event, safeSegments, botMessageIds),
205
+ media = {
206
+ images: safeSegments.filter(
207
+ (s) => String(s.type).toLowerCase() === "image" && !isSticker(s),
208
+ ).length,
209
+ stickers: safeSegments.filter(isSticker).length,
210
+ other: safeSegments.filter((s) =>
211
+ ["record", "video", "file", "json", "xml"].includes(
212
+ String(s.type).toLowerCase(),
213
+ ),
214
+ ).length,
215
+ },
216
+ textParts = safeSegments.map((s) => {
217
+ const type = String(s.type || "").toLowerCase();
218
+ if (type === "text" && typeof s.data.text === "string")
219
+ return s.data.text;
220
+ if (type === "at")
221
+ return atId(s) === String(event.self_id) ? "@我" : "[提及成员]";
222
+ if (type === "image" && !isSticker(s)) return "[图片]";
223
+ if (isSticker(s)) return "[表情]";
224
+ if (["record", "video", "file", "json", "xml"].includes(type))
225
+ return "[媒体]";
226
+ return "";
227
+ }),
228
+ plainText = safeSegments
229
+ .filter((s) => s.type === "text" && typeof s.data.text === "string")
230
+ .map((s) => s.data.text)
231
+ .join("")
232
+ .trim(),
233
+ visibleText = textParts.join("").trim(),
234
+ text =
235
+ visibleText ||
236
+ (atSelf || replyToBot ? (replyToBot ? "[回复你]" : "[叫了你一声]") : "");
237
+ if (!text) return null;
238
+ const mediaCount = media.images + media.stickers + media.other;
239
+ const kind = event.message_type;
240
+ const nativeId = String(event.group_id || event.user_id);
241
+ const facts = segmentFacts(safeSegments);
242
+ return {
243
+ sessionId: formatSessionKey({
244
+ channel: "onebot",
245
+ accountId: String(event.self_id),
246
+ kind,
247
+ nativeId,
248
+ }),
249
+ channel: "onebot",
250
+ accountId: String(event.self_id),
251
+ nativeId,
252
+ segments: safeSegments,
253
+ kind,
254
+ userId: String(event.user_id),
255
+ name: senderLabel(event.sender, event.user_id),
256
+ accountName: senderLabel(
257
+ { nickname: event.sender?.nickname, nick: event.sender?.nick },
258
+ event.user_id,
259
+ ),
260
+ text: text.slice(0, 4000),
261
+ mentioned: atSelf || replyToBot,
262
+ replyToBot,
263
+ platformId: String(event.message_id),
264
+ time: event.time ? event.time * 1000 : Date.now(),
265
+ mentions: facts.mentions,
266
+ replyId: facts.replyId,
267
+ attachments: facts.attachments,
268
+ media: {
269
+ ...media,
270
+ count: mediaCount,
271
+ only: mediaCount > 0 && !plainText && !atSelf && !replyToBot,
272
+ imageOnly:
273
+ media.images > 0 &&
274
+ mediaCount === media.images &&
275
+ !plainText &&
276
+ !atSelf &&
277
+ !replyToBot,
278
+ stickerOnly:
279
+ media.stickers > 0 &&
280
+ mediaCount === media.stickers &&
281
+ !plainText &&
282
+ !atSelf &&
283
+ !replyToBot,
284
+ },
285
+ eventId: `${event.self_id}:${event.message_type}:${event.group_id || event.user_id}:${event.message_id}`,
286
+ raw: event,
287
+ };
288
+ }