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
@@ -1,91 +1,91 @@
1
- // Tables a plugin is allowed to have a share in. The plugin never sees the
2
- // database; these rows are written by the host on its behalf.
3
- export function migratePlugins(db) {
4
- db.exec(`
5
- CREATE TABLE IF NOT EXISTS plugin_installs (
6
- id TEXT PRIMARY KEY,
7
- version TEXT NOT NULL,
8
- enabled INTEGER NOT NULL DEFAULT 0,
9
- state TEXT NOT NULL DEFAULT 'installed',
10
- source TEXT NOT NULL DEFAULT '',
11
- sha256 TEXT NOT NULL DEFAULT '',
12
- api TEXT NOT NULL DEFAULT '1.0',
13
- permissions TEXT NOT NULL DEFAULT '[]',
14
- granted TEXT NOT NULL DEFAULT '[]',
15
- manifest TEXT NOT NULL DEFAULT '{}',
16
- path TEXT NOT NULL DEFAULT '',
17
- error TEXT NOT NULL DEFAULT '',
18
- installed INTEGER NOT NULL,
19
- updated INTEGER NOT NULL
20
- );
21
- CREATE TABLE IF NOT EXISTS plugin_settings (
22
- plugin_id TEXT NOT NULL,
23
- key TEXT NOT NULL,
24
- value TEXT NOT NULL,
25
- secret INTEGER NOT NULL DEFAULT 0,
26
- PRIMARY KEY(plugin_id, key)
27
- );
28
- CREATE TABLE IF NOT EXISTS plugin_kv (
29
- plugin_id TEXT NOT NULL,
30
- key TEXT NOT NULL,
31
- value TEXT NOT NULL,
32
- updated INTEGER NOT NULL,
33
- PRIMARY KEY(plugin_id, key)
34
- );
35
- CREATE TABLE IF NOT EXISTS plugin_events (
36
- id INTEGER PRIMARY KEY,
37
- plugin_id TEXT NOT NULL,
38
- time INTEGER NOT NULL,
39
- kind TEXT NOT NULL,
40
- level TEXT NOT NULL DEFAULT 'info',
41
- message TEXT NOT NULL
42
- );
43
- CREATE INDEX IF NOT EXISTS plugin_events_plugin ON plugin_events(plugin_id, id DESC);
44
- CREATE TABLE IF NOT EXISTS plugin_sources (
45
- id TEXT PRIMARY KEY,
46
- url TEXT NOT NULL,
47
- created INTEGER NOT NULL
48
- );
49
- CREATE TABLE IF NOT EXISTS mind_observations (
50
- id TEXT PRIMARY KEY,
51
- created INTEGER NOT NULL,
52
- plugin_id TEXT NOT NULL,
53
- summary TEXT NOT NULL,
54
- detail TEXT NOT NULL DEFAULT '',
55
- discretion TEXT NOT NULL DEFAULT 'open',
56
- hash TEXT NOT NULL,
57
- revoked INTEGER NOT NULL DEFAULT 0
58
- );
59
- CREATE INDEX IF NOT EXISTS mind_observations_plugin ON mind_observations(plugin_id, created);
60
- CREATE TABLE IF NOT EXISTS mind_actions (
61
- id TEXT PRIMARY KEY,
62
- created INTEGER NOT NULL,
63
- plugin_id TEXT NOT NULL DEFAULT '',
64
- action TEXT NOT NULL,
65
- label TEXT NOT NULL DEFAULT '',
66
- input TEXT NOT NULL DEFAULT '{}',
67
- reason TEXT NOT NULL DEFAULT '',
68
- risk TEXT NOT NULL DEFAULT 'low',
69
- confirm TEXT NOT NULL DEFAULT 'none',
70
- state TEXT NOT NULL,
71
- result TEXT NOT NULL DEFAULT '',
72
- session_id TEXT,
73
- trace_id TEXT,
74
- finished INTEGER
75
- );
76
- CREATE INDEX IF NOT EXISTS mind_actions_state ON mind_actions(state, created);
77
- CREATE TABLE IF NOT EXISTS core_perceptions (
78
- cache_key TEXT PRIMARY KEY,
79
- session_id TEXT NOT NULL,
80
- message_id INTEGER,
81
- plugin_id TEXT NOT NULL,
82
- type TEXT NOT NULL,
83
- text TEXT NOT NULL,
84
- created INTEGER NOT NULL
85
- );
86
- `);
87
- // A restarted process cannot know whether a running action finished.
88
- db.prepare(
89
- "UPDATE mind_actions SET state='uncertain', result='实例中断,没有自动重做' WHERE state='running'",
90
- ).run();
91
- }
1
+ // Tables a plugin is allowed to have a share in. The plugin never sees the
2
+ // database; these rows are written by the host on its behalf.
3
+ export function migratePlugins(db) {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS plugin_installs (
6
+ id TEXT PRIMARY KEY,
7
+ version TEXT NOT NULL,
8
+ enabled INTEGER NOT NULL DEFAULT 0,
9
+ state TEXT NOT NULL DEFAULT 'installed',
10
+ source TEXT NOT NULL DEFAULT '',
11
+ sha256 TEXT NOT NULL DEFAULT '',
12
+ api TEXT NOT NULL DEFAULT '1.0',
13
+ permissions TEXT NOT NULL DEFAULT '[]',
14
+ granted TEXT NOT NULL DEFAULT '[]',
15
+ manifest TEXT NOT NULL DEFAULT '{}',
16
+ path TEXT NOT NULL DEFAULT '',
17
+ error TEXT NOT NULL DEFAULT '',
18
+ installed INTEGER NOT NULL,
19
+ updated INTEGER NOT NULL
20
+ );
21
+ CREATE TABLE IF NOT EXISTS plugin_settings (
22
+ plugin_id TEXT NOT NULL,
23
+ key TEXT NOT NULL,
24
+ value TEXT NOT NULL,
25
+ secret INTEGER NOT NULL DEFAULT 0,
26
+ PRIMARY KEY(plugin_id, key)
27
+ );
28
+ CREATE TABLE IF NOT EXISTS plugin_kv (
29
+ plugin_id TEXT NOT NULL,
30
+ key TEXT NOT NULL,
31
+ value TEXT NOT NULL,
32
+ updated INTEGER NOT NULL,
33
+ PRIMARY KEY(plugin_id, key)
34
+ );
35
+ CREATE TABLE IF NOT EXISTS plugin_events (
36
+ id INTEGER PRIMARY KEY,
37
+ plugin_id TEXT NOT NULL,
38
+ time INTEGER NOT NULL,
39
+ kind TEXT NOT NULL,
40
+ level TEXT NOT NULL DEFAULT 'info',
41
+ message TEXT NOT NULL
42
+ );
43
+ CREATE INDEX IF NOT EXISTS plugin_events_plugin ON plugin_events(plugin_id, id DESC);
44
+ CREATE TABLE IF NOT EXISTS plugin_sources (
45
+ id TEXT PRIMARY KEY,
46
+ url TEXT NOT NULL,
47
+ created INTEGER NOT NULL
48
+ );
49
+ CREATE TABLE IF NOT EXISTS mind_observations (
50
+ id TEXT PRIMARY KEY,
51
+ created INTEGER NOT NULL,
52
+ plugin_id TEXT NOT NULL,
53
+ summary TEXT NOT NULL,
54
+ detail TEXT NOT NULL DEFAULT '',
55
+ discretion TEXT NOT NULL DEFAULT 'open',
56
+ hash TEXT NOT NULL,
57
+ revoked INTEGER NOT NULL DEFAULT 0
58
+ );
59
+ CREATE INDEX IF NOT EXISTS mind_observations_plugin ON mind_observations(plugin_id, created);
60
+ CREATE TABLE IF NOT EXISTS mind_actions (
61
+ id TEXT PRIMARY KEY,
62
+ created INTEGER NOT NULL,
63
+ plugin_id TEXT NOT NULL DEFAULT '',
64
+ action TEXT NOT NULL,
65
+ label TEXT NOT NULL DEFAULT '',
66
+ input TEXT NOT NULL DEFAULT '{}',
67
+ reason TEXT NOT NULL DEFAULT '',
68
+ risk TEXT NOT NULL DEFAULT 'low',
69
+ confirm TEXT NOT NULL DEFAULT 'none',
70
+ state TEXT NOT NULL,
71
+ result TEXT NOT NULL DEFAULT '',
72
+ session_id TEXT,
73
+ trace_id TEXT,
74
+ finished INTEGER
75
+ );
76
+ CREATE INDEX IF NOT EXISTS mind_actions_state ON mind_actions(state, created);
77
+ CREATE TABLE IF NOT EXISTS core_perceptions (
78
+ cache_key TEXT PRIMARY KEY,
79
+ session_id TEXT NOT NULL,
80
+ message_id INTEGER,
81
+ plugin_id TEXT NOT NULL,
82
+ type TEXT NOT NULL,
83
+ text TEXT NOT NULL,
84
+ created INTEGER NOT NULL
85
+ );
86
+ `);
87
+ // A restarted process cannot know whether a running action finished.
88
+ db.prepare(
89
+ "UPDATE mind_actions SET state='uncertain', result='实例中断,没有自动重做' WHERE state='running'",
90
+ ).run();
91
+ }
@@ -1,112 +1,112 @@
1
- import { inflateRawSync } from "node:zlib";
2
- import { mkdirSync, writeFileSync } from "node:fs";
3
- import { dirname, join, normalize } from "node:path";
4
-
5
- const MAX_FILES = 200;
6
- const MAX_BYTES = 60 * 1024 * 1024;
7
-
8
- function findEocd(buffer) {
9
- const start = Math.max(0, buffer.length - 22 - 65535);
10
- for (let i = buffer.length - 22; i >= start; i--)
11
- if (buffer.readUInt32LE(i) === 0x06054b50) return i;
12
- return -1;
13
- }
14
-
15
- function safeName(name) {
16
- const text = String(name || "").replace(/\\/g, "/");
17
- if (
18
- !text ||
19
- text.startsWith("/") ||
20
- text.includes("\0") ||
21
- /^[a-z]:/i.test(text) ||
22
- text.split("/").some((part) => part === ".." || part === "")
23
- )
24
- throw new Error("压缩包路径无效");
25
- return text;
26
- }
27
-
28
- // Reads a zip the way a stranger's file has to be read: only stored and
29
- // deflate entries, no encryption, no links, no paths that leave the folder.
30
- export function readZip(buffer) {
31
- if (!Buffer.isBuffer(buffer) || buffer.length < 22)
32
- throw new Error("压缩包无法读取");
33
- const eocd = findEocd(buffer);
34
- if (eocd < 0) throw new Error("压缩包无法读取");
35
- if (buffer.readUInt16LE(eocd + 4) !== 0)
36
- throw new Error("压缩包格式不受支持");
37
- const entries = buffer.readUInt16LE(eocd + 10);
38
- if (!entries || entries > MAX_FILES) throw new Error("压缩包文件过多");
39
- let offset = buffer.readUInt32LE(eocd + 16);
40
- if (offset >= buffer.length) throw new Error("压缩包无法读取");
41
- const files = [];
42
- let total = 0;
43
- for (let index = 0; index < entries; index++) {
44
- if (buffer.readUInt32LE(offset) !== 0x02014b50)
45
- throw new Error("压缩包无法读取");
46
- const flags = buffer.readUInt16LE(offset + 8);
47
- const method = buffer.readUInt16LE(offset + 10);
48
- const compressedSize = buffer.readUInt32LE(offset + 20);
49
- const size = buffer.readUInt32LE(offset + 24);
50
- const nameLength = buffer.readUInt16LE(offset + 28);
51
- const extraLength = buffer.readUInt16LE(offset + 30);
52
- const commentLength = buffer.readUInt16LE(offset + 32);
53
- const attributes = buffer.readUInt32LE(offset + 38);
54
- const local = buffer.readUInt32LE(offset + 42);
55
- const name = buffer
56
- .subarray(offset + 46, offset + 46 + nameLength)
57
- .toString("utf8");
58
- offset += 46 + nameLength + extraLength + commentLength;
59
- if (flags & 1) throw new Error("压缩包不能加密");
60
- if (size === 0xffffffff || compressedSize === 0xffffffff)
61
- throw new Error("压缩包格式不受支持");
62
- if (
63
- (attributes >>> 16) & 0o170000 &&
64
- ((attributes >>> 16) & 0o170000) !== 0o100000 &&
65
- ((attributes >>> 16) & 0o170000) !== 0o040000
66
- )
67
- throw new Error("压缩包不能包含链接");
68
- if (method !== 0 && method !== 8) throw new Error("压缩包格式不受支持");
69
- if (name.endsWith("/") || name.endsWith("\\")) continue;
70
- const fileName = safeName(name);
71
- if (buffer.readUInt32LE(local) !== 0x04034b50)
72
- throw new Error("压缩包无法读取");
73
- const dataAt =
74
- local +
75
- 30 +
76
- buffer.readUInt16LE(local + 26) +
77
- buffer.readUInt16LE(local + 28);
78
- const compressed = buffer.subarray(dataAt, dataAt + compressedSize);
79
- const bytes =
80
- method === 0 ? Buffer.from(compressed) : inflateRawSync(compressed);
81
- if (size && bytes.length !== size) throw new Error("压缩包内容不完整");
82
- total += bytes.length;
83
- if (total > MAX_BYTES) throw new Error("压缩包解压后过大");
84
- files.push({ name: fileName, bytes });
85
- }
86
- return files;
87
- }
88
-
89
- export function stripRoot(files) {
90
- const roots = new Set(files.map((file) => file.name.split("/")[0]));
91
- if (roots.size !== 1) return files;
92
- const root = [...roots][0];
93
- if (files.some((file) => file.name === root)) return files;
94
- return files
95
- .map((file) => ({
96
- ...file,
97
- name: file.name.slice(root.length + 1),
98
- }))
99
- .filter((file) => file.name);
100
- }
101
-
102
- export function writeTree(dir, files) {
103
- for (const file of files) {
104
- const target = join(
105
- dir,
106
- ...normalize(file.name).split(/[\\/]/).filter(Boolean),
107
- );
108
- if (!target.startsWith(dir)) throw new Error("压缩包路径无效");
109
- mkdirSync(dirname(target), { recursive: true });
110
- writeFileSync(target, file.bytes);
111
- }
112
- }
1
+ import { inflateRawSync } from "node:zlib";
2
+ import { mkdirSync, writeFileSync } from "node:fs";
3
+ import { dirname, join, normalize } from "node:path";
4
+
5
+ const MAX_FILES = 200;
6
+ const MAX_BYTES = 60 * 1024 * 1024;
7
+
8
+ function findEocd(buffer) {
9
+ const start = Math.max(0, buffer.length - 22 - 65535);
10
+ for (let i = buffer.length - 22; i >= start; i--)
11
+ if (buffer.readUInt32LE(i) === 0x06054b50) return i;
12
+ return -1;
13
+ }
14
+
15
+ function safeName(name) {
16
+ const text = String(name || "").replace(/\\/g, "/");
17
+ if (
18
+ !text ||
19
+ text.startsWith("/") ||
20
+ text.includes("\0") ||
21
+ /^[a-z]:/i.test(text) ||
22
+ text.split("/").some((part) => part === ".." || part === "")
23
+ )
24
+ throw new Error("压缩包路径无效");
25
+ return text;
26
+ }
27
+
28
+ // Reads a zip the way a stranger's file has to be read: only stored and
29
+ // deflate entries, no encryption, no links, no paths that leave the folder.
30
+ export function readZip(buffer) {
31
+ if (!Buffer.isBuffer(buffer) || buffer.length < 22)
32
+ throw new Error("压缩包无法读取");
33
+ const eocd = findEocd(buffer);
34
+ if (eocd < 0) throw new Error("压缩包无法读取");
35
+ if (buffer.readUInt16LE(eocd + 4) !== 0)
36
+ throw new Error("压缩包格式不受支持");
37
+ const entries = buffer.readUInt16LE(eocd + 10);
38
+ if (!entries || entries > MAX_FILES) throw new Error("压缩包文件过多");
39
+ let offset = buffer.readUInt32LE(eocd + 16);
40
+ if (offset >= buffer.length) throw new Error("压缩包无法读取");
41
+ const files = [];
42
+ let total = 0;
43
+ for (let index = 0; index < entries; index++) {
44
+ if (buffer.readUInt32LE(offset) !== 0x02014b50)
45
+ throw new Error("压缩包无法读取");
46
+ const flags = buffer.readUInt16LE(offset + 8);
47
+ const method = buffer.readUInt16LE(offset + 10);
48
+ const compressedSize = buffer.readUInt32LE(offset + 20);
49
+ const size = buffer.readUInt32LE(offset + 24);
50
+ const nameLength = buffer.readUInt16LE(offset + 28);
51
+ const extraLength = buffer.readUInt16LE(offset + 30);
52
+ const commentLength = buffer.readUInt16LE(offset + 32);
53
+ const attributes = buffer.readUInt32LE(offset + 38);
54
+ const local = buffer.readUInt32LE(offset + 42);
55
+ const name = buffer
56
+ .subarray(offset + 46, offset + 46 + nameLength)
57
+ .toString("utf8");
58
+ offset += 46 + nameLength + extraLength + commentLength;
59
+ if (flags & 1) throw new Error("压缩包不能加密");
60
+ if (size === 0xffffffff || compressedSize === 0xffffffff)
61
+ throw new Error("压缩包格式不受支持");
62
+ if (
63
+ (attributes >>> 16) & 0o170000 &&
64
+ ((attributes >>> 16) & 0o170000) !== 0o100000 &&
65
+ ((attributes >>> 16) & 0o170000) !== 0o040000
66
+ )
67
+ throw new Error("压缩包不能包含链接");
68
+ if (method !== 0 && method !== 8) throw new Error("压缩包格式不受支持");
69
+ if (name.endsWith("/") || name.endsWith("\\")) continue;
70
+ const fileName = safeName(name);
71
+ if (buffer.readUInt32LE(local) !== 0x04034b50)
72
+ throw new Error("压缩包无法读取");
73
+ const dataAt =
74
+ local +
75
+ 30 +
76
+ buffer.readUInt16LE(local + 26) +
77
+ buffer.readUInt16LE(local + 28);
78
+ const compressed = buffer.subarray(dataAt, dataAt + compressedSize);
79
+ const bytes =
80
+ method === 0 ? Buffer.from(compressed) : inflateRawSync(compressed);
81
+ if (size && bytes.length !== size) throw new Error("压缩包内容不完整");
82
+ total += bytes.length;
83
+ if (total > MAX_BYTES) throw new Error("压缩包解压后过大");
84
+ files.push({ name: fileName, bytes });
85
+ }
86
+ return files;
87
+ }
88
+
89
+ export function stripRoot(files) {
90
+ const roots = new Set(files.map((file) => file.name.split("/")[0]));
91
+ if (roots.size !== 1) return files;
92
+ const root = [...roots][0];
93
+ if (files.some((file) => file.name === root)) return files;
94
+ return files
95
+ .map((file) => ({
96
+ ...file,
97
+ name: file.name.slice(root.length + 1),
98
+ }))
99
+ .filter((file) => file.name);
100
+ }
101
+
102
+ export function writeTree(dir, files) {
103
+ for (const file of files) {
104
+ const target = join(
105
+ dir,
106
+ ...normalize(file.name).split(/[\\/]/).filter(Boolean),
107
+ );
108
+ if (!target.startsWith(dir)) throw new Error("压缩包路径无效");
109
+ mkdirSync(dirname(target), { recursive: true });
110
+ writeFileSync(target, file.bytes);
111
+ }
112
+ }
@@ -1,118 +1,118 @@
1
- import {
2
- mkdirSync,
3
- readFileSync,
4
- rmdirSync,
5
- rmSync,
6
- statSync,
7
- writeFileSync,
8
- } from "node:fs";
9
- import { join } from "node:path";
10
- import { runtimePaths } from "./paths.js";
11
-
12
- const file = () => join(runtimePaths().data, "startup.json");
13
- const lock = () => join(runtimePaths().data, "startup.lock");
14
-
15
- function read() {
16
- try {
17
- const info = JSON.parse(readFileSync(file(), "utf8"));
18
- if (!Number.isInteger(info.pid) || info.pid <= 0) return null;
19
- return info;
20
- } catch {
21
- return null;
22
- }
23
- }
24
-
25
- function alive(pid) {
26
- try {
27
- process.kill(pid, 0);
28
- return true;
29
- } catch {
30
- return false;
31
- }
32
- }
33
-
34
- function write(role) {
35
- mkdirSync(runtimePaths().data, { recursive: true, mode: 0o700 });
36
- writeFileSync(
37
- file(),
38
- JSON.stringify({ pid: process.pid, role, at: Date.now() }),
39
- );
40
- }
41
-
42
- function busy() {
43
- const info = read();
44
- if (info && alive(info.pid)) return true;
45
- try {
46
- // Another process may have acquired the directory but not yet written its
47
- // identity. Do not steal that claim during the small handover window.
48
- return !info && Date.now() - statSync(lock()).mtimeMs < 30000;
49
- } catch {
50
- return false;
51
- }
52
- }
53
-
54
- function claim(role) {
55
- mkdirSync(runtimePaths().data, { recursive: true, mode: 0o700 });
56
- if (busy()) return false;
57
- try {
58
- mkdirSync(lock(), { mode: 0o700 });
59
- } catch (error) {
60
- if (error.code !== "EEXIST") throw error;
61
- if (busy()) return false;
62
- // Reclaim a dead owner's claim; the normal concurrent-launch path above
63
- // never removes a directory just acquired by another process.
64
- rmSync(file(), { force: true });
65
- try {
66
- rmdirSync(lock());
67
- } catch (error) {
68
- if (error.code !== "ENOENT") throw error;
69
- }
70
- try {
71
- mkdirSync(lock(), { mode: 0o700 });
72
- } catch (error) {
73
- if (error.code === "EEXIST") return false;
74
- throw error;
75
- }
76
- }
77
- write(role);
78
- return true;
79
- }
80
-
81
- // The launcher holds this while the server is still loading, so a second
82
- // click waits instead of copying the database again.
83
- export function claimLaunch() {
84
- return claim("launch");
85
- }
86
-
87
- // Called before the database is opened. A second server exits instead of
88
- // copying the same file.
89
- export function claimServer() {
90
- const other = read();
91
- if (
92
- other?.role === "launch" &&
93
- other.pid === Number(process.env.LUCKYTRI_LAUNCH_PID)
94
- ) {
95
- write("server");
96
- return;
97
- }
98
- if (!claim("server")) {
99
- console.error("已有一次启动在进行,这次不再重复打开数据库。");
100
- process.exit(1);
101
- }
102
- }
103
-
104
- export function starting() {
105
- return busy();
106
- }
107
-
108
- export function clearStarting() {
109
- const info = read();
110
- if (info?.pid === process.pid) {
111
- rmSync(file(), { force: true });
112
- try {
113
- rmdirSync(lock());
114
- } catch (error) {
115
- if (error.code !== "ENOENT") throw error;
116
- }
117
- }
118
- }
1
+ import {
2
+ mkdirSync,
3
+ readFileSync,
4
+ rmdirSync,
5
+ rmSync,
6
+ statSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { runtimePaths } from "./paths.js";
11
+
12
+ const file = () => join(runtimePaths().data, "startup.json");
13
+ const lock = () => join(runtimePaths().data, "startup.lock");
14
+
15
+ function read() {
16
+ try {
17
+ const info = JSON.parse(readFileSync(file(), "utf8"));
18
+ if (!Number.isInteger(info.pid) || info.pid <= 0) return null;
19
+ return info;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function alive(pid) {
26
+ try {
27
+ process.kill(pid, 0);
28
+ return true;
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function write(role) {
35
+ mkdirSync(runtimePaths().data, { recursive: true, mode: 0o700 });
36
+ writeFileSync(
37
+ file(),
38
+ JSON.stringify({ pid: process.pid, role, at: Date.now() }),
39
+ );
40
+ }
41
+
42
+ function busy() {
43
+ const info = read();
44
+ if (info && alive(info.pid)) return true;
45
+ try {
46
+ // Another process may have acquired the directory but not yet written its
47
+ // identity. Do not steal that claim during the small handover window.
48
+ return !info && Date.now() - statSync(lock()).mtimeMs < 30000;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ function claim(role) {
55
+ mkdirSync(runtimePaths().data, { recursive: true, mode: 0o700 });
56
+ if (busy()) return false;
57
+ try {
58
+ mkdirSync(lock(), { mode: 0o700 });
59
+ } catch (error) {
60
+ if (error.code !== "EEXIST") throw error;
61
+ if (busy()) return false;
62
+ // Reclaim a dead owner's claim; the normal concurrent-launch path above
63
+ // never removes a directory just acquired by another process.
64
+ rmSync(file(), { force: true });
65
+ try {
66
+ rmdirSync(lock());
67
+ } catch (error) {
68
+ if (error.code !== "ENOENT") throw error;
69
+ }
70
+ try {
71
+ mkdirSync(lock(), { mode: 0o700 });
72
+ } catch (error) {
73
+ if (error.code === "EEXIST") return false;
74
+ throw error;
75
+ }
76
+ }
77
+ write(role);
78
+ return true;
79
+ }
80
+
81
+ // The launcher holds this while the server is still loading, so a second
82
+ // click waits instead of copying the database again.
83
+ export function claimLaunch() {
84
+ return claim("launch");
85
+ }
86
+
87
+ // Called before the database is opened. A second server exits instead of
88
+ // copying the same file.
89
+ export function claimServer() {
90
+ const other = read();
91
+ if (
92
+ other?.role === "launch" &&
93
+ other.pid === Number(process.env.LUCKYTRI_LAUNCH_PID)
94
+ ) {
95
+ write("server");
96
+ return;
97
+ }
98
+ if (!claim("server")) {
99
+ console.error("已有一次启动在进行,这次不再重复打开数据库。");
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ export function starting() {
105
+ return busy();
106
+ }
107
+
108
+ export function clearStarting() {
109
+ const info = read();
110
+ if (info?.pid === process.pid) {
111
+ rmSync(file(), { force: true });
112
+ try {
113
+ rmdirSync(lock());
114
+ } catch (error) {
115
+ if (error.code !== "ENOENT") throw error;
116
+ }
117
+ }
118
+ }