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
|
@@ -1,201 +1,201 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join, normalize, sep } from "node:path";
|
|
3
|
-
import { VERSION } from "../version.js";
|
|
4
|
-
import { API_VERSION, permissionKnown } from "./permissions.js";
|
|
5
|
-
|
|
6
|
-
export const RESERVED_IDS = new Set([
|
|
7
|
-
"onebot",
|
|
8
|
-
"qqbot",
|
|
9
|
-
"preview",
|
|
10
|
-
"core",
|
|
11
|
-
"mind",
|
|
12
|
-
"luckytri",
|
|
13
|
-
]);
|
|
14
|
-
const ID = /^[a-z][a-z0-9-]{1,39}$/;
|
|
15
|
-
const SETTING_KEY = /^[a-z][A-Za-z0-9-]{0,39}$/;
|
|
16
|
-
const SEMVER =
|
|
17
|
-
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
18
|
-
const HOST = /^[a-z0-9.-]+$/i;
|
|
19
|
-
|
|
20
|
-
export function parseVersion(value) {
|
|
21
|
-
const match = SEMVER.exec(String(value || ""));
|
|
22
|
-
if (!match) return null;
|
|
23
|
-
return match.slice(1, 4).map(Number);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function satisfies(range, version) {
|
|
27
|
-
const have = parseVersion(version);
|
|
28
|
-
if (!have || typeof range !== "string") return false;
|
|
29
|
-
const parts = range.trim().split(/\s+/);
|
|
30
|
-
return parts.every((part) => {
|
|
31
|
-
const match = /^(>=|>|<=|<)?(\d+\.\d+\.\d+)$/.exec(part);
|
|
32
|
-
if (!match) return false;
|
|
33
|
-
const op = match[1] || "=";
|
|
34
|
-
const want = parseVersion(match[2]);
|
|
35
|
-
const cmp = have[0] - want[0] || have[1] - want[1] || have[2] - want[2];
|
|
36
|
-
if (op === ">=") return cmp >= 0;
|
|
37
|
-
if (op === ">") return cmp > 0;
|
|
38
|
-
if (op === "<=") return cmp <= 0;
|
|
39
|
-
if (op === "<") return cmp < 0;
|
|
40
|
-
return cmp === 0;
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function apiCompatible(wanted, host = API_VERSION) {
|
|
45
|
-
const plugin = parseVersion(`${wanted}.0`);
|
|
46
|
-
const current = parseVersion(`${host}.0`);
|
|
47
|
-
if (!plugin || !current) return false;
|
|
48
|
-
return plugin[0] === current[0] && plugin[1] <= current[1];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function setting(field) {
|
|
52
|
-
const types = [
|
|
53
|
-
"text",
|
|
54
|
-
"secret",
|
|
55
|
-
"number",
|
|
56
|
-
"boolean",
|
|
57
|
-
"select",
|
|
58
|
-
"textarea",
|
|
59
|
-
"urls",
|
|
60
|
-
];
|
|
61
|
-
if (
|
|
62
|
-
!field ||
|
|
63
|
-
!SETTING_KEY.test(field.key || "") ||
|
|
64
|
-
!types.includes(field.type)
|
|
65
|
-
)
|
|
66
|
-
throw Error("插件设置项无效");
|
|
67
|
-
if (typeof field.label !== "string" || !field.label.trim())
|
|
68
|
-
throw Error("插件设置项无效");
|
|
69
|
-
if (field.type === "select" && !Array.isArray(field.options))
|
|
70
|
-
throw Error("插件设置项无效");
|
|
71
|
-
return {
|
|
72
|
-
key: field.key,
|
|
73
|
-
type: field.type,
|
|
74
|
-
label: String(field.label).slice(0, 80),
|
|
75
|
-
required: !!field.required,
|
|
76
|
-
default: field.default ?? (field.type === "boolean" ? false : ""),
|
|
77
|
-
...(field.allowNetwork ? { allowNetwork: true } : {}),
|
|
78
|
-
...(Array.isArray(field.options)
|
|
79
|
-
? {
|
|
80
|
-
options: field.options.slice(0, 20).map((item) => ({
|
|
81
|
-
value: String(item.value ?? item).slice(0, 80),
|
|
82
|
-
label: String(item.label ?? item.value ?? item).slice(0, 80),
|
|
83
|
-
})),
|
|
84
|
-
}
|
|
85
|
-
: {}),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function named(list, pattern, map) {
|
|
90
|
-
if (!Array.isArray(list)) return [];
|
|
91
|
-
return list.map((item) => {
|
|
92
|
-
const value = map(item);
|
|
93
|
-
if (!pattern.test(value.name || value.kind || value.type || ""))
|
|
94
|
-
throw Error("插件贡献的名字无效");
|
|
95
|
-
return value;
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// A manifest is data. Anything that does not fit is refused before the
|
|
100
|
-
// plugin process is ever started.
|
|
101
|
-
export function readManifest(dir) {
|
|
102
|
-
let raw;
|
|
103
|
-
try {
|
|
104
|
-
raw = JSON.parse(readFileSync(join(dir, "luckytri-plugin.json"), "utf8"));
|
|
105
|
-
} catch {
|
|
106
|
-
throw Error("插件清单无法读取");
|
|
107
|
-
}
|
|
108
|
-
return validateManifest(raw);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function validateManifest(raw, version = VERSION) {
|
|
112
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
113
|
-
throw Error("插件清单无效");
|
|
114
|
-
if (!ID.test(raw.id || "") || RESERVED_IDS.has(raw.id))
|
|
115
|
-
throw Error("插件标识无效");
|
|
116
|
-
if (!parseVersion(raw.version)) throw Error("插件版本号无效");
|
|
117
|
-
if (!apiCompatible(raw.pluginApi)) throw Error("插件 API 版本不兼容");
|
|
118
|
-
if (raw.luckytri && !satisfies(raw.luckytri, version))
|
|
119
|
-
throw Error("插件不支持这个 LuckyTri 版本");
|
|
120
|
-
const entry = normalize(String(raw.entry || "index.js"));
|
|
121
|
-
if (
|
|
122
|
-
!entry ||
|
|
123
|
-
entry.startsWith("..") ||
|
|
124
|
-
entry.startsWith("/") ||
|
|
125
|
-
entry.includes(`..${sep}`) ||
|
|
126
|
-
entry.includes("..\\") ||
|
|
127
|
-
entry.includes("../")
|
|
128
|
-
)
|
|
129
|
-
throw Error("插件入口无效");
|
|
130
|
-
const permissions = [...new Set(raw.permissions || [])];
|
|
131
|
-
if (permissions.some((item) => !permissionKnown(item)))
|
|
132
|
-
throw Error("插件权限无效");
|
|
133
|
-
const network = [
|
|
134
|
-
...new Set((raw.network || []).map((host) => String(host).toLowerCase())),
|
|
135
|
-
];
|
|
136
|
-
if (network.some((host) => !HOST.test(host) || host.length > 200))
|
|
137
|
-
throw Error("插件网络主机无效");
|
|
138
|
-
const name = String(raw.name || "")
|
|
139
|
-
.trim()
|
|
140
|
-
.slice(0, 40);
|
|
141
|
-
if (!name) throw Error("插件清单无效");
|
|
142
|
-
const contributes = raw.contributes || {};
|
|
143
|
-
const short = /^[a-z][a-z0-9-]{0,40}$/;
|
|
144
|
-
return {
|
|
145
|
-
id: raw.id,
|
|
146
|
-
name,
|
|
147
|
-
version: raw.version,
|
|
148
|
-
pluginApi: raw.pluginApi,
|
|
149
|
-
luckytri: raw.luckytri || "",
|
|
150
|
-
description: String(raw.description || "").slice(0, 400),
|
|
151
|
-
author: String(raw.author || "").slice(0, 80),
|
|
152
|
-
license: String(raw.license || "").slice(0, 40),
|
|
153
|
-
entry,
|
|
154
|
-
permissions,
|
|
155
|
-
network,
|
|
156
|
-
settings: (raw.settings || []).map(setting),
|
|
157
|
-
contributes: {
|
|
158
|
-
senses: (contributes.senses || []).map((key) => {
|
|
159
|
-
if (!short.test(key)) throw Error("插件贡献的名字无效");
|
|
160
|
-
return key;
|
|
161
|
-
}),
|
|
162
|
-
channels: named(contributes.channels, short, (item) => ({
|
|
163
|
-
type: item.type === raw.id ? item.type : `${raw.id}-${item.type}`,
|
|
164
|
-
label: String(item.label || item.type).slice(0, 40),
|
|
165
|
-
capabilities: item.capabilities || {},
|
|
166
|
-
mediaHosts: String(item.mediaHosts || "").slice(0, 200),
|
|
167
|
-
})),
|
|
168
|
-
activities: (contributes.activities || []).map((item) => {
|
|
169
|
-
if (!short.test(item?.kind || "")) throw Error("插件贡献的名字无效");
|
|
170
|
-
return {
|
|
171
|
-
kind: `${raw.id}.${item.kind}`,
|
|
172
|
-
label: String(item.label || item.kind).slice(0, 40),
|
|
173
|
-
describe: String(item.describe || "").slice(0, 160),
|
|
174
|
-
keywords: String(item.keywords || "").slice(0, 200),
|
|
175
|
-
minutes: Number(item.minutes) || 15,
|
|
176
|
-
energy: !!item.energy,
|
|
177
|
-
};
|
|
178
|
-
}),
|
|
179
|
-
actions: (contributes.actions || []).map((item) => {
|
|
180
|
-
if (!short.test(item?.name || "")) throw Error("插件贡献的名字无效");
|
|
181
|
-
return {
|
|
182
|
-
name: `${raw.id}.${item.name}`,
|
|
183
|
-
label: String(item.label || item.name).slice(0, 40),
|
|
184
|
-
describe: String(item.describe || "").slice(0, 160),
|
|
185
|
-
input: Array.isArray(item.input) ? item.input.slice(0, 12) : [],
|
|
186
|
-
risk: item.risk === "high" ? "high" : "low",
|
|
187
|
-
confirm: item.confirm === "owner" ? "owner" : "none",
|
|
188
|
-
where: item.where === "any" ? "any" : "private",
|
|
189
|
-
dailyLimit: Number.isInteger(item.dailyLimit) ? item.dailyLimit : 20,
|
|
190
|
-
};
|
|
191
|
-
}),
|
|
192
|
-
perceive: (contributes.perceive || []).map((type) => {
|
|
193
|
-
const value = String(type).slice(0, 40);
|
|
194
|
-
if (!value) throw Error("插件贡献的名字无效");
|
|
195
|
-
return value;
|
|
196
|
-
}),
|
|
197
|
-
page: !!contributes.ui?.page,
|
|
198
|
-
},
|
|
199
|
-
i18n: raw.i18n && typeof raw.i18n === "object" ? raw.i18n : {},
|
|
200
|
-
};
|
|
201
|
-
}
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join, normalize, sep } from "node:path";
|
|
3
|
+
import { VERSION } from "../version.js";
|
|
4
|
+
import { API_VERSION, permissionKnown } from "./permissions.js";
|
|
5
|
+
|
|
6
|
+
export const RESERVED_IDS = new Set([
|
|
7
|
+
"onebot",
|
|
8
|
+
"qqbot",
|
|
9
|
+
"preview",
|
|
10
|
+
"core",
|
|
11
|
+
"mind",
|
|
12
|
+
"luckytri",
|
|
13
|
+
]);
|
|
14
|
+
const ID = /^[a-z][a-z0-9-]{1,39}$/;
|
|
15
|
+
const SETTING_KEY = /^[a-z][A-Za-z0-9-]{0,39}$/;
|
|
16
|
+
const SEMVER =
|
|
17
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
18
|
+
const HOST = /^[a-z0-9.-]+$/i;
|
|
19
|
+
|
|
20
|
+
export function parseVersion(value) {
|
|
21
|
+
const match = SEMVER.exec(String(value || ""));
|
|
22
|
+
if (!match) return null;
|
|
23
|
+
return match.slice(1, 4).map(Number);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function satisfies(range, version) {
|
|
27
|
+
const have = parseVersion(version);
|
|
28
|
+
if (!have || typeof range !== "string") return false;
|
|
29
|
+
const parts = range.trim().split(/\s+/);
|
|
30
|
+
return parts.every((part) => {
|
|
31
|
+
const match = /^(>=|>|<=|<)?(\d+\.\d+\.\d+)$/.exec(part);
|
|
32
|
+
if (!match) return false;
|
|
33
|
+
const op = match[1] || "=";
|
|
34
|
+
const want = parseVersion(match[2]);
|
|
35
|
+
const cmp = have[0] - want[0] || have[1] - want[1] || have[2] - want[2];
|
|
36
|
+
if (op === ">=") return cmp >= 0;
|
|
37
|
+
if (op === ">") return cmp > 0;
|
|
38
|
+
if (op === "<=") return cmp <= 0;
|
|
39
|
+
if (op === "<") return cmp < 0;
|
|
40
|
+
return cmp === 0;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function apiCompatible(wanted, host = API_VERSION) {
|
|
45
|
+
const plugin = parseVersion(`${wanted}.0`);
|
|
46
|
+
const current = parseVersion(`${host}.0`);
|
|
47
|
+
if (!plugin || !current) return false;
|
|
48
|
+
return plugin[0] === current[0] && plugin[1] <= current[1];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setting(field) {
|
|
52
|
+
const types = [
|
|
53
|
+
"text",
|
|
54
|
+
"secret",
|
|
55
|
+
"number",
|
|
56
|
+
"boolean",
|
|
57
|
+
"select",
|
|
58
|
+
"textarea",
|
|
59
|
+
"urls",
|
|
60
|
+
];
|
|
61
|
+
if (
|
|
62
|
+
!field ||
|
|
63
|
+
!SETTING_KEY.test(field.key || "") ||
|
|
64
|
+
!types.includes(field.type)
|
|
65
|
+
)
|
|
66
|
+
throw Error("插件设置项无效");
|
|
67
|
+
if (typeof field.label !== "string" || !field.label.trim())
|
|
68
|
+
throw Error("插件设置项无效");
|
|
69
|
+
if (field.type === "select" && !Array.isArray(field.options))
|
|
70
|
+
throw Error("插件设置项无效");
|
|
71
|
+
return {
|
|
72
|
+
key: field.key,
|
|
73
|
+
type: field.type,
|
|
74
|
+
label: String(field.label).slice(0, 80),
|
|
75
|
+
required: !!field.required,
|
|
76
|
+
default: field.default ?? (field.type === "boolean" ? false : ""),
|
|
77
|
+
...(field.allowNetwork ? { allowNetwork: true } : {}),
|
|
78
|
+
...(Array.isArray(field.options)
|
|
79
|
+
? {
|
|
80
|
+
options: field.options.slice(0, 20).map((item) => ({
|
|
81
|
+
value: String(item.value ?? item).slice(0, 80),
|
|
82
|
+
label: String(item.label ?? item.value ?? item).slice(0, 80),
|
|
83
|
+
})),
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function named(list, pattern, map) {
|
|
90
|
+
if (!Array.isArray(list)) return [];
|
|
91
|
+
return list.map((item) => {
|
|
92
|
+
const value = map(item);
|
|
93
|
+
if (!pattern.test(value.name || value.kind || value.type || ""))
|
|
94
|
+
throw Error("插件贡献的名字无效");
|
|
95
|
+
return value;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// A manifest is data. Anything that does not fit is refused before the
|
|
100
|
+
// plugin process is ever started.
|
|
101
|
+
export function readManifest(dir) {
|
|
102
|
+
let raw;
|
|
103
|
+
try {
|
|
104
|
+
raw = JSON.parse(readFileSync(join(dir, "luckytri-plugin.json"), "utf8"));
|
|
105
|
+
} catch {
|
|
106
|
+
throw Error("插件清单无法读取");
|
|
107
|
+
}
|
|
108
|
+
return validateManifest(raw);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function validateManifest(raw, version = VERSION) {
|
|
112
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
113
|
+
throw Error("插件清单无效");
|
|
114
|
+
if (!ID.test(raw.id || "") || RESERVED_IDS.has(raw.id))
|
|
115
|
+
throw Error("插件标识无效");
|
|
116
|
+
if (!parseVersion(raw.version)) throw Error("插件版本号无效");
|
|
117
|
+
if (!apiCompatible(raw.pluginApi)) throw Error("插件 API 版本不兼容");
|
|
118
|
+
if (raw.luckytri && !satisfies(raw.luckytri, version))
|
|
119
|
+
throw Error("插件不支持这个 LuckyTri 版本");
|
|
120
|
+
const entry = normalize(String(raw.entry || "index.js"));
|
|
121
|
+
if (
|
|
122
|
+
!entry ||
|
|
123
|
+
entry.startsWith("..") ||
|
|
124
|
+
entry.startsWith("/") ||
|
|
125
|
+
entry.includes(`..${sep}`) ||
|
|
126
|
+
entry.includes("..\\") ||
|
|
127
|
+
entry.includes("../")
|
|
128
|
+
)
|
|
129
|
+
throw Error("插件入口无效");
|
|
130
|
+
const permissions = [...new Set(raw.permissions || [])];
|
|
131
|
+
if (permissions.some((item) => !permissionKnown(item)))
|
|
132
|
+
throw Error("插件权限无效");
|
|
133
|
+
const network = [
|
|
134
|
+
...new Set((raw.network || []).map((host) => String(host).toLowerCase())),
|
|
135
|
+
];
|
|
136
|
+
if (network.some((host) => !HOST.test(host) || host.length > 200))
|
|
137
|
+
throw Error("插件网络主机无效");
|
|
138
|
+
const name = String(raw.name || "")
|
|
139
|
+
.trim()
|
|
140
|
+
.slice(0, 40);
|
|
141
|
+
if (!name) throw Error("插件清单无效");
|
|
142
|
+
const contributes = raw.contributes || {};
|
|
143
|
+
const short = /^[a-z][a-z0-9-]{0,40}$/;
|
|
144
|
+
return {
|
|
145
|
+
id: raw.id,
|
|
146
|
+
name,
|
|
147
|
+
version: raw.version,
|
|
148
|
+
pluginApi: raw.pluginApi,
|
|
149
|
+
luckytri: raw.luckytri || "",
|
|
150
|
+
description: String(raw.description || "").slice(0, 400),
|
|
151
|
+
author: String(raw.author || "").slice(0, 80),
|
|
152
|
+
license: String(raw.license || "").slice(0, 40),
|
|
153
|
+
entry,
|
|
154
|
+
permissions,
|
|
155
|
+
network,
|
|
156
|
+
settings: (raw.settings || []).map(setting),
|
|
157
|
+
contributes: {
|
|
158
|
+
senses: (contributes.senses || []).map((key) => {
|
|
159
|
+
if (!short.test(key)) throw Error("插件贡献的名字无效");
|
|
160
|
+
return key;
|
|
161
|
+
}),
|
|
162
|
+
channels: named(contributes.channels, short, (item) => ({
|
|
163
|
+
type: item.type === raw.id ? item.type : `${raw.id}-${item.type}`,
|
|
164
|
+
label: String(item.label || item.type).slice(0, 40),
|
|
165
|
+
capabilities: item.capabilities || {},
|
|
166
|
+
mediaHosts: String(item.mediaHosts || "").slice(0, 200),
|
|
167
|
+
})),
|
|
168
|
+
activities: (contributes.activities || []).map((item) => {
|
|
169
|
+
if (!short.test(item?.kind || "")) throw Error("插件贡献的名字无效");
|
|
170
|
+
return {
|
|
171
|
+
kind: `${raw.id}.${item.kind}`,
|
|
172
|
+
label: String(item.label || item.kind).slice(0, 40),
|
|
173
|
+
describe: String(item.describe || "").slice(0, 160),
|
|
174
|
+
keywords: String(item.keywords || "").slice(0, 200),
|
|
175
|
+
minutes: Number(item.minutes) || 15,
|
|
176
|
+
energy: !!item.energy,
|
|
177
|
+
};
|
|
178
|
+
}),
|
|
179
|
+
actions: (contributes.actions || []).map((item) => {
|
|
180
|
+
if (!short.test(item?.name || "")) throw Error("插件贡献的名字无效");
|
|
181
|
+
return {
|
|
182
|
+
name: `${raw.id}.${item.name}`,
|
|
183
|
+
label: String(item.label || item.name).slice(0, 40),
|
|
184
|
+
describe: String(item.describe || "").slice(0, 160),
|
|
185
|
+
input: Array.isArray(item.input) ? item.input.slice(0, 12) : [],
|
|
186
|
+
risk: item.risk === "high" ? "high" : "low",
|
|
187
|
+
confirm: item.confirm === "owner" ? "owner" : "none",
|
|
188
|
+
where: item.where === "any" ? "any" : "private",
|
|
189
|
+
dailyLimit: Number.isInteger(item.dailyLimit) ? item.dailyLimit : 20,
|
|
190
|
+
};
|
|
191
|
+
}),
|
|
192
|
+
perceive: (contributes.perceive || []).map((type) => {
|
|
193
|
+
const value = String(type).slice(0, 40);
|
|
194
|
+
if (!value) throw Error("插件贡献的名字无效");
|
|
195
|
+
return value;
|
|
196
|
+
}),
|
|
197
|
+
page: !!contributes.ui?.page,
|
|
198
|
+
},
|
|
199
|
+
i18n: raw.i18n && typeof raw.i18n === "object" ? raw.i18n : {},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
// What a plugin may be given. The risk is shown when the owner agrees;
|
|
2
|
-
// the host is what actually refuses a call the plugin was not given.
|
|
3
|
-
export const PERMISSIONS = {
|
|
4
|
-
"mind.read": {
|
|
5
|
-
risk: "low",
|
|
6
|
-
label: "知道她醒着没有、心情和在做什么",
|
|
7
|
-
},
|
|
8
|
-
notify: { risk: "low", label: "给工作室发一条通知" },
|
|
9
|
-
ui: { risk: "low", label: "在工作室里放一个自己的页面" },
|
|
10
|
-
"http.routes": { risk: "low", label: "给自己的页面提供接口" },
|
|
11
|
-
senses: { risk: "medium", label: "把感知放进她的此刻" },
|
|
12
|
-
observe: { risk: "medium", label: "把发生的事作为经历交给她" },
|
|
13
|
-
"knowledge.offer": { risk: "medium", label: "往她的书架放资料" },
|
|
14
|
-
activities: { risk: "medium", label: "给她一件可以自己做的事" },
|
|
15
|
-
models: { risk: "medium", label: "使用她的模型服务" },
|
|
16
|
-
"net.fetch": { risk: "medium", label: "访问声明过的网站" },
|
|
17
|
-
actions: { risk: "high", label: "让她可以选择做一件事" },
|
|
18
|
-
perceive: { risk: "high", label: "读取聊天里的附件" },
|
|
19
|
-
channel: { risk: "high", label: "接入一个平台,并看到那里的消息" },
|
|
20
|
-
"http.public": { risk: "high", label: "开放一个不需要令牌的地址" },
|
|
21
|
-
"net.raw": { risk: "high", label: "自己直接联网" },
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export const API_VERSION = "1.0";
|
|
25
|
-
export const API_FEATURES = [
|
|
26
|
-
"senses",
|
|
27
|
-
"observe",
|
|
28
|
-
"activities",
|
|
29
|
-
"actions",
|
|
30
|
-
"perceive",
|
|
31
|
-
"channel",
|
|
32
|
-
"knowledge",
|
|
33
|
-
"models",
|
|
34
|
-
"http",
|
|
35
|
-
"notify",
|
|
36
|
-
"mind.read",
|
|
37
|
-
"ui",
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
export function permissionKnown(name) {
|
|
41
|
-
return Object.hasOwn(PERMISSIONS, name);
|
|
42
|
-
}
|
|
1
|
+
// What a plugin may be given. The risk is shown when the owner agrees;
|
|
2
|
+
// the host is what actually refuses a call the plugin was not given.
|
|
3
|
+
export const PERMISSIONS = {
|
|
4
|
+
"mind.read": {
|
|
5
|
+
risk: "low",
|
|
6
|
+
label: "知道她醒着没有、心情和在做什么",
|
|
7
|
+
},
|
|
8
|
+
notify: { risk: "low", label: "给工作室发一条通知" },
|
|
9
|
+
ui: { risk: "low", label: "在工作室里放一个自己的页面" },
|
|
10
|
+
"http.routes": { risk: "low", label: "给自己的页面提供接口" },
|
|
11
|
+
senses: { risk: "medium", label: "把感知放进她的此刻" },
|
|
12
|
+
observe: { risk: "medium", label: "把发生的事作为经历交给她" },
|
|
13
|
+
"knowledge.offer": { risk: "medium", label: "往她的书架放资料" },
|
|
14
|
+
activities: { risk: "medium", label: "给她一件可以自己做的事" },
|
|
15
|
+
models: { risk: "medium", label: "使用她的模型服务" },
|
|
16
|
+
"net.fetch": { risk: "medium", label: "访问声明过的网站" },
|
|
17
|
+
actions: { risk: "high", label: "让她可以选择做一件事" },
|
|
18
|
+
perceive: { risk: "high", label: "读取聊天里的附件" },
|
|
19
|
+
channel: { risk: "high", label: "接入一个平台,并看到那里的消息" },
|
|
20
|
+
"http.public": { risk: "high", label: "开放一个不需要令牌的地址" },
|
|
21
|
+
"net.raw": { risk: "high", label: "自己直接联网" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const API_VERSION = "1.0";
|
|
25
|
+
export const API_FEATURES = [
|
|
26
|
+
"senses",
|
|
27
|
+
"observe",
|
|
28
|
+
"activities",
|
|
29
|
+
"actions",
|
|
30
|
+
"perceive",
|
|
31
|
+
"channel",
|
|
32
|
+
"knowledge",
|
|
33
|
+
"models",
|
|
34
|
+
"http",
|
|
35
|
+
"notify",
|
|
36
|
+
"mind.read",
|
|
37
|
+
"ui",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export function permissionKnown(name) {
|
|
41
|
+
return Object.hasOwn(PERMISSIONS, name);
|
|
42
|
+
}
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import { registerHooks } from "node:module";
|
|
2
|
-
|
|
3
|
-
// A plugin process may use the language. It may not open the database, start
|
|
4
|
-
// another process, or reach the network unless it was explicitly allowed to.
|
|
5
|
-
const raw = process.env.LUCKYTRI_NET_RAW === "1";
|
|
6
|
-
const blocked = new Set([
|
|
7
|
-
"sqlite",
|
|
8
|
-
"child_process",
|
|
9
|
-
"worker_threads",
|
|
10
|
-
"cluster",
|
|
11
|
-
"inspector",
|
|
12
|
-
...(raw
|
|
13
|
-
? []
|
|
14
|
-
: ["net", "http", "https", "tls", "dgram", "dns", "http2", "tls"]),
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
function denied(specifier) {
|
|
18
|
-
const name = String(specifier || "").replace(/^node:/, "");
|
|
19
|
-
const root = name.split("/")[0];
|
|
20
|
-
return blocked.has(name) || blocked.has(root);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
registerHooks({
|
|
24
|
-
resolve(specifier, context, next) {
|
|
25
|
-
if (denied(specifier)) {
|
|
26
|
-
const error = new Error("插件没有这个能力");
|
|
27
|
-
error.code = "ERR_PLUGIN_DENIED";
|
|
28
|
-
throw error;
|
|
29
|
-
}
|
|
30
|
-
return next(specifier, context);
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
process.kill = () => {
|
|
35
|
-
throw new Error("插件不能结束别的进程");
|
|
36
|
-
};
|
|
1
|
+
import { registerHooks } from "node:module";
|
|
2
|
+
|
|
3
|
+
// A plugin process may use the language. It may not open the database, start
|
|
4
|
+
// another process, or reach the network unless it was explicitly allowed to.
|
|
5
|
+
const raw = process.env.LUCKYTRI_NET_RAW === "1";
|
|
6
|
+
const blocked = new Set([
|
|
7
|
+
"sqlite",
|
|
8
|
+
"child_process",
|
|
9
|
+
"worker_threads",
|
|
10
|
+
"cluster",
|
|
11
|
+
"inspector",
|
|
12
|
+
...(raw
|
|
13
|
+
? []
|
|
14
|
+
: ["net", "http", "https", "tls", "dgram", "dns", "http2", "tls"]),
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function denied(specifier) {
|
|
18
|
+
const name = String(specifier || "").replace(/^node:/, "");
|
|
19
|
+
const root = name.split("/")[0];
|
|
20
|
+
return blocked.has(name) || blocked.has(root);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
registerHooks({
|
|
24
|
+
resolve(specifier, context, next) {
|
|
25
|
+
if (denied(specifier)) {
|
|
26
|
+
const error = new Error("插件没有这个能力");
|
|
27
|
+
error.code = "ERR_PLUGIN_DENIED";
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
return next(specifier, context);
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
process.kill = () => {
|
|
35
|
+
throw new Error("插件不能结束别的进程");
|
|
36
|
+
};
|