luckytri 1.0.0 → 1.0.2

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 (139) hide show
  1. package/.github/SECURITY.md +4 -4
  2. package/README.en.md +252 -250
  3. package/README.md +252 -250
  4. package/bin/luckytri.js +5 -1
  5. package/docs/en/CHANGELOG.md +8 -0
  6. package/docs/en/connect.md +18 -18
  7. package/docs/en/guide.md +202 -216
  8. package/docs/en/npm-release.md +25 -25
  9. package/docs/zh/CHANGELOG.md +8 -0
  10. package/docs/zh/connect.md +17 -17
  11. package/docs/zh/guide.md +202 -216
  12. package/docs/zh/npm-release.md +33 -33
  13. package/package.json +110 -108
  14. package/plugins/sdk/index.d.ts +155 -155
  15. package/plugins/sdk/testing.js +58 -58
  16. package/plugins/weather/index.js +82 -82
  17. package/plugins/weather/luckytri-plugin.json +29 -29
  18. package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
  19. package/public/app/assets/Empty-DP6WMGHi.js +2 -0
  20. package/public/app/assets/Field-CtbdqaiB.js +1 -0
  21. package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
  22. package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
  23. package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
  24. package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
  25. package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
  26. package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
  27. package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
  28. package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
  29. package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
  30. package/public/app/assets/TimePage-7-awH5XL.js +3 -0
  31. package/public/app/assets/index-4rItIkD-.js +4 -0
  32. package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
  33. package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
  34. package/public/app/assets/media-SMfTT-yc.js +1 -0
  35. package/public/app/index.html +3 -3
  36. package/public/guide.en.html +72 -99
  37. package/public/guide.html +44 -86
  38. package/public/tutorial.en.md +25 -39
  39. package/public/tutorial.md +25 -39
  40. package/scripts/backup.js +311 -311
  41. package/scripts/browser.js +41 -41
  42. package/scripts/import-data.js +128 -0
  43. package/scripts/launch.js +101 -94
  44. package/scripts/plugin.js +150 -150
  45. package/scripts/reset-password.js +22 -0
  46. package/scripts/runtime.js +25 -25
  47. package/scripts/service.js +56 -47
  48. package/scripts/setup.js +50 -50
  49. package/scripts/start-server.js +41 -41
  50. package/scripts/stop.js +36 -36
  51. package/server/app.js +112 -96
  52. package/server/auth.js +217 -0
  53. package/server/channels/contract.js +57 -57
  54. package/server/channels/hub.js +165 -165
  55. package/server/channels/index.js +28 -25
  56. package/server/channels/onebot/index.js +335 -336
  57. package/server/channels/onebot/token.js +22 -3
  58. package/server/core/capabilities.js +208 -208
  59. package/server/core/events.js +64 -60
  60. package/server/core/image-preprocessor.js +208 -208
  61. package/server/core/model-manager.js +1213 -1213
  62. package/server/core/net-guard.js +49 -49
  63. package/server/core/orchestrator.js +569 -569
  64. package/server/core/perceivers.js +91 -91
  65. package/server/core/persona-manager.js +243 -243
  66. package/server/core/repository.js +293 -293
  67. package/server/core/response-validator.js +508 -508
  68. package/server/core/turn-processor.js +630 -630
  69. package/server/core/turn.js +236 -236
  70. package/server/core/vision-manager.js +633 -633
  71. package/server/i18n/errors.js +548 -497
  72. package/server/i18n/labels.js +649 -653
  73. package/server/index.js +151 -102
  74. package/server/knowledge/manager.js +386 -386
  75. package/server/knowledge/schema.js +12 -8
  76. package/server/local-access.js +32 -0
  77. package/server/mind/anticipations.js +507 -507
  78. package/server/mind/api.js +703 -703
  79. package/server/mind/budget.js +152 -152
  80. package/server/mind/context-selection.js +61 -61
  81. package/server/mind/day-planner.js +324 -324
  82. package/server/mind/evidence.js +148 -148
  83. package/server/mind/index.js +445 -445
  84. package/server/mind/life/diary.js +786 -786
  85. package/server/mind/life/index.js +1049 -1049
  86. package/server/mind/observations.js +84 -84
  87. package/server/mind/own-day.js +324 -324
  88. package/server/mind/own-voice.js +318 -318
  89. package/server/mind/senses.js +53 -53
  90. package/server/mind/time/activity-clock.js +196 -196
  91. package/server/mind/time/availability.js +69 -69
  92. package/server/mind/time/commitment-review.js +198 -198
  93. package/server/mind/time/executor.js +512 -512
  94. package/server/mind/time/experiences.js +128 -128
  95. package/server/mind/time/games.js +562 -562
  96. package/server/mind/time/index.js +739 -739
  97. package/server/mind/time/intent.js +166 -166
  98. package/server/mind/time/kinds.js +171 -171
  99. package/server/mind/time/plugin-activity.js +207 -207
  100. package/server/mind/time/tasks.js +587 -587
  101. package/server/mind/util.js +110 -110
  102. package/server/mind/view.js +273 -273
  103. package/server/paths.js +102 -102
  104. package/server/plugins/api.js +204 -204
  105. package/server/plugins/host.js +967 -967
  106. package/server/plugins/install.js +295 -295
  107. package/server/plugins/manifest.js +201 -201
  108. package/server/plugins/permissions.js +42 -42
  109. package/server/plugins/runner/guard.mjs +36 -36
  110. package/server/plugins/runner/worker.mjs +250 -250
  111. package/server/plugins/schema.js +91 -91
  112. package/server/plugins/zip.js +112 -112
  113. package/server/startup-lock.js +118 -118
  114. package/server/storage/archive.js +242 -241
  115. package/server/storage/backup-cleanup-worker.js +8 -8
  116. package/server/storage/backup-cleanup.js +415 -414
  117. package/server/storage/backup-lock.js +76 -76
  118. package/server/storage/backup-scheduler.js +131 -131
  119. package/server/storage/data-package.js +298 -0
  120. package/server/storage/data-transfer-api.js +50 -0
  121. package/server/storage/data-transfer-worker.js +18 -0
  122. package/server/storage/data-transfer.js +415 -0
  123. package/server/storage/store.js +202 -199
  124. package/server/studio/readiness.js +181 -190
  125. package/server/studio/settings.js +236 -236
  126. package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
  127. package/public/app/assets/Empty-CpP5kQHH.js +0 -2
  128. package/public/app/assets/Field-D4gY-INU.js +0 -1
  129. package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
  130. package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
  131. package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
  132. package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
  133. package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
  134. package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
  135. package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
  136. package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
  137. package/public/app/assets/TimePage-Cw44inM4.js +0 -3
  138. package/public/app/assets/index-BheRW-1t.js +0 -4
  139. package/public/app/assets/media-BzPDBm7e.js +0 -1
