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.
- package/.github/SECURITY.md +4 -4
- package/README.en.md +252 -250
- package/README.md +252 -250
- package/bin/luckytri.js +5 -1
- package/docs/en/CHANGELOG.md +8 -0
- package/docs/en/connect.md +18 -18
- package/docs/en/guide.md +202 -216
- package/docs/en/npm-release.md +25 -25
- package/docs/zh/CHANGELOG.md +8 -0
- package/docs/zh/connect.md +17 -17
- package/docs/zh/guide.md +202 -216
- package/docs/zh/npm-release.md +33 -33
- package/package.json +110 -108
- package/plugins/sdk/index.d.ts +155 -155
- package/plugins/sdk/testing.js +58 -58
- package/plugins/weather/index.js +82 -82
- package/plugins/weather/luckytri-plugin.json +29 -29
- package/public/app/assets/ChatsPage-BhlJHseG.js +3 -0
- package/public/app/assets/Empty-DP6WMGHi.js +2 -0
- package/public/app/assets/Field-CtbdqaiB.js +1 -0
- package/public/app/assets/HeartPage-32BeTPn2.js +1 -0
- package/public/app/assets/LifePage-CxSm0XKI.js +1 -0
- package/public/app/assets/MemoryPage-BeKBcPCY.js +1 -0
- package/public/app/assets/NaturePage-NEPYl2HS.js +3 -0
- package/public/app/assets/PeoplePage-DxtpekwM.js +1 -0
- package/public/app/assets/PluginsPage-CVVQwEBI.css +1 -0
- package/public/app/assets/PluginsPage-DksEZbUU.js +2 -0
- package/public/app/assets/{SystemPage-CAMPoYqM.css → SystemPage-BkRu0Rvx.css} +1 -1
- package/public/app/assets/SystemPage-BvzJlHuY.js +1 -0
- package/public/app/assets/TimePage-7-awH5XL.js +3 -0
- package/public/app/assets/index-4rItIkD-.js +4 -0
- package/public/app/assets/{index-YDz1ree1.css → index-DY4e15Kh.css} +1 -1
- package/public/app/assets/{knowledge-DG0e0uKw.js → knowledge-C6_HvoyB.js} +1 -1
- package/public/app/assets/media-SMfTT-yc.js +1 -0
- package/public/app/index.html +3 -3
- package/public/guide.en.html +72 -99
- package/public/guide.html +44 -86
- package/public/tutorial.en.md +25 -39
- package/public/tutorial.md +25 -39
- package/scripts/backup.js +311 -311
- package/scripts/browser.js +41 -41
- package/scripts/import-data.js +128 -0
- package/scripts/launch.js +101 -94
- package/scripts/plugin.js +150 -150
- package/scripts/reset-password.js +22 -0
- package/scripts/runtime.js +25 -25
- package/scripts/service.js +56 -47
- package/scripts/setup.js +50 -50
- package/scripts/start-server.js +41 -41
- package/scripts/stop.js +36 -36
- package/server/app.js +112 -96
- package/server/auth.js +217 -0
- package/server/channels/contract.js +57 -57
- package/server/channels/hub.js +165 -165
- package/server/channels/index.js +28 -25
- package/server/channels/onebot/index.js +335 -336
- package/server/channels/onebot/token.js +22 -3
- package/server/core/capabilities.js +208 -208
- package/server/core/events.js +64 -60
- package/server/core/image-preprocessor.js +208 -208
- package/server/core/model-manager.js +1213 -1213
- package/server/core/net-guard.js +49 -49
- package/server/core/orchestrator.js +569 -569
- package/server/core/perceivers.js +91 -91
- package/server/core/persona-manager.js +243 -243
- package/server/core/repository.js +293 -293
- package/server/core/response-validator.js +508 -508
- package/server/core/turn-processor.js +630 -630
- package/server/core/turn.js +236 -236
- package/server/core/vision-manager.js +633 -633
- package/server/i18n/errors.js +548 -497
- package/server/i18n/labels.js +649 -653
- package/server/index.js +151 -102
- package/server/knowledge/manager.js +386 -386
- package/server/knowledge/schema.js +12 -8
- package/server/local-access.js +32 -0
- package/server/mind/anticipations.js +507 -507
- package/server/mind/api.js +703 -703
- package/server/mind/budget.js +152 -152
- package/server/mind/context-selection.js +61 -61
- package/server/mind/day-planner.js +324 -324
- package/server/mind/evidence.js +148 -148
- package/server/mind/index.js +445 -445
- package/server/mind/life/diary.js +786 -786
- package/server/mind/life/index.js +1049 -1049
- package/server/mind/observations.js +84 -84
- package/server/mind/own-day.js +324 -324
- package/server/mind/own-voice.js +318 -318
- package/server/mind/senses.js +53 -53
- package/server/mind/time/activity-clock.js +196 -196
- package/server/mind/time/availability.js +69 -69
- package/server/mind/time/commitment-review.js +198 -198
- package/server/mind/time/executor.js +512 -512
- package/server/mind/time/experiences.js +128 -128
- package/server/mind/time/games.js +562 -562
- package/server/mind/time/index.js +739 -739
- package/server/mind/time/intent.js +166 -166
- package/server/mind/time/kinds.js +171 -171
- package/server/mind/time/plugin-activity.js +207 -207
- package/server/mind/time/tasks.js +587 -587
- package/server/mind/util.js +110 -110
- package/server/mind/view.js +273 -273
- package/server/paths.js +102 -102
- package/server/plugins/api.js +204 -204
- package/server/plugins/host.js +967 -967
- package/server/plugins/install.js +295 -295
- package/server/plugins/manifest.js +201 -201
- package/server/plugins/permissions.js +42 -42
- package/server/plugins/runner/guard.mjs +36 -36
- package/server/plugins/runner/worker.mjs +250 -250
- package/server/plugins/schema.js +91 -91
- package/server/plugins/zip.js +112 -112
- package/server/startup-lock.js +118 -118
- package/server/storage/archive.js +242 -241
- package/server/storage/backup-cleanup-worker.js +8 -8
- package/server/storage/backup-cleanup.js +415 -414
- package/server/storage/backup-lock.js +76 -76
- package/server/storage/backup-scheduler.js +131 -131
- package/server/storage/data-package.js +298 -0
- package/server/storage/data-transfer-api.js +50 -0
- package/server/storage/data-transfer-worker.js +18 -0
- package/server/storage/data-transfer.js +415 -0
- package/server/storage/store.js +202 -199
- package/server/studio/readiness.js +181 -190
- package/server/studio/settings.js +236 -236
- package/public/app/assets/ChatsPage-C-FcRapR.js +0 -3
- package/public/app/assets/Empty-CpP5kQHH.js +0 -2
- package/public/app/assets/Field-D4gY-INU.js +0 -1
- package/public/app/assets/HeartPage-CPy_thQd.js +0 -1
- package/public/app/assets/LifePage-CxWvwmSf.js +0 -1
- package/public/app/assets/MemoryPage-DC4rhQ68.js +0 -1
- package/public/app/assets/NaturePage-IvYw04Zq.js +0 -3
- package/public/app/assets/PeoplePage-Cqefm9-q.js +0 -1
- package/public/app/assets/PluginsPage-DKSaKvXb.js +0 -2
- package/public/app/assets/PluginsPage-HyBe5D8w.css +0 -1
- package/public/app/assets/SystemPage-CJmtJkrK.js +0 -1
- package/public/app/assets/TimePage-Cw44inM4.js +0 -3
- package/public/app/assets/index-BheRW-1t.js +0 -4
- package/public/app/assets/media-BzPDBm7e.js +0 -1
package/server/plugins/schema.js
CHANGED
|
@@ -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
|
+
}
|
package/server/plugins/zip.js
CHANGED
|
@@ -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
|
+
}
|
package/server/startup-lock.js
CHANGED
|
@@ -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
|
+
}
|