luckytri 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/SECURITY.md +71 -0
- package/LICENSE +21 -0
- package/README.en.md +250 -0
- package/README.md +250 -0
- package/bin/luckytri.js +107 -0
- package/docs/brand/banner.png +0 -0
- package/docs/brand/palette.png +0 -0
- package/docs/brand/portrait.png +0 -0
- package/docs/en/CHANGELOG.md +113 -0
- package/docs/en/architecture.md +152 -0
- package/docs/en/chat-style.md +62 -0
- package/docs/en/connect.md +63 -0
- package/docs/en/guide.md +216 -0
- package/docs/en/her-life.md +189 -0
- package/docs/en/npm-release.md +25 -0
- package/docs/en/plugins.md +82 -0
- package/docs/en/recovery.md +28 -0
- package/docs/en/time.md +71 -0
- package/docs/en/webui.md +73 -0
- package/docs/zh/CHANGELOG.md +113 -0
- package/docs/zh/architecture.md +152 -0
- package/docs/zh/chat-style.md +62 -0
- package/docs/zh/connect.md +63 -0
- package/docs/zh/guide.md +216 -0
- package/docs/zh/her-life.md +189 -0
- package/docs/zh/npm-release.md +33 -0
- package/docs/zh/plugins.md +82 -0
- package/docs/zh/recovery.md +28 -0
- package/docs/zh/time.md +71 -0
- package/docs/zh/webui.md +73 -0
- package/package.json +108 -0
- package/plugins/sdk/index.d.ts +155 -0
- package/plugins/sdk/testing.js +58 -0
- package/plugins/weather/index.js +82 -0
- package/plugins/weather/luckytri-plugin.json +29 -0
- package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
- package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
- package/public/app/assets/Empty-CpP5kQHH.js +2 -0
- package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
- package/public/app/assets/Field-D4gY-INU.js +1 -0
- package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
- package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
- package/public/app/assets/LifePage-BUi5euBF.css +1 -0
- package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
- package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
- package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
- package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
- package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
- package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
- package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
- package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
- package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
- package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
- package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
- package/public/app/assets/TimePage-BOms_n4I.css +1 -0
- package/public/app/assets/TimePage-Cw44inM4.js +3 -0
- package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
- package/public/app/assets/index-BheRW-1t.js +4 -0
- package/public/app/assets/index-YDz1ree1.css +1 -0
- package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
- package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
- package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
- package/public/app/assets/media-BzPDBm7e.js +1 -0
- package/public/app/index.html +27 -0
- package/public/guide.en.html +1053 -0
- package/public/guide.html +893 -0
- package/public/plugin-kit/v1/bridge.js +30 -0
- package/public/plugin-kit/v1/kit.css +14 -0
- package/public/tutorial.en.md +214 -0
- package/public/tutorial.md +214 -0
- package/scripts/backup.js +311 -0
- package/scripts/browser.js +41 -0
- package/scripts/launch.js +94 -0
- package/scripts/plugin.js +150 -0
- package/scripts/proxy-config.js +167 -0
- package/scripts/recover.js +26 -0
- package/scripts/runtime.js +25 -0
- package/scripts/service.js +47 -0
- package/scripts/setup.js +50 -0
- package/scripts/start-server.js +41 -0
- package/scripts/stop.js +36 -0
- package/server/app.js +96 -0
- package/server/channels/adapters.js +27 -0
- package/server/channels/contract.js +57 -0
- package/server/channels/hub.js +165 -0
- package/server/channels/index.js +25 -0
- package/server/channels/onebot/adapter.js +288 -0
- package/server/channels/onebot/index.js +336 -0
- package/server/channels/onebot/token.js +3 -0
- package/server/channels/qqbot/adapter.js +253 -0
- package/server/channels/qqbot/api.js +199 -0
- package/server/channels/qqbot/credentials.js +11 -0
- package/server/channels/qqbot/gateway.js +274 -0
- package/server/channels/qqbot/index.js +389 -0
- package/server/channels/segments.js +40 -0
- package/server/channels/session-key.js +153 -0
- package/server/channels/state.js +42 -0
- package/server/core/api.js +564 -0
- package/server/core/capabilities.js +208 -0
- package/server/core/context-builder.js +288 -0
- package/server/core/context-compactor.js +454 -0
- package/server/core/conversation-cues.js +325 -0
- package/server/core/conversation-manager.js +87 -0
- package/server/core/events.js +60 -0
- package/server/core/feedback.js +8 -0
- package/server/core/group-style.js +72 -0
- package/server/core/image-preprocessor.js +208 -0
- package/server/core/initiative-context.js +66 -0
- package/server/core/input-budget.js +32 -0
- package/server/core/local-demo.js +98 -0
- package/server/core/message-manager.js +118 -0
- package/server/core/message-scheduler.js +78 -0
- package/server/core/model-json.js +190 -0
- package/server/core/model-manager.js +1213 -0
- package/server/core/model-presets.js +900 -0
- package/server/core/name-call.js +29 -0
- package/server/core/net-guard.js +49 -0
- package/server/core/network.js +157 -0
- package/server/core/orchestrator.js +569 -0
- package/server/core/perceivers.js +91 -0
- package/server/core/person-name.js +22 -0
- package/server/core/persona-manager.js +243 -0
- package/server/core/reply-delivery.js +393 -0
- package/server/core/reply-target-resolver.js +50 -0
- package/server/core/repository.js +293 -0
- package/server/core/response-generator.js +50 -0
- package/server/core/response-validator.js +508 -0
- package/server/core/retired-prompts.js +51 -0
- package/server/core/sessions.js +109 -0
- package/server/core/speaker-names.js +92 -0
- package/server/core/topic-tracker.js +65 -0
- package/server/core/turn-processor.js +630 -0
- package/server/core/turn.js +236 -0
- package/server/core/vision-manager.js +633 -0
- package/server/http.js +18 -0
- package/server/i18n/decisions.js +206 -0
- package/server/i18n/errors.js +497 -0
- package/server/i18n/index.js +144 -0
- package/server/i18n/labels.js +653 -0
- package/server/i18n/models.js +111 -0
- package/server/index.js +102 -0
- package/server/knowledge/api.js +342 -0
- package/server/knowledge/embedding-profile.js +114 -0
- package/server/knowledge/index.js +13 -0
- package/server/knowledge/manager.js +386 -0
- package/server/knowledge/retrieval.js +93 -0
- package/server/knowledge/schema.js +154 -0
- package/server/knowledge/vector-index.js +90 -0
- package/server/mind/affect.js +265 -0
- package/server/mind/anticipations.js +507 -0
- package/server/mind/api.js +703 -0
- package/server/mind/attention.js +200 -0
- package/server/mind/bonds.js +558 -0
- package/server/mind/budget.js +152 -0
- package/server/mind/clock.js +83 -0
- package/server/mind/context-selection.js +61 -0
- package/server/mind/continuity.js +562 -0
- package/server/mind/conversation-origin.js +175 -0
- package/server/mind/day-planner.js +324 -0
- package/server/mind/days.js +210 -0
- package/server/mind/evidence-read.js +45 -0
- package/server/mind/evidence.js +148 -0
- package/server/mind/faces.js +138 -0
- package/server/mind/guard.js +60 -0
- package/server/mind/index.js +445 -0
- package/server/mind/initiative.js +333 -0
- package/server/mind/life/activities.js +21 -0
- package/server/mind/life/context.js +309 -0
- package/server/mind/life/diary.js +786 -0
- package/server/mind/life/growth.js +252 -0
- package/server/mind/life/index.js +1049 -0
- package/server/mind/life/presence.js +305 -0
- package/server/mind/meetings.js +1069 -0
- package/server/mind/memory-policy.js +48 -0
- package/server/mind/memory.js +888 -0
- package/server/mind/nature.js +262 -0
- package/server/mind/novelty.js +19 -0
- package/server/mind/observations.js +84 -0
- package/server/mind/own-day.js +324 -0
- package/server/mind/own-voice.js +318 -0
- package/server/mind/periods.js +118 -0
- package/server/mind/reading.js +120 -0
- package/server/mind/salience.js +201 -0
- package/server/mind/schema.js +562 -0
- package/server/mind/self.js +484 -0
- package/server/mind/senses.js +53 -0
- package/server/mind/thoughts.js +274 -0
- package/server/mind/time/activity-clock.js +196 -0
- package/server/mind/time/agenda.js +242 -0
- package/server/mind/time/api.js +200 -0
- package/server/mind/time/attention.js +34 -0
- package/server/mind/time/availability.js +69 -0
- package/server/mind/time/commitment-review.js +198 -0
- package/server/mind/time/delivery-links.js +130 -0
- package/server/mind/time/executor.js +512 -0
- package/server/mind/time/experiences.js +128 -0
- package/server/mind/time/games.js +562 -0
- package/server/mind/time/index.js +739 -0
- package/server/mind/time/intent.js +166 -0
- package/server/mind/time/kinds.js +171 -0
- package/server/mind/time/model-material.js +77 -0
- package/server/mind/time/plugin-activity.js +207 -0
- package/server/mind/time/presentation.js +51 -0
- package/server/mind/time/schedule.js +83 -0
- package/server/mind/time/schema.js +84 -0
- package/server/mind/time/search.js +383 -0
- package/server/mind/time/sharing.js +472 -0
- package/server/mind/time/task-links.js +311 -0
- package/server/mind/time/tasks.js +587 -0
- package/server/mind/time/works.js +432 -0
- package/server/mind/traits.js +191 -0
- package/server/mind/util.js +110 -0
- package/server/mind/view.js +273 -0
- package/server/paths.js +102 -0
- package/server/plugins/api.js +204 -0
- package/server/plugins/host.js +967 -0
- package/server/plugins/install.js +295 -0
- package/server/plugins/manifest.js +201 -0
- package/server/plugins/permissions.js +42 -0
- package/server/plugins/runner/guard.mjs +36 -0
- package/server/plugins/runner/worker.mjs +250 -0
- package/server/plugins/schema.js +91 -0
- package/server/plugins/zip.js +112 -0
- package/server/startup-lock.js +118 -0
- package/server/storage/archive.js +241 -0
- package/server/storage/backup-cleanup-worker.js +8 -0
- package/server/storage/backup-cleanup.js +414 -0
- package/server/storage/backup-lock.js +76 -0
- package/server/storage/backup-scheduler.js +131 -0
- package/server/storage/store.js +199 -0
- package/server/studio/display-names.js +138 -0
- package/server/studio/management.js +102 -0
- package/server/studio/readiness.js +190 -0
- package/server/studio/settings.js +236 -0
- package/server/version.js +10 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { lookup } from "node:dns/promises";
|
|
3
|
+
import { cpSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { assertPublicHost } from "../core/net-guard.js";
|
|
6
|
+
import { VERSION } from "../version.js";
|
|
7
|
+
import { readManifest, satisfies, validateManifest } from "./manifest.js";
|
|
8
|
+
import { readZip, stripRoot, writeTree } from "./zip.js";
|
|
9
|
+
|
|
10
|
+
const DOWNLOAD = 20 * 1024 * 1024;
|
|
11
|
+
const STAGED_FOR = 30 * 60000;
|
|
12
|
+
|
|
13
|
+
async function download(url, hops = 0, timeoutMs = 30000) {
|
|
14
|
+
if (hops > 3) throw new Error("跳转过多");
|
|
15
|
+
const parsed = new URL(url);
|
|
16
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password)
|
|
17
|
+
throw new Error("这个地址不能访问");
|
|
18
|
+
await assertPublicHost(parsed.hostname, lookup, "这个地址不能访问");
|
|
19
|
+
let response;
|
|
20
|
+
try {
|
|
21
|
+
response = await fetch(parsed, {
|
|
22
|
+
redirect: "manual",
|
|
23
|
+
headers: { "User-Agent": `LuckyTri/${VERSION}` },
|
|
24
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
25
|
+
});
|
|
26
|
+
} catch {
|
|
27
|
+
throw new Error("插件包下载失败");
|
|
28
|
+
}
|
|
29
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
30
|
+
const next = new URL(response.headers.get("location") || "", parsed).href;
|
|
31
|
+
return download(next, hops + 1, timeoutMs);
|
|
32
|
+
}
|
|
33
|
+
if (!response.ok) throw new Error("插件包下载失败");
|
|
34
|
+
const length = Number(response.headers.get("content-length"));
|
|
35
|
+
if (length > DOWNLOAD) throw new Error("插件包过大");
|
|
36
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
37
|
+
if (bytes.length > DOWNLOAD) throw new Error("插件包过大");
|
|
38
|
+
return bytes;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function preview(bytes, { source, trusted = false, sha256 = "" }) {
|
|
42
|
+
const hash = createHash("sha256").update(bytes).digest("hex");
|
|
43
|
+
if (sha256 && sha256.toLowerCase() !== hash)
|
|
44
|
+
throw new Error("插件包校验不符");
|
|
45
|
+
const files = stripRoot(readZip(bytes));
|
|
46
|
+
const manifestFile = files.find(
|
|
47
|
+
(file) => file.name === "luckytri-plugin.json",
|
|
48
|
+
);
|
|
49
|
+
if (!manifestFile) throw new Error("插件清单无法读取");
|
|
50
|
+
const manifest = validateManifest(
|
|
51
|
+
JSON.parse(manifestFile.bytes.toString("utf8")),
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
token: randomUUID(),
|
|
55
|
+
bytes,
|
|
56
|
+
files,
|
|
57
|
+
manifest,
|
|
58
|
+
sha256: hash,
|
|
59
|
+
source,
|
|
60
|
+
trusted: trusted && !!sha256,
|
|
61
|
+
at: Date.now(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createInstaller(host) {
|
|
66
|
+
const staged = new Map();
|
|
67
|
+
function remember(item) {
|
|
68
|
+
for (const [token, old] of staged)
|
|
69
|
+
if (Date.now() - old.at > STAGED_FOR) staged.delete(token);
|
|
70
|
+
staged.set(item.token, item);
|
|
71
|
+
return describe(item);
|
|
72
|
+
}
|
|
73
|
+
function describe(item) {
|
|
74
|
+
return {
|
|
75
|
+
token: item.token,
|
|
76
|
+
trusted: item.trusted,
|
|
77
|
+
sha256: item.sha256,
|
|
78
|
+
source: item.source,
|
|
79
|
+
manifest: item.manifest,
|
|
80
|
+
permissions: item.manifest.permissions,
|
|
81
|
+
network: item.manifest.network,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
async stageUrl({ url, sha256 = "" }) {
|
|
86
|
+
const bytes = await download(url);
|
|
87
|
+
return remember(
|
|
88
|
+
preview(bytes, { source: "url", sha256, trusted: !!sha256 }),
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
async stageRegistry({ url, sha256, id }) {
|
|
92
|
+
const bytes = await download(url);
|
|
93
|
+
const item = preview(bytes, { source: "market", sha256, trusted: true });
|
|
94
|
+
if (id && item.manifest.id !== id) throw new Error("插件包和索引不一致");
|
|
95
|
+
return remember(item);
|
|
96
|
+
},
|
|
97
|
+
async stageGithub(repository) {
|
|
98
|
+
const parsed = new URL(repository);
|
|
99
|
+
if (parsed.hostname !== "github.com") throw new Error("这个地址不能访问");
|
|
100
|
+
const [owner, repo] = parsed.pathname.split("/").filter(Boolean);
|
|
101
|
+
if (!owner || !repo) throw new Error("插件仓库地址无效");
|
|
102
|
+
const name = repo.replace(/\.git$/, "");
|
|
103
|
+
let bytes;
|
|
104
|
+
try {
|
|
105
|
+
const release = await download(
|
|
106
|
+
`https://api.github.com/repos/${owner}/${name}/releases/latest`,
|
|
107
|
+
);
|
|
108
|
+
const data = JSON.parse(bytesToText(release));
|
|
109
|
+
const asset = (data.assets || []).find((item) =>
|
|
110
|
+
String(item.name || "").endsWith(".zip"),
|
|
111
|
+
);
|
|
112
|
+
bytes = await download(asset?.browser_download_url || data.zipball_url);
|
|
113
|
+
} catch {
|
|
114
|
+
bytes = await download(
|
|
115
|
+
`https://codeload.github.com/${owner}/${name}/zip/refs/heads/main`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return remember(preview(bytes, { source: "github", trusted: false }));
|
|
119
|
+
},
|
|
120
|
+
stageUpload(bytes) {
|
|
121
|
+
return remember(
|
|
122
|
+
preview(Buffer.from(bytes), { source: "upload", trusted: false }),
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
stageDir(dir) {
|
|
126
|
+
const manifest = readManifest(dir);
|
|
127
|
+
const item = {
|
|
128
|
+
token: randomUUID(),
|
|
129
|
+
dir,
|
|
130
|
+
manifest,
|
|
131
|
+
sha256: "",
|
|
132
|
+
source: "dev",
|
|
133
|
+
trusted: false,
|
|
134
|
+
at: Date.now(),
|
|
135
|
+
};
|
|
136
|
+
return remember(item);
|
|
137
|
+
},
|
|
138
|
+
commit(token) {
|
|
139
|
+
const item = staged.get(token);
|
|
140
|
+
if (!item || Date.now() - item.at > STAGED_FOR)
|
|
141
|
+
throw new Error("这次导入已经过期");
|
|
142
|
+
const { manifest } = item;
|
|
143
|
+
const target =
|
|
144
|
+
item.source === "dev"
|
|
145
|
+
? item.dir
|
|
146
|
+
: join(host.dataRoot, manifest.id, manifest.version);
|
|
147
|
+
if (item.source !== "dev") {
|
|
148
|
+
rmSync(target, { recursive: true, force: true });
|
|
149
|
+
mkdirSync(target, { recursive: true });
|
|
150
|
+
writeTree(target, item.files);
|
|
151
|
+
const checked = readManifest(target);
|
|
152
|
+
if (checked.id !== manifest.id || checked.version !== manifest.version)
|
|
153
|
+
throw new Error("插件包和索引不一致");
|
|
154
|
+
}
|
|
155
|
+
host.db
|
|
156
|
+
.prepare(
|
|
157
|
+
`INSERT INTO plugin_installs(id,version,enabled,state,source,sha256,api,permissions,granted,manifest,path,error,installed,updated)
|
|
158
|
+
VALUES (?,?,0,'installed',?,?,?,?,'[]',?,?, '',?,?)
|
|
159
|
+
ON CONFLICT(id) DO UPDATE SET version=excluded.version,source=excluded.source,sha256=excluded.sha256,api=excluded.api,permissions=excluded.permissions,manifest=excluded.manifest,path=excluded.path,error='',updated=excluded.updated`,
|
|
160
|
+
)
|
|
161
|
+
.run(
|
|
162
|
+
manifest.id,
|
|
163
|
+
manifest.version,
|
|
164
|
+
item.source,
|
|
165
|
+
item.sha256,
|
|
166
|
+
manifest.pluginApi,
|
|
167
|
+
JSON.stringify(manifest.permissions),
|
|
168
|
+
JSON.stringify(manifest),
|
|
169
|
+
target,
|
|
170
|
+
host.now(),
|
|
171
|
+
host.now(),
|
|
172
|
+
);
|
|
173
|
+
staged.delete(token);
|
|
174
|
+
host.store.revision++;
|
|
175
|
+
return host.list().find((row) => row.id === manifest.id);
|
|
176
|
+
},
|
|
177
|
+
async registries() {
|
|
178
|
+
const saved = host.db
|
|
179
|
+
.prepare("SELECT id,url FROM plugin_sources ORDER BY created")
|
|
180
|
+
.all();
|
|
181
|
+
if (saved.length) return saved;
|
|
182
|
+
const url =
|
|
183
|
+
"https://raw.githubusercontent.com/TodayYueC/LuckyTri-Plugins/main/index.json";
|
|
184
|
+
return [{ id: "default", url }];
|
|
185
|
+
},
|
|
186
|
+
saveRegistry(url) {
|
|
187
|
+
const parsed = new URL(url);
|
|
188
|
+
if (parsed.protocol !== "https:") throw new Error("这个地址不能访问");
|
|
189
|
+
const id = createHash("sha256").update(url).digest("hex").slice(0, 16);
|
|
190
|
+
host.db
|
|
191
|
+
.prepare(
|
|
192
|
+
"INSERT INTO plugin_sources(id,url,created) VALUES (?,?,?) ON CONFLICT(id) DO UPDATE SET url=excluded.url",
|
|
193
|
+
)
|
|
194
|
+
.run(id, url, host.now());
|
|
195
|
+
return id;
|
|
196
|
+
},
|
|
197
|
+
removeRegistry(id) {
|
|
198
|
+
host.db.prepare("DELETE FROM plugin_sources WHERE id=?").run(id);
|
|
199
|
+
},
|
|
200
|
+
async catalog() {
|
|
201
|
+
const plugins = [];
|
|
202
|
+
let unavailable = false;
|
|
203
|
+
for (const source of await this.registries()) {
|
|
204
|
+
try {
|
|
205
|
+
const bytes = await download(source.url, 0, 8000);
|
|
206
|
+
const index = JSON.parse(bytes.toString("utf8"));
|
|
207
|
+
if (index.format !== 1 || !Array.isArray(index.plugins))
|
|
208
|
+
throw new Error("插件索引无效");
|
|
209
|
+
for (const plugin of index.plugins) {
|
|
210
|
+
const versions = (plugin.versions || []).filter(
|
|
211
|
+
(item) =>
|
|
212
|
+
item.pluginApi &&
|
|
213
|
+
satisfies(item.luckytri || ">=1.0.0 <2.0.0", VERSION),
|
|
214
|
+
);
|
|
215
|
+
if (!versions.length) continue;
|
|
216
|
+
plugins.push({
|
|
217
|
+
...plugin,
|
|
218
|
+
source: source.url,
|
|
219
|
+
versions,
|
|
220
|
+
latest: versions[versions.length - 1],
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
unavailable = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { plugins, unavailable };
|
|
228
|
+
},
|
|
229
|
+
async remove(id, { data = false } = {}) {
|
|
230
|
+
await host.disable(id);
|
|
231
|
+
const row = host.db
|
|
232
|
+
.prepare("SELECT * FROM plugin_installs WHERE id=?")
|
|
233
|
+
.get(id);
|
|
234
|
+
if (row?.source !== "builtin" && row?.source !== "dev" && row?.path)
|
|
235
|
+
rmSync(row.path, { recursive: true, force: true });
|
|
236
|
+
if (data) {
|
|
237
|
+
rmSync(join(host.dataRoot, id, "data"), {
|
|
238
|
+
recursive: true,
|
|
239
|
+
force: true,
|
|
240
|
+
});
|
|
241
|
+
host.db
|
|
242
|
+
.prepare("DELETE FROM plugin_settings WHERE plugin_id=?")
|
|
243
|
+
.run(id);
|
|
244
|
+
host.db.prepare("DELETE FROM plugin_kv WHERE plugin_id=?").run(id);
|
|
245
|
+
}
|
|
246
|
+
if (row?.source !== "builtin")
|
|
247
|
+
host.db.prepare("DELETE FROM plugin_installs WHERE id=?").run(id);
|
|
248
|
+
else
|
|
249
|
+
host.db
|
|
250
|
+
.prepare(
|
|
251
|
+
"UPDATE plugin_installs SET enabled=0,state='installed',granted='[]' WHERE id=?",
|
|
252
|
+
)
|
|
253
|
+
.run(id);
|
|
254
|
+
host.store.revision++;
|
|
255
|
+
return host.list();
|
|
256
|
+
},
|
|
257
|
+
rollback(id) {
|
|
258
|
+
const row = host.db
|
|
259
|
+
.prepare("SELECT * FROM plugin_installs WHERE id=?")
|
|
260
|
+
.get(id);
|
|
261
|
+
if (!row) throw new Error("插件不存在");
|
|
262
|
+
const parent = join(host.dataRoot, id);
|
|
263
|
+
const versions = readdirSync(parent).filter(
|
|
264
|
+
(name) => name !== "data" && name !== row.version,
|
|
265
|
+
);
|
|
266
|
+
const previous = versions.sort().at(-1);
|
|
267
|
+
if (!previous) throw new Error("没有可以退回的版本");
|
|
268
|
+
const dir = join(parent, previous);
|
|
269
|
+
const manifest = readManifest(dir);
|
|
270
|
+
host.db
|
|
271
|
+
.prepare(
|
|
272
|
+
"UPDATE plugin_installs SET version=?,path=?,manifest=?,permissions=?,updated=? WHERE id=?",
|
|
273
|
+
)
|
|
274
|
+
.run(
|
|
275
|
+
manifest.version,
|
|
276
|
+
dir,
|
|
277
|
+
JSON.stringify(manifest),
|
|
278
|
+
JSON.stringify(manifest.permissions),
|
|
279
|
+
host.now(),
|
|
280
|
+
id,
|
|
281
|
+
);
|
|
282
|
+
host.store.revision++;
|
|
283
|
+
return host.list().find((item) => item.id === id);
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function bytesToText(bytes) {
|
|
289
|
+
return Buffer.from(bytes).toString("utf8");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function copyPlugin(from, to) {
|
|
293
|
+
mkdirSync(to, { recursive: true });
|
|
294
|
+
cpSync(from, to, { recursive: true });
|
|
295
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
};
|