@@ -0,0 +1,415 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { randomUUID } from "node:crypto";
3
+ import {
4
+ createReadStream,
5
+ createWriteStream,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ writeFileSync,
10
+ readdirSync,
11
+ lstatSync,
12
+ rmSync,
13
+ renameSync,
14
+ } from "node:fs";
15
+ import { join, resolve, relative, isAbsolute } from "node:path";
16
+ import { pipeline } from "node:stream/promises";
17
+ import { Transform } from "node:stream";
18
+ import { runtimePaths } from "../paths.js";
19
+ import { packageHeader } from "./data-package.js";
20
+ import { DatabaseSync } from "node:sqlite";
21
+
22
+ const ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
23
+ const TTL = 3600000;
24
+ export const IMPORT_FAILURE =
25
+ "导入未完成,原数据已保留。请重新上传或查看运行日志。";
26
+ export const IMPORT_START_FAILURE =
27
+ "导入启动未完成,未替换运行数据。请重新启动实例。";
28
+ export const IMPORT_RESTART_FAILURE =
29
+ "数据已导入,但自动重启未完成。请手动重新启动实例。";
30
+ export const transferRoot = () => join(runtimePaths().data, "transfers");
31
+ export function readTransferJob(root, id) {
32
+ if (!ID.test(String(id))) throw Error("数据任务不存在");
33
+ const job = JSON.parse(readFileSync(join(root, id, "job.json"), "utf8"));
34
+ if (job.id !== id) throw Error("数据任务不存在");
35
+ return job;
36
+ }
37
+ export function writeTransferJob(root, job) {
38
+ if (!ID.test(job.id)) throw Error("数据任务不存在");
39
+ const temporary = join(root, job.id, `.job-${randomUUID()}.partial`);
40
+ try {
41
+ writeFileSync(temporary, JSON.stringify(job), {
42
+ mode: 0o600,
43
+ flush: true,
44
+ flag: "wx",
45
+ });
46
+ renameSync(temporary, join(root, job.id, "job.json"));
47
+ } finally {
48
+ rmSync(temporary, { force: true });
49
+ }
50
+ }
51
+ function work(payload) {
52
+ return new Promise((resolve, reject) => {
53
+ const worker = new Worker(
54
+ new URL("./data-transfer-worker.js", import.meta.url),
55
+ { workerData: payload, stdout: true, stderr: true },
56
+ );
57
+ // Staging migrations must not interleave their console output with the
58
+ // service or a test runner's protocol. Results travel through parentPort.
59
+ worker.stdout.resume();
60
+ worker.stderr.resume();
61
+ worker.once("message", ({ result, error }) =>
62
+ error ? reject(Error(error)) : resolve(result),
63
+ );
64
+ worker.once("error", reject);
65
+ worker.once("exit", (code) => {
66
+ if (code) reject(Error("数据处理失败"));
67
+ });
68
+ });
69
+ }
70
+ export function createDataTransfer({
71
+ source = runtimePaths().database,
72
+ backups = runtimePaths().backups,
73
+ directory = transferRoot(),
74
+ backup,
75
+ cleanup,
76
+ onApply,
77
+ run = work,
78
+ now = Date.now,
79
+ maxUploadBytes,
80
+ } = {}) {
81
+ const root = resolve(directory);
82
+ mkdirSync(root, { recursive: true, mode: 0o700 });
83
+ let active = false;
84
+ const maxBytes =
85
+ maxUploadBytes ??
86
+ Math.floor(
87
+ Math.min(
88
+ 100,
89
+ Math.max(1, Number(process.env.DATA_TRANSFER_MAX_GB) || 10),
90
+ ) * 1073741824,
91
+ );
92
+ const get = (id) => {
93
+ try {
94
+ return readTransferJob(root, id);
95
+ } catch {
96
+ throw Error("数据任务不存在");
97
+ }
98
+ };
99
+ const save = (job) => {
100
+ writeTransferJob(root, job);
101
+ return job;
102
+ };
103
+ const publicJob = (job) => {
104
+ const { manifest, ownerPid, ...rest } = job;
105
+ return rest;
106
+ };
107
+ const check = () => {
108
+ if (active || backup?.running || cleanup?.running)
109
+ throw Error("数据处理或备份正在进行,请稍后再试");
110
+ if (source === ":memory:") throw Error("当前实例不支持文件导入导出");
111
+ };
112
+ const prune = () => {
113
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
114
+ if (!entry.isDirectory() || !ID.test(entry.name)) continue;
115
+ const path = join(root, entry.name);
116
+ const part = relative(root, resolve(path));
117
+ if (
118
+ !part ||
119
+ part.startsWith("..") ||
120
+ isAbsolute(part) ||
121
+ lstatSync(path).isSymbolicLink()
122
+ )
123
+ continue;
124
+ try {
125
+ const job = get(entry.name);
126
+ if (
127
+ now() - (job.applied || job.readyAt || job.created) > TTL &&
128
+ !["processing", "uploading", "queued"].includes(job.status)
129
+ )
130
+ rmSync(path, { recursive: true, force: true });
131
+ } catch {
132
+ /* Ignore incomplete metadata while a job is publishing it. */
133
+ }
134
+ }
135
+ };
136
+ const clearMigrationCopies = (job) => {
137
+ const dir = join(root, job.id, "backups");
138
+ const part = relative(root, resolve(dir));
139
+ if (
140
+ part &&
141
+ !part.startsWith("..") &&
142
+ !isAbsolute(part) &&
143
+ existsSync(dir) &&
144
+ lstatSync(dir).isDirectory() &&
145
+ !lstatSync(dir).isSymbolicLink()
146
+ )
147
+ try {
148
+ rmSync(dir, { recursive: true, force: true });
149
+ } catch {
150
+ return false;
151
+ }
152
+ return true;
153
+ };
154
+ const clearFiles = (job) => {
155
+ let cleared = true;
156
+ for (const file of [
157
+ "upload",
158
+ "data.db",
159
+ "data.db.json",
160
+ "data.db-wal",
161
+ "data.db-shm",
162
+ ])
163
+ try {
164
+ rmSync(join(root, job.id, file), { force: true });
165
+ } catch {
166
+ cleared = false;
167
+ }
168
+ return clearMigrationCopies(job) && cleared;
169
+ };
170
+ const create = (kind, name = "") => {
171
+ prune();
172
+ // Keep at most one unused file of each kind, rather than accumulating
173
+ // multi-GiB previews every time the user picks another file.
174
+ for (const id of readdirSync(root).filter((id) => ID.test(id))) {
175
+ const previous = get(id);
176
+ if (previous.kind === kind && previous.status === "ready") {
177
+ if (!clearFiles(previous))
178
+ throw Error("临时数据文件正在使用,请稍后再试");
179
+ save({ ...previous, status: "expired" });
180
+ }
181
+ }
182
+ const id = randomUUID();
183
+ mkdirSync(join(root, id), { mode: 0o700 });
184
+ return save({
185
+ id,
186
+ kind,
187
+ name,
188
+ created: now(),
189
+ ownerPid: process.pid,
190
+ status: kind === "export" ? "processing" : "uploading",
191
+ });
192
+ };
193
+ const processJob = async (job) => {
194
+ try {
195
+ const result = await run({
196
+ kind: job.kind,
197
+ source,
198
+ backups,
199
+ target: join(root, job.id, "data.db"),
200
+ upload: join(root, job.id, "upload"),
201
+ });
202
+ rmSync(join(root, job.id, "upload"), { force: true });
203
+ clearMigrationCopies(job);
204
+ save({ ...job, status: "ready", readyAt: now(), ...result });
205
+ } catch (error) {
206
+ clearFiles(job);
207
+ try {
208
+ save({
209
+ ...job,
210
+ status: "error",
211
+ error: error.message || "数据处理失败",
212
+ });
213
+ } catch {
214
+ console.error("数据任务状态保存失败");
215
+ }
216
+ } finally {
217
+ active = false;
218
+ }
219
+ };
220
+ const alive = (pid) => {
221
+ if (!Number.isInteger(pid) || pid <= 0) return false;
222
+ try {
223
+ process.kill(pid, 0);
224
+ return true;
225
+ } catch (error) {
226
+ return error.code !== "ESRCH";
227
+ }
228
+ };
229
+ for (const id of readdirSync(root).filter((id) => ID.test(id))) {
230
+ try {
231
+ const job = get(id);
232
+ if (
233
+ job.kind === "import" &&
234
+ job.status === "queued" &&
235
+ source !== ":memory:" &&
236
+ existsSync(source)
237
+ ) {
238
+ const db = new DatabaseSync(source, { readOnly: true });
239
+ let marker;
240
+ try {
241
+ marker = JSON.parse(
242
+ db.prepare("SELECT value FROM settings WHERE id=1").get().value,
243
+ ).dataImportId;
244
+ } finally {
245
+ db.close();
246
+ }
247
+ if (marker === id) {
248
+ save({ ...job, status: "done", applied: now() });
249
+ continue;
250
+ }
251
+ }
252
+ if (
253
+ ["processing", "uploading", "queued"].includes(job.status) &&
254
+ !alive(job.ownerPid)
255
+ ) {
256
+ clearFiles(job);
257
+ save({
258
+ ...job,
259
+ status: "error",
260
+ readyAt: now(),
261
+ error: "数据处理被中断,请重新操作",
262
+ });
263
+ }
264
+ } catch {}
265
+ }
266
+ return {
267
+ get busy() {
268
+ return active;
269
+ },
270
+ info() {
271
+ prune();
272
+ const imports = readdirSync(root)
273
+ .filter((id) => ID.test(id))
274
+ .map((id) => {
275
+ try {
276
+ return get(id);
277
+ } catch {
278
+ return null;
279
+ }
280
+ })
281
+ .filter(
282
+ (job) =>
283
+ job?.kind === "import" && ["done", "error"].includes(job.status),
284
+ );
285
+ imports.sort((a, b) => b.created - a.created);
286
+ return {
287
+ available: source !== ":memory:",
288
+ maxBytes,
289
+ busy: active,
290
+ lastImport: imports[0] ? publicJob(imports[0]) : null,
291
+ };
292
+ },
293
+ job(id) {
294
+ return publicJob(get(id));
295
+ },
296
+ export() {
297
+ check();
298
+ active = true;
299
+ let job;
300
+ try {
301
+ job = create("export");
302
+ } catch (error) {
303
+ active = false;
304
+ throw error;
305
+ }
306
+ void processJob(job);
307
+ return publicJob(job);
308
+ },
309
+ async upload(stream, { bytes = 0, name = "" } = {}) {
310
+ check();
311
+ if (bytes > maxBytes) throw Error("数据文件超过导入大小限制");
312
+ active = true;
313
+ let job;
314
+ try {
315
+ job = create(
316
+ "import",
317
+ String(name).replace(/[\\/]/g, "_").slice(0, 180),
318
+ );
319
+ } catch (error) {
320
+ active = false;
321
+ throw error;
322
+ }
323
+ let total = 0;
324
+ const limit = new Transform({
325
+ transform(chunk, _encoding, done) {
326
+ total += chunk.length;
327
+ done(
328
+ total > maxBytes ? Error("数据文件超过导入大小限制") : null,
329
+ chunk,
330
+ );
331
+ },
332
+ });
333
+ try {
334
+ await pipeline(
335
+ stream,
336
+ limit,
337
+ createWriteStream(join(root, job.id, "upload"), {
338
+ flags: "wx",
339
+ mode: 0o600,
340
+ }),
341
+ );
342
+ if (!total) throw Error("数据文件为空");
343
+ save({ ...job, status: "processing" });
344
+ void processJob(job);
345
+ return publicJob({ ...job, status: "processing" });
346
+ } catch (error) {
347
+ active = false;
348
+ rmSync(join(root, job.id, "upload"), { force: true });
349
+ save({
350
+ ...job,
351
+ status: "error",
352
+ error: error.code === "ENOSPC" ? "磁盘空间不足" : "数据上传失败",
353
+ });
354
+ throw Error("数据上传失败");
355
+ }
356
+ },
357
+ async download(id, res) {
358
+ if (active) throw Error("数据处理或备份正在进行,请稍后再试");
359
+ const job = get(id);
360
+ if (
361
+ job.kind !== "export" ||
362
+ job.status !== "ready" ||
363
+ now() - (job.readyAt || job.created) > TTL
364
+ )
365
+ throw Error("导出文件尚未就绪或已过期");
366
+ const header = packageHeader(job.manifest);
367
+ res.setHeader("Content-Type", "application/octet-stream");
368
+ res.setHeader(
369
+ "Content-Disposition",
370
+ `attachment; filename="luckytri-data-${new Date(job.created).toISOString().slice(0, 10)}.luckytri"`,
371
+ );
372
+ res.setHeader("Content-Length", header.length + job.bytes);
373
+ active = true;
374
+ try {
375
+ res.write(header);
376
+ await pipeline(createReadStream(join(root, id, "data.db")), res);
377
+ clearFiles(job);
378
+ save({ ...job, status: "done" });
379
+ } finally {
380
+ active = false;
381
+ }
382
+ },
383
+ async apply(id) {
384
+ check();
385
+ const job = get(id);
386
+ if (!onApply) throw Error("当前实例不支持自动导入,请使用恢复命令");
387
+ if (
388
+ job.kind !== "import" ||
389
+ job.status !== "ready" ||
390
+ now() - (job.readyAt || job.created) > TTL
391
+ )
392
+ throw Error("导入数据尚未就绪或已过期");
393
+ active = true;
394
+ save({ ...job, status: "queued" });
395
+ try {
396
+ const owner = await onApply(id);
397
+ if (Number.isInteger(owner) && owner > 0)
398
+ save({ ...job, status: "queued", ownerPid: owner });
399
+ } catch (error) {
400
+ active = false;
401
+ save(job);
402
+ throw error;
403
+ }
404
+ return { queued: true, id };
405
+ },
406
+ discard(id) {
407
+ const job = get(id);
408
+ if (["processing", "uploading", "queued"].includes(job.status))
409
+ throw Error("数据处理正在进行,请稍后再试");
410
+ if (!clearFiles(job)) throw Error("临时数据文件正在使用,请稍后再试");
411
+ save({ ...job, status: "expired" });
412
+ return { ok: true };
413
+ },
414
+ };
415
+ }