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,311 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readdirSync,
7
+ readFileSync,
8
+ realpathSync,
9
+ renameSync,
10
+ rmSync,
11
+ statSync,
12
+ } from "node:fs";
13
+ import { resolve, join, basename, dirname } from "node:path";
14
+ import {
15
+ copyDatabaseFile,
16
+ archiveIdentity,
17
+ writeArchiveManifest,
18
+ ensureArchiveIdentity,
19
+ } from "../server/storage/archive.js";
20
+ import { withBackupLock } from "../server/storage/backup-lock.js";
21
+ import { pathToFileURL } from "node:url";
22
+ import { randomUUID } from "node:crypto";
23
+ import { runtimePaths } from "../server/paths.js";
24
+
25
+ const HOUR = 3600000;
26
+ const DAY = 24 * HOUR;
27
+ export const AUTO_PREFIX = "luckytri-auto-";
28
+ const MISSING = "数据库不存在,请先启动一次 LuckyTri";
29
+
30
+ export function backupDirectory(
31
+ source = runtimePaths().database,
32
+ env = process.env,
33
+ ) {
34
+ return env.BACKUP_DIR
35
+ ? resolve(runtimePaths(env).home, env.BACKUP_DIR)
36
+ : join(dirname(resolve(source)), "backups");
37
+ }
38
+
39
+ function copyDatabase(source, target) {
40
+ copyDatabaseFile(source, target);
41
+ }
42
+
43
+ export function backupDatabase(
44
+ source = runtimePaths().database,
45
+ directory = backupDirectory(source),
46
+ ) {
47
+ if (!existsSync(source)) throw new Error(MISSING);
48
+ return withBackupLock(directory, () => fullBackup(source, directory));
49
+ }
50
+
51
+ function fullBackup(source, directory) {
52
+ if (!existsSync(source)) throw new Error(MISSING);
53
+ ensureArchiveIdentity(source);
54
+ mkdirSync(directory, { recursive: true });
55
+ const target = resolve(
56
+ join(
57
+ directory,
58
+ `luckytri-${new Date().toISOString().replace(/[:.]/g, "-")}-${randomUUID().slice(0, 8)}.db`,
59
+ ),
60
+ );
61
+ const partial = `${target}.partial`;
62
+ try {
63
+ copyDatabase(source, partial);
64
+ writeArchiveManifest(partial, { filename: basename(target) });
65
+ renameSync(partial, target);
66
+ renameSync(`${partial}.json`, `${target}.json`);
67
+ } catch (error) {
68
+ rmSync(partial, { force: true });
69
+ rmSync(`${partial}.json`, { force: true });
70
+ throw error;
71
+ }
72
+ return target;
73
+ }
74
+
75
+ // Automatic backups exist to keep her continuity: what she remembers, how she
76
+ // has changed and the days she has lived. Model call traces are megabytes each
77
+ // and can be rebuilt from nothing that matters, so old ones are left out; that
78
+ // keeps a daily copy small enough to actually keep a week of them.
79
+ // When a backup was made comes from its name, so copying the folder to another
80
+ // disk does not make old backups look new.
81
+ function stampTime(name, file) {
82
+ const match = /(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/.exec(
83
+ name,
84
+ );
85
+ const parsed = match
86
+ ? Date.parse(`${match[1]}T${match[2]}:${match[3]}:${match[4]}.${match[5]}Z`)
87
+ : NaN;
88
+ return Number.isFinite(parsed) ? parsed : statSync(file).mtimeMs;
89
+ }
90
+
91
+ function autoBackups(directory, { source, databaseId } = {}) {
92
+ if (!existsSync(directory)) return [];
93
+ const scoped = !!source || databaseId !== undefined;
94
+ const sourceStat = source && existsSync(source) ? statSync(source) : null;
95
+ const sourcePath = source
96
+ ? sourceStat
97
+ ? realpathSync(source)
98
+ : resolve(source)
99
+ : null;
100
+ const identity = databaseId ?? (sourceStat ? archiveIdentity(source) : null);
101
+ const normalize = (file) =>
102
+ process.platform === "win32" ? file.toLowerCase() : file;
103
+ const entries = [];
104
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
105
+ if (
106
+ !entry.isFile() ||
107
+ !entry.name.startsWith(AUTO_PREFIX) ||
108
+ !entry.name.endsWith(".db")
109
+ )
110
+ continue;
111
+ const file = join(directory, entry.name);
112
+ const stat = lstatSync(file);
113
+ if (
114
+ !stat.isFile() ||
115
+ (sourcePath && normalize(realpathSync(file)) === normalize(sourcePath)) ||
116
+ (sourceStat?.ino &&
117
+ stat.ino === sourceStat.ino &&
118
+ stat.dev === sourceStat.dev)
119
+ )
120
+ continue;
121
+ if (scoped) {
122
+ if (!identity) continue;
123
+ try {
124
+ const manifestPath = `${file}.json`;
125
+ if (!lstatSync(manifestPath).isFile()) continue;
126
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
127
+ if (
128
+ manifest.format !== 1 ||
129
+ manifest.app !== "luckytri" ||
130
+ manifest.kind !== "auto" ||
131
+ manifest.databaseId !== identity ||
132
+ archiveIdentity(file) !== identity
133
+ )
134
+ continue;
135
+ } catch {
136
+ // Legacy, damaged and unidentifiable copies require a manual decision.
137
+ continue;
138
+ }
139
+ }
140
+ entries.push({ name: entry.name, file, time: stampTime(entry.name, file) });
141
+ }
142
+ return entries.sort(
143
+ (a, b) => b.time - a.time || b.name.localeCompare(a.name),
144
+ );
145
+ }
146
+
147
+ // Newest first, with the size the management page shows.
148
+ export function listAutoBackups(directory, options = {}) {
149
+ return autoBackups(directory, options).map(({ name, file, time }) => ({
150
+ name,
151
+ time,
152
+ bytes: statSync(file).size,
153
+ }));
154
+ }
155
+
156
+ export function autoBackupDue(
157
+ directory,
158
+ { now = Date.now(), intervalMs = DAY, source, databaseId } = {},
159
+ ) {
160
+ const latest = autoBackups(directory, { source, databaseId })[0];
161
+ return !latest || now - latest.time >= intervalMs;
162
+ }
163
+
164
+ // Trim a fresh copy down to what continuity needs, then prove it is readable.
165
+ function slimAndVerify(file, traceCutoff) {
166
+ const db = new DatabaseSync(file);
167
+ try {
168
+ const hasTraces = db
169
+ .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
170
+ .get("core_traces");
171
+ if (hasTraces) {
172
+ db.prepare("DELETE FROM core_traces WHERE time<?").run(traceCutoff);
173
+ db.exec("VACUUM");
174
+ }
175
+ const check = db.prepare("PRAGMA integrity_check").all();
176
+ if (check.length !== 1 || check[0].integrity_check !== "ok")
177
+ throw new Error("备份未通过完整性检查,已丢弃");
178
+ const hasEvents = db
179
+ .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
180
+ .get("core_events");
181
+ return {
182
+ events: hasEvents
183
+ ? Number(db.prepare("SELECT COUNT(*) n FROM core_events").get().n)
184
+ : 0,
185
+ };
186
+ } finally {
187
+ db.close();
188
+ }
189
+ }
190
+
191
+ export function autoBackup(options = {}) {
192
+ const source = options.source || runtimePaths().database;
193
+ const directory = options.directory || backupDirectory(source);
194
+ if (!existsSync(source)) throw new Error(MISSING);
195
+ return withBackupLock(directory, () =>
196
+ runAutoBackup({ ...options, source, directory }),
197
+ );
198
+ }
199
+
200
+ function runAutoBackup({
201
+ source = runtimePaths().database,
202
+ directory = backupDirectory(source),
203
+ now = Date.now(),
204
+ intervalMs = DAY,
205
+ keep = 7,
206
+ keepTraceDays = 1,
207
+ force = false,
208
+ } = {}) {
209
+ if (!existsSync(source)) throw new Error(MISSING);
210
+ if (!force && !autoBackupDue(directory, { now, intervalMs, source }))
211
+ return { status: "skipped" };
212
+ const databaseId = ensureArchiveIdentity(source);
213
+ mkdirSync(directory, { recursive: true });
214
+ const sourceStat = statSync(source);
215
+ const sourcePath = realpathSync(source);
216
+ const normalize = (file) =>
217
+ process.platform === "win32" ? file.toLowerCase() : file;
218
+ // A run that was cut off leaves a partial file; it is never a backup.
219
+ for (const name of readdirSync(directory)) {
220
+ if (!name.startsWith(AUTO_PREFIX) || !name.endsWith(".partial")) continue;
221
+ const file = join(directory, name);
222
+ const stat = lstatSync(file);
223
+ if (
224
+ stat.isFile() &&
225
+ now - stat.mtimeMs > 6 * HOUR &&
226
+ normalize(realpathSync(file)) !== normalize(sourcePath) &&
227
+ !(
228
+ sourceStat.ino &&
229
+ stat.ino === sourceStat.ino &&
230
+ stat.dev === sourceStat.dev
231
+ )
232
+ )
233
+ rmSync(file, { force: true });
234
+ }
235
+ let stampTimeMs = now;
236
+ let final;
237
+ do {
238
+ const stamp = new Date(stampTimeMs++).toISOString().replace(/[:.]/g, "-");
239
+ final = resolve(join(directory, `${AUTO_PREFIX}${stamp}.db`));
240
+ } while (existsSync(final) || existsSync(`${final}.json`));
241
+ const partial = `${final}.${randomUUID().slice(0, 8)}.partial`;
242
+ let info;
243
+ try {
244
+ copyDatabase(source, partial);
245
+ info = slimAndVerify(partial, now - keepTraceDays * DAY);
246
+ writeArchiveManifest(partial, { kind: "auto", filename: basename(final) });
247
+ renameSync(partial, final);
248
+ renameSync(`${partial}.json`, `${final}.json`);
249
+ } catch (error) {
250
+ rmSync(partial, { force: true });
251
+ rmSync(`${partial}.json`, { force: true });
252
+ throw error;
253
+ }
254
+ const removed = [];
255
+ for (const old of autoBackups(directory, { source, databaseId }).slice(
256
+ Math.max(1, keep),
257
+ )) {
258
+ if (existsSync(`${old.file}-wal`) || existsSync(`${old.file}-shm`))
259
+ continue;
260
+ rmSync(old.file, { force: true });
261
+ rmSync(`${old.file}.json`, { force: true });
262
+ removed.push(old.name);
263
+ }
264
+ return {
265
+ status: "created",
266
+ file: final,
267
+ bytes: statSync(final).size,
268
+ events: info.events,
269
+ removed,
270
+ };
271
+ }
272
+
273
+ // Reads the schedule from the environment; 0 turns automatic backups off.
274
+ export function autoBackupOptions(env = process.env) {
275
+ const hours = Number(env.BACKUP_INTERVAL_HOURS ?? 24);
276
+ const keep = Number(env.BACKUP_KEEP ?? 7);
277
+ const traceDays = Number(env.BACKUP_TRACE_DAYS ?? 1);
278
+ return {
279
+ enabled: Number.isFinite(hours) && hours > 0,
280
+ intervalMs: (Number.isFinite(hours) && hours > 0 ? hours : 24) * HOUR,
281
+ keep: Number.isFinite(keep) && keep >= 1 ? Math.floor(keep) : 7,
282
+ keepTraceDays: Number.isFinite(traceDays) && traceDays >= 0 ? traceDays : 1,
283
+ };
284
+ }
285
+
286
+ if (
287
+ process.argv[1] &&
288
+ import.meta.url === pathToFileURL(resolve(process.argv[1])).href
289
+ ) {
290
+ try {
291
+ const { prepareRuntime } = await import("./runtime.js");
292
+ prepareRuntime({ initialize: false });
293
+ if (process.argv.includes("--auto")) {
294
+ const options = autoBackupOptions();
295
+ const result = autoBackup({
296
+ intervalMs: options.intervalMs,
297
+ keep: options.keep,
298
+ keepTraceDays: options.keepTraceDays,
299
+ force: process.argv.includes("--force"),
300
+ });
301
+ console.log(
302
+ result.status === "created"
303
+ ? `自动备份完成:${result.file}(${Math.round(result.bytes / 1048576)} MB,${result.events} 条事件${result.removed.length ? `,清理旧备份 ${result.removed.length} 份` : ""})`
304
+ : "自动备份未到时间,已跳过",
305
+ );
306
+ } else console.log("备份完成:" + backupDatabase());
307
+ } catch (error) {
308
+ console.error(error.message);
309
+ process.exitCode = 1;
310
+ }
311
+ }
@@ -0,0 +1,41 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export function browserCommand(url, platform = process.platform) {
4
+ if (platform === "win32")
5
+ return {
6
+ command: "powershell.exe",
7
+ args: [
8
+ "-NoProfile",
9
+ "-NonInteractive",
10
+ "-Command",
11
+ "Start-Process $env:LUCKY_LAUNCH_URL",
12
+ ],
13
+ env: { ...process.env, LUCKY_LAUNCH_URL: url },
14
+ };
15
+ return { command: platform === "darwin" ? "open" : "xdg-open", args: [url] };
16
+ }
17
+
18
+ export async function openBrowser(url) {
19
+ const { command, args, env } = browserCommand(url);
20
+ const child = spawn(command, args, {
21
+ windowsHide: true,
22
+ stdio: "ignore",
23
+ env,
24
+ });
25
+ await new Promise((resolve, reject) => {
26
+ const timeout = setTimeout(() => {
27
+ child.kill();
28
+ reject(Error("浏览器未打开,请手动打开上方地址。"));
29
+ }, 10000);
30
+ const finish = (error) => {
31
+ clearTimeout(timeout);
32
+ error ? reject(error) : resolve();
33
+ };
34
+ child.once("error", () =>
35
+ finish(Error("浏览器未打开,请手动打开上方地址。")),
36
+ );
37
+ child.once("exit", (code) =>
38
+ finish(code === 0 ? null : Error("浏览器未打开,请手动打开上方地址。")),
39
+ );
40
+ });
41
+ }
@@ -0,0 +1,94 @@
1
+ import { openSync, closeSync, mkdirSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { join } from "node:path";
4
+ import { prepareRuntime } from "./runtime.js";
5
+ import { running, serviceUrl } from "./service.js";
6
+ import { openBrowser } from "./browser.js";
7
+ import {
8
+ claimLaunch,
9
+ clearStarting,
10
+ starting,
11
+ } from "../server/startup-lock.js";
12
+ import {
13
+ resolveProxyEnvironment,
14
+ supportsNodeEnvironmentProxy,
15
+ } from "./proxy-config.js";
16
+
17
+ try {
18
+ const paths = prepareRuntime();
19
+ const proxy = await resolveProxyEnvironment(process.env);
20
+ const base = serviceUrl();
21
+ if (proxy.enabled && !supportsNodeEnvironmentProxy())
22
+ throw new Error(
23
+ "系统代理转发需要 Node.js 24.5 或更高版本。请升级 Node.js 后重试。",
24
+ );
25
+ if (!(await running({ base }))) {
26
+ let childExited = false;
27
+ const fresh = claimLaunch();
28
+ if (!fresh) console.log("上一次启动还在进行,正在等它就绪,不会再开一份。");
29
+ else {
30
+ console.log("正在启动 LuckyTri…");
31
+ console.log("窗口会停在这里直到服务就绪,请不要关闭。");
32
+ try {
33
+ await import("express").catch(() => {
34
+ throw new Error("缺少依赖,请先在项目目录执行 npm install。");
35
+ });
36
+ mkdirSync(paths.data, { recursive: true, mode: 0o700 });
37
+ const log = openSync(paths.log, "a", 0o600);
38
+ const args = [
39
+ ...(proxy.enabled ? ["--use-env-proxy"] : []),
40
+ join(paths.package, "server", "index.js"),
41
+ ];
42
+ const child = spawn(process.execPath, args, {
43
+ cwd: paths.home,
44
+ detached: true,
45
+ windowsHide: true,
46
+ env: {
47
+ ...process.env,
48
+ ...proxy.env,
49
+ LUCKYTRI_LAUNCH_PID: String(process.pid),
50
+ },
51
+ stdio: ["ignore", log, log],
52
+ });
53
+ closeSync(log);
54
+ child.once("exit", () => {
55
+ childExited = true;
56
+ });
57
+ await new Promise((resolve, reject) => {
58
+ child.once("spawn", resolve);
59
+ child.once("error", reject);
60
+ });
61
+ child.unref();
62
+ } catch (error) {
63
+ clearStarting();
64
+ throw error;
65
+ }
66
+ }
67
+ let ready = false;
68
+ for (let attempt = 0; attempt < 2000; attempt++) {
69
+ await new Promise((resolve) => setTimeout(resolve, 300));
70
+ if (attempt > 0 && attempt % 16 === 0) console.log("仍在启动…");
71
+ if (await running({ base, lenient: true })) {
72
+ ready = true;
73
+ break;
74
+ }
75
+ if (fresh && childExited)
76
+ throw new Error(`启动进程已退出,请查看 ${paths.log}`);
77
+ if (!fresh && !starting())
78
+ throw new Error("上一次启动已中断,请再试一次。");
79
+ }
80
+ if (!ready) throw new Error(`启动未完成,请查看 ${paths.log}`);
81
+ console.log(
82
+ proxy.enabled
83
+ ? "LuckyTri 已在后台启动,外部 API 走系统代理。"
84
+ : "LuckyTri 已在后台启动。",
85
+ );
86
+ } else console.log("LuckyTri 已经运行,直接打开管理台。");
87
+ console.log(base);
88
+ if (!process.argv.includes("--no-browser")) {
89
+ await openBrowser(base).catch((error) => console.warn(error.message));
90
+ }
91
+ } catch (error) {
92
+ console.error(error.message);
93
+ process.exitCode = 1;
94
+ }
@@ -0,0 +1,150 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ mkdirSync,
4
+ readdirSync,
5
+ readFileSync,
6
+ statSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { join, relative } from "node:path";
10
+ import { validateManifest } from "../server/plugins/manifest.js";
11
+
12
+ const command = process.argv[2];
13
+ const arg = process.argv[3];
14
+
15
+ function walk(dir, out = []) {
16
+ for (const name of readdirSync(dir)) {
17
+ if (name === "node_modules") continue;
18
+ const path = join(dir, name);
19
+ if (statSync(path).isDirectory()) walk(path, out);
20
+ else out.push(path);
21
+ }
22
+ return out;
23
+ }
24
+
25
+ function crc32(buffer) {
26
+ let crc = ~0;
27
+ for (const byte of buffer) {
28
+ crc ^= byte;
29
+ for (let bit = 0; bit < 8; bit++)
30
+ crc = crc & 1 ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
31
+ }
32
+ return ~crc >>> 0;
33
+ }
34
+
35
+ function pack(dir) {
36
+ const files = walk(dir).map((path) => ({
37
+ name: relative(dir, path).replace(/\\/g, "/"),
38
+ bytes: readFileSync(path),
39
+ }));
40
+ const locals = [];
41
+ const central = [];
42
+ let offset = 0;
43
+ for (const file of files) {
44
+ const name = Buffer.from(file.name);
45
+ const crc = crc32(file.bytes);
46
+ const local = Buffer.alloc(30 + name.length);
47
+ local.writeUInt32LE(0x04034b50, 0);
48
+ local.writeUInt16LE(20, 4);
49
+ local.writeUInt32LE(crc, 14);
50
+ local.writeUInt32LE(file.bytes.length, 18);
51
+ local.writeUInt32LE(file.bytes.length, 22);
52
+ local.writeUInt16LE(name.length, 26);
53
+ name.copy(local, 30);
54
+ const centralHeader = Buffer.alloc(46 + name.length);
55
+ centralHeader.writeUInt32LE(0x02014b50, 0);
56
+ centralHeader.writeUInt16LE(20, 4);
57
+ centralHeader.writeUInt16LE(20, 6);
58
+ centralHeader.writeUInt32LE(crc, 16);
59
+ centralHeader.writeUInt32LE(file.bytes.length, 20);
60
+ centralHeader.writeUInt32LE(file.bytes.length, 24);
61
+ centralHeader.writeUInt16LE(name.length, 28);
62
+ centralHeader.writeUInt32LE(offset, 42);
63
+ name.copy(centralHeader, 46);
64
+ locals.push(local, file.bytes);
65
+ central.push(centralHeader);
66
+ offset += local.length + file.bytes.length;
67
+ }
68
+ const directory = Buffer.concat(central);
69
+ const end = Buffer.alloc(22);
70
+ end.writeUInt32LE(0x06054b50, 0);
71
+ end.writeUInt16LE(files.length, 8);
72
+ end.writeUInt16LE(files.length, 10);
73
+ end.writeUInt32LE(directory.length, 12);
74
+ end.writeUInt32LE(offset, 16);
75
+ return Buffer.concat([...locals, directory, end]);
76
+ }
77
+
78
+ if (command === "new") {
79
+ const id = arg || "example";
80
+ const dir = join("plugins", id);
81
+ mkdirSync(dir, { recursive: true });
82
+ writeFileSync(
83
+ join(dir, "luckytri-plugin.json"),
84
+ JSON.stringify(
85
+ {
86
+ id,
87
+ name: id,
88
+ version: "0.1.0",
89
+ pluginApi: "1.0",
90
+ luckytri: ">=1.0.0 <2.0.0",
91
+ description: "",
92
+ entry: "index.js",
93
+ permissions: [],
94
+ contributes: {},
95
+ },
96
+ null,
97
+ 2,
98
+ ),
99
+ );
100
+ writeFileSync(
101
+ join(dir, "index.js"),
102
+ "export default { activate(ctx) { ctx.status.set({ text: '已接上', tone: 'ok' }); } };\n",
103
+ );
104
+ console.log(dir);
105
+ } else if (command === "pack") {
106
+ const dir = arg;
107
+ const manifest = JSON.parse(
108
+ readFileSync(join(dir, "luckytri-plugin.json"), "utf8"),
109
+ );
110
+ const bytes = pack(dir);
111
+ const file = `${manifest.id}-${manifest.version}.zip`;
112
+ writeFileSync(file, bytes);
113
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
114
+ console.log(
115
+ JSON.stringify(
116
+ {
117
+ id: manifest.id,
118
+ version: manifest.version,
119
+ pluginApi: manifest.pluginApi,
120
+ luckytri: manifest.luckytri,
121
+ sha256,
122
+ permissions: manifest.permissions,
123
+ network: manifest.network || [],
124
+ file,
125
+ },
126
+ null,
127
+ 2,
128
+ ),
129
+ );
130
+ } else if (command === "registry-check") {
131
+ const index = JSON.parse(readFileSync(arg || "index.json", "utf8"));
132
+ if (index.format !== 1 || !Array.isArray(index.plugins))
133
+ throw new Error("插件索引无效");
134
+ for (const plugin of index.plugins)
135
+ for (const version of plugin.versions || [])
136
+ if (!version.sha256 || !version.url) throw new Error("插件索引无效");
137
+ console.log(`ok ${index.plugins.length}`);
138
+ } else if (command === "check") {
139
+ const dir = resolveDir(arg);
140
+ const manifest = validateManifest(
141
+ JSON.parse(readFileSync(join(dir, "luckytri-plugin.json"), "utf8")),
142
+ );
143
+ console.log(`${manifest.id} ${manifest.version} 清单通过`);
144
+ } else {
145
+ console.log("用法: npm run plugin -- new|check|pack|registry-check");
146
+ }
147
+
148
+ function resolveDir(value) {
149
+ return value || ".";
150
+ }