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,199 @@
1
+ // QQ 开放平台 HTTP interface: AccessToken management and message sending.
2
+ // https://bot.q.qq.com/wiki/develop/api-v2/
3
+
4
+ export const API_BASE = "https://api.bot.qq.com";
5
+ // The platform documents both hosts for the token call over time; try them in
6
+ // order, but only fall through on a network failure or a missing route.
7
+ export const TOKEN_URLS = [
8
+ "https://bots.qq.com/app/getAppAccessToken",
9
+ "https://api.bot.qq.com/app/getAppAccessToken",
10
+ ];
11
+
12
+ export class QQBotError extends Error {
13
+ constructor(message, { status = 0, code = 0, traceId = "" } = {}) {
14
+ super(message);
15
+ this.name = "QQBotError";
16
+ this.status = status;
17
+ this.code = Number(code) || 0;
18
+ this.traceId = traceId;
19
+ }
20
+ }
21
+
22
+ // An answer that never arrived is different from an answer that said no: the
23
+ // first leaves delivery unknown and must not be retried automatically.
24
+ export const isUncertain = (error) =>
25
+ error instanceof QQBotError ? error.uncertain === true : false;
26
+
27
+ function readable(body, status) {
28
+ const text =
29
+ body?.message || body?.msg || body?.error_description || body?.error || "";
30
+ return String(text || `HTTP ${status}`).slice(0, 200);
31
+ }
32
+
33
+ export class QQBotApi {
34
+ constructor({
35
+ appId,
36
+ secret,
37
+ fetch: fetcher = globalThis.fetch,
38
+ apiBase = API_BASE,
39
+ tokenUrls = TOKEN_URLS,
40
+ now = Date.now,
41
+ timeoutMs = 10000,
42
+ }) {
43
+ this.appId = String(appId || "");
44
+ this.secret = String(secret || "");
45
+ this.fetch = fetcher;
46
+ this.apiBase = String(apiBase).replace(/\/+$/, "");
47
+ this.tokenUrls = tokenUrls;
48
+ this.now = now;
49
+ this.timeoutMs = timeoutMs;
50
+ this.cached = null;
51
+ this.inflight = null;
52
+ }
53
+
54
+ async #post(url, body) {
55
+ return this.fetch(url, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify(body),
59
+ signal: AbortSignal.timeout(this.timeoutMs),
60
+ });
61
+ }
62
+
63
+ async #fetchToken() {
64
+ let failure = null;
65
+ for (const url of this.tokenUrls) {
66
+ let response;
67
+ try {
68
+ response = await this.#post(url, {
69
+ appId: this.appId,
70
+ clientSecret: this.secret,
71
+ });
72
+ } catch (error) {
73
+ failure = new QQBotError("无法连接 QQ 开放平台");
74
+ failure.cause = error;
75
+ continue;
76
+ }
77
+ if (response.status === 404) {
78
+ failure = new QQBotError("QQ 开放平台没有找到鉴权接口", {
79
+ status: 404,
80
+ });
81
+ continue;
82
+ }
83
+ let body = null;
84
+ try {
85
+ body = await response.json();
86
+ } catch {
87
+ /* Reported below with the HTTP status. */
88
+ }
89
+ const token = body?.access_token;
90
+ if (!response.ok || !token) {
91
+ throw new QQBotError(
92
+ readable(body, response.status) || "AppID 或 AppSecret 不正确",
93
+ { status: response.status, code: body?.code },
94
+ );
95
+ }
96
+ const seconds = Number(body.expires_in);
97
+ return {
98
+ token: String(token),
99
+ expiresAt:
100
+ this.now() +
101
+ (Number.isFinite(seconds) && seconds > 0 ? seconds : 7200) * 1000,
102
+ };
103
+ }
104
+ throw failure || new QQBotError("无法获取 AccessToken");
105
+ }
106
+
107
+ // The token is shared by every caller and renewed a little before it
108
+ // expires; the platform keeps the old one valid for the last minute.
109
+ async token({ force = false } = {}) {
110
+ if (!this.appId || !this.secret)
111
+ throw new QQBotError("尚未填写 AppID 和 AppSecret");
112
+ if (!force && this.cached && this.cached.expiresAt - this.now() > 90000)
113
+ return this.cached.token;
114
+ this.inflight ||= this.#fetchToken()
115
+ .then((value) => {
116
+ this.cached = value;
117
+ return value;
118
+ })
119
+ .finally(() => {
120
+ this.inflight = null;
121
+ });
122
+ return (await this.inflight).token;
123
+ }
124
+
125
+ invalidate() {
126
+ this.cached = null;
127
+ }
128
+
129
+ async request(method, path, body, { retry = true } = {}) {
130
+ const token = await this.token();
131
+ let response;
132
+ try {
133
+ response = await this.fetch(this.apiBase + path, {
134
+ method,
135
+ headers: {
136
+ Authorization: `QQBot ${token}`,
137
+ "Content-Type": "application/json",
138
+ },
139
+ body: body === undefined ? undefined : JSON.stringify(body),
140
+ signal: AbortSignal.timeout(this.timeoutMs),
141
+ });
142
+ } catch (error) {
143
+ const timedOut =
144
+ error?.name === "TimeoutError" || error?.name === "AbortError";
145
+ const neverLeft =
146
+ /^(ENOTFOUND|ECONNREFUSED|EAI_AGAIN|ENETUNREACH|ERR_TLS|CERT_|UNABLE_TO)/.test(
147
+ String(error?.cause?.code || error?.code || ""),
148
+ );
149
+ const failure = new QQBotError(
150
+ timedOut ? "QQ 发送确认超时,不自动重发" : "无法连接 QQ 开放平台",
151
+ );
152
+ // A send that left but was never answered may have arrived. Never
153
+ // repeat it on our own; one that could not leave is safe to report.
154
+ failure.uncertain = method !== "GET" && !neverLeft;
155
+ failure.cause = error;
156
+ throw failure;
157
+ }
158
+ let data = null;
159
+ try {
160
+ data = await response.json();
161
+ } catch {
162
+ /* An empty body is fine for some calls. */
163
+ }
164
+ const traceId = response.headers?.get?.("x-tps-trace-id") || "";
165
+ if (response.status === 401 && retry) {
166
+ this.invalidate();
167
+ return this.request(method, path, body, { retry: false });
168
+ }
169
+ if (!response.ok)
170
+ throw new QQBotError(readable(data, response.status), {
171
+ status: response.status,
172
+ code: data?.code ?? data?.err_code,
173
+ traceId,
174
+ });
175
+ return data ?? {};
176
+ }
177
+
178
+ async gatewayUrl() {
179
+ const data = await this.request("GET", "/gateway");
180
+ if (!data?.url) throw new QQBotError("QQ 开放平台没有返回网关地址");
181
+ return String(data.url);
182
+ }
183
+
184
+ sendGroup(groupOpenid, body) {
185
+ return this.request(
186
+ "POST",
187
+ `/v2/groups/${encodeURIComponent(groupOpenid)}/messages`,
188
+ body,
189
+ );
190
+ }
191
+
192
+ sendUser(openid, body) {
193
+ return this.request(
194
+ "POST",
195
+ `/v2/users/${encodeURIComponent(openid)}/messages`,
196
+ body,
197
+ );
198
+ }
199
+ }
@@ -0,0 +1,11 @@
1
+ // The AppID and AppSecret of the official bot. The environment wins over what
2
+ // was saved in the studio, the same way LLM_API_KEY does for the model key.
3
+ export function qqbotCredentials(store, env = process.env) {
4
+ const saved = store.settings();
5
+ return {
6
+ appId: String(env.QQBOT_APP_ID || saved.qqbotAppId || "").trim(),
7
+ secret: String(env.QQBOT_APP_SECRET || saved.qqbotSecret || "").trim(),
8
+ appIdFromEnv: !!env.QQBOT_APP_ID,
9
+ secretFromEnv: !!env.QQBOT_APP_SECRET,
10
+ };
11
+ }
@@ -0,0 +1,274 @@
1
+ import { WebSocket } from "ws";
2
+
3
+ // Opcodes of the QQ 开放平台 WebSocket gateway.
4
+ const OP = {
5
+ dispatch: 0,
6
+ heartbeat: 1,
7
+ identify: 2,
8
+ resume: 6,
9
+ reconnect: 7,
10
+ invalidSession: 9,
11
+ hello: 10,
12
+ ack: 11,
13
+ };
14
+
15
+ // GROUP_AND_C2C_EVENT: group messages, private messages, and the events that
16
+ // say a room switched proactive messages on or off.
17
+ export const INTENTS = 1 << 25;
18
+
19
+ const BACKOFF = [1000, 2000, 4000, 8000, 15000, 30000, 60000];
20
+ // Close codes after which the saved session cannot be resumed.
21
+ const FRESH = new Set([
22
+ 4006, 4007, 4009, 4900, 4901, 4902, 4903, 4904, 4905, 4906, 4907, 4908, 4909,
23
+ 4910, 4911, 4912, 4913,
24
+ ]);
25
+ // Close codes that retrying will not fix: the platform has taken the bot
26
+ // offline or refused its permissions.
27
+ const FATAL = new Set([4914, 4915]);
28
+ const TOKEN_REJECTED = new Set([4004]);
29
+
30
+ // Keeps one connection to the gateway alive: Hello → Identify, heartbeat,
31
+ // Resume after a short drop, a fresh session when the platform says so, and a
32
+ // patient reconnect with backoff.
33
+ export class QQBotGateway {
34
+ constructor({
35
+ api,
36
+ onDispatch,
37
+ onState = () => {},
38
+ WebSocketImpl = WebSocket,
39
+ now = Date.now,
40
+ backoff = BACKOFF,
41
+ }) {
42
+ this.backoff = backoff;
43
+ this.api = api;
44
+ this.onDispatch = onDispatch;
45
+ this.onState = onState;
46
+ this.WebSocketImpl = WebSocketImpl;
47
+ this.now = now;
48
+ this.socket = null;
49
+ this.sessionId = "";
50
+ this.seq = null;
51
+ this.ready = false;
52
+ this.attempt = 0;
53
+ this.stopped = true;
54
+ this.heartbeatTimer = null;
55
+ this.reconnectTimer = null;
56
+ this.acked = true;
57
+ this.generation = 0;
58
+ }
59
+
60
+ start() {
61
+ if (!this.stopped) return;
62
+ this.stopped = false;
63
+ this.attempt = 0;
64
+ this.#connect();
65
+ }
66
+
67
+ stop() {
68
+ this.stopped = true;
69
+ this.generation++;
70
+ clearTimeout(this.reconnectTimer);
71
+ clearInterval(this.heartbeatTimer);
72
+ this.reconnectTimer = null;
73
+ this.heartbeatTimer = null;
74
+ const socket = this.socket;
75
+ this.socket = null;
76
+ this.ready = false;
77
+ try {
78
+ socket?.removeAllListeners?.();
79
+ socket?.on?.("error", () => {});
80
+ socket?.terminate?.();
81
+ } catch {
82
+ /* Already gone. */
83
+ }
84
+ }
85
+
86
+ async #connect() {
87
+ const generation = ++this.generation;
88
+ clearTimeout(this.reconnectTimer);
89
+ this.onState({ state: "connecting" });
90
+ let url;
91
+ try {
92
+ url = await this.api.gatewayUrl();
93
+ } catch (error) {
94
+ if (generation !== this.generation || this.stopped) return;
95
+ this.onState({ state: "error", error: error.message });
96
+ return this.#later();
97
+ }
98
+ if (generation !== this.generation || this.stopped) return;
99
+ let socket;
100
+ try {
101
+ socket = new this.WebSocketImpl(url);
102
+ } catch (error) {
103
+ this.onState({ state: "error", error: error.message });
104
+ return this.#later();
105
+ }
106
+ this.socket = socket;
107
+ socket.on("message", (data) => {
108
+ if (generation === this.generation) this.#message(data, socket);
109
+ });
110
+ socket.on("error", (error) => {
111
+ if (generation === this.generation)
112
+ this.onState({ state: "error", error: error.message });
113
+ });
114
+ socket.on("close", (code) => {
115
+ if (generation !== this.generation) return;
116
+ this.#closed(Number(code) || 0);
117
+ });
118
+ }
119
+
120
+ async #identify(socket) {
121
+ const token = await this.api.token();
122
+ if (this.sessionId && this.seq != null)
123
+ socket.send(
124
+ JSON.stringify({
125
+ op: OP.resume,
126
+ d: {
127
+ token: `QQBot ${token}`,
128
+ session_id: this.sessionId,
129
+ seq: this.seq,
130
+ },
131
+ }),
132
+ );
133
+ else
134
+ socket.send(
135
+ JSON.stringify({
136
+ op: OP.identify,
137
+ d: {
138
+ token: `QQBot ${token}`,
139
+ intents: INTENTS,
140
+ shard: [0, 1],
141
+ properties: {},
142
+ },
143
+ }),
144
+ );
145
+ }
146
+
147
+ #message(data, socket) {
148
+ let payload;
149
+ try {
150
+ payload = JSON.parse(data.toString());
151
+ } catch {
152
+ return;
153
+ }
154
+ if (!payload || typeof payload !== "object") return;
155
+ if (Number.isInteger(payload.s)) this.seq = payload.s;
156
+ switch (payload.op) {
157
+ case OP.hello: {
158
+ const interval = Math.max(
159
+ 1000,
160
+ Number(payload.d?.heartbeat_interval) || 40000,
161
+ );
162
+ this.acked = true;
163
+ clearInterval(this.heartbeatTimer);
164
+ this.heartbeatTimer = setInterval(() => {
165
+ if (!this.acked) {
166
+ // A whole interval without an answer: the line is dead.
167
+ try {
168
+ socket.terminate();
169
+ } catch {
170
+ /* Closing anyway. */
171
+ }
172
+ return;
173
+ }
174
+ this.acked = false;
175
+ try {
176
+ socket.send(JSON.stringify({ op: OP.heartbeat, d: this.seq }));
177
+ } catch {
178
+ /* The close handler reconnects. */
179
+ }
180
+ }, interval);
181
+ this.heartbeatTimer.unref?.();
182
+ this.#identify(socket).catch((error) => {
183
+ this.onState({ state: "error", error: error.message });
184
+ try {
185
+ socket.terminate();
186
+ } catch {
187
+ /* Reconnect handles it. */
188
+ }
189
+ });
190
+ break;
191
+ }
192
+ case OP.ack:
193
+ this.acked = true;
194
+ break;
195
+ case OP.heartbeat:
196
+ try {
197
+ socket.send(JSON.stringify({ op: OP.heartbeat, d: this.seq }));
198
+ } catch {
199
+ /* Ignored. */
200
+ }
201
+ break;
202
+ case OP.reconnect:
203
+ try {
204
+ socket.terminate();
205
+ } catch {
206
+ /* Ignored. */
207
+ }
208
+ break;
209
+ case OP.invalidSession:
210
+ // The saved session is no good any more: start over.
211
+ this.sessionId = "";
212
+ this.seq = null;
213
+ try {
214
+ socket.terminate();
215
+ } catch {
216
+ /* Ignored. */
217
+ }
218
+ break;
219
+ case OP.dispatch:
220
+ if (payload.t === "READY") {
221
+ this.sessionId = String(payload.d?.session_id || "");
222
+ this.ready = true;
223
+ this.attempt = 0;
224
+ this.onState({ state: "ready", user: payload.d?.user || null });
225
+ } else if (payload.t === "RESUMED") {
226
+ this.ready = true;
227
+ this.attempt = 0;
228
+ this.onState({ state: "ready", resumed: true });
229
+ } else {
230
+ try {
231
+ this.onDispatch(payload.t, payload.d, payload.id);
232
+ } catch (error) {
233
+ console.error(`QQ 官方机器人事件处理失败:${error.message}`);
234
+ }
235
+ }
236
+ break;
237
+ default:
238
+ }
239
+ }
240
+
241
+ #closed(code) {
242
+ clearInterval(this.heartbeatTimer);
243
+ this.heartbeatTimer = null;
244
+ this.socket = null;
245
+ const wasReady = this.ready;
246
+ this.ready = false;
247
+ if (this.stopped) return;
248
+ if (FRESH.has(code)) {
249
+ this.sessionId = "";
250
+ this.seq = null;
251
+ }
252
+ if (TOKEN_REJECTED.has(code)) this.api.invalidate();
253
+ if (FATAL.has(code)) {
254
+ this.stopped = true;
255
+ this.onState({
256
+ state: "error",
257
+ error: `QQ 开放平台关闭了连接(${code}),请检查机器人状态与权限`,
258
+ fatal: true,
259
+ });
260
+ return;
261
+ }
262
+ this.onState({ state: "closed", code, wasReady });
263
+ this.#later();
264
+ }
265
+
266
+ #later() {
267
+ if (this.stopped) return;
268
+ const wait = this.backoff[Math.min(this.attempt, this.backoff.length - 1)];
269
+ this.attempt++;
270
+ clearTimeout(this.reconnectTimer);
271
+ this.reconnectTimer = setTimeout(() => this.#connect(), wait);
272
+ this.reconnectTimer.unref?.();
273
+ }
274
+ }