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,967 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { lookup } from "node:dns/promises";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
readFileSync,
|
|
9
|
+
statSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { defineChannel } from "../channels/contract.js";
|
|
14
|
+
import { formatSessionKey } from "../channels/session-key.js";
|
|
15
|
+
import { withFallback } from "../core/model-manager.js";
|
|
16
|
+
import { assertPublicHost } from "../core/net-guard.js";
|
|
17
|
+
import { text } from "../mind/util.js";
|
|
18
|
+
import {
|
|
19
|
+
registerActivity,
|
|
20
|
+
setActivityEnabled,
|
|
21
|
+
unregisterActivities,
|
|
22
|
+
wakeActivities,
|
|
23
|
+
} from "../mind/time/kinds.js";
|
|
24
|
+
import { API_FEATURES, PERMISSIONS } from "./permissions.js";
|
|
25
|
+
import { readManifest } from "./manifest.js";
|
|
26
|
+
import { createInstaller } from "./install.js";
|
|
27
|
+
import { runtimePaths } from "../paths.js";
|
|
28
|
+
|
|
29
|
+
const RUNNER = fileURLToPath(new URL("./runner/worker.mjs", import.meta.url));
|
|
30
|
+
const ROOT = resolve(fileURLToPath(new URL("../../", import.meta.url)));
|
|
31
|
+
const BACKOFF = [5000, 30000, 120000];
|
|
32
|
+
const LIMIT = 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
function rowOf(db, id) {
|
|
35
|
+
return db.prepare("SELECT * FROM plugin_installs WHERE id=?").get(id);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function decode(row) {
|
|
39
|
+
if (!row) return null;
|
|
40
|
+
return {
|
|
41
|
+
...row,
|
|
42
|
+
enabled: !!row.enabled,
|
|
43
|
+
permissions: JSON.parse(row.permissions || "[]"),
|
|
44
|
+
granted: JSON.parse(row.granted || "[]"),
|
|
45
|
+
manifest: JSON.parse(row.manifest || "{}"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class PluginHost {
|
|
50
|
+
constructor({
|
|
51
|
+
store,
|
|
52
|
+
chat,
|
|
53
|
+
channel,
|
|
54
|
+
life,
|
|
55
|
+
root = ROOT,
|
|
56
|
+
dataRoot = runtimePaths().plugins,
|
|
57
|
+
now = () => Date.now(),
|
|
58
|
+
}) {
|
|
59
|
+
this.store = store;
|
|
60
|
+
this.db = store.db;
|
|
61
|
+
this.chat = chat;
|
|
62
|
+
this.channel = channel;
|
|
63
|
+
this.life = life;
|
|
64
|
+
this.root = root;
|
|
65
|
+
this.dataRoot = dataRoot;
|
|
66
|
+
this.now = now;
|
|
67
|
+
this.procs = new Map();
|
|
68
|
+
this.routes = [];
|
|
69
|
+
this.closed = false;
|
|
70
|
+
this.seen = { phase: "", mood: "", activity: "" };
|
|
71
|
+
this.install = createInstaller(this);
|
|
72
|
+
}
|
|
73
|
+
// What she herself can tell about this system. The studio reads the same rows.
|
|
74
|
+
aware() {
|
|
75
|
+
const lines = [];
|
|
76
|
+
try {
|
|
77
|
+
const link = this.channel?.status?.();
|
|
78
|
+
if (link && "online" in link)
|
|
79
|
+
lines.push(link.online ? "QQ 接上了" : "QQ 还没接上");
|
|
80
|
+
} catch {
|
|
81
|
+
/* A channel that cannot answer still leaves the plugins visible. */
|
|
82
|
+
}
|
|
83
|
+
for (const item of this.list()) {
|
|
84
|
+
const name = item.manifest?.name || item.id;
|
|
85
|
+
if (item.state === "error") lines.push(`${name}接上时出错了`);
|
|
86
|
+
else if (!item.enabled) lines.push(`${name}还没有接上`);
|
|
87
|
+
else if (item.running) lines.push(`${name}开着`);
|
|
88
|
+
else lines.push(`${name}正在接上`);
|
|
89
|
+
}
|
|
90
|
+
return lines.slice(0, 8);
|
|
91
|
+
}
|
|
92
|
+
allows(id, permission) {
|
|
93
|
+
const row = decode(rowOf(this.db, id));
|
|
94
|
+
return !!row?.enabled && row.granted.includes(permission);
|
|
95
|
+
}
|
|
96
|
+
log(id, level, message) {
|
|
97
|
+
this.db
|
|
98
|
+
.prepare(
|
|
99
|
+
"INSERT INTO plugin_events(plugin_id,time,kind,level,message) VALUES (?,?,?,?,?)",
|
|
100
|
+
)
|
|
101
|
+
.run(id, this.now(), "log", level, text(message, 500));
|
|
102
|
+
this.store.revision++;
|
|
103
|
+
}
|
|
104
|
+
// Built-in folders and previously installed copies. Nothing is started
|
|
105
|
+
// until the owner has enabled it.
|
|
106
|
+
scan() {
|
|
107
|
+
const found = [];
|
|
108
|
+
const builtin = join(this.root, "plugins");
|
|
109
|
+
if (existsSync(builtin)) {
|
|
110
|
+
for (const name of readdirSync(builtin)) {
|
|
111
|
+
const dir = join(builtin, name);
|
|
112
|
+
if (!existsSync(join(dir, "luckytri-plugin.json"))) continue;
|
|
113
|
+
try {
|
|
114
|
+
found.push({
|
|
115
|
+
manifest: readManifest(dir),
|
|
116
|
+
path: dir,
|
|
117
|
+
source: "builtin",
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.log(name, "error", error.message);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (existsSync(this.dataRoot)) {
|
|
125
|
+
for (const id of readdirSync(this.dataRoot)) {
|
|
126
|
+
const base = join(this.dataRoot, id);
|
|
127
|
+
if (!statSync(base).isDirectory()) continue;
|
|
128
|
+
for (const version of readdirSync(base)) {
|
|
129
|
+
const dir = join(base, version);
|
|
130
|
+
if (!existsSync(join(dir, "luckytri-plugin.json"))) continue;
|
|
131
|
+
try {
|
|
132
|
+
found.push({
|
|
133
|
+
manifest: readManifest(dir),
|
|
134
|
+
path: dir,
|
|
135
|
+
source: "installed",
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
this.log(id, "error", error.message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const upsert = this.db.prepare(
|
|
144
|
+
`INSERT INTO plugin_installs(id,version,enabled,state,source,sha256,api,permissions,granted,manifest,path,error,installed,updated)
|
|
145
|
+
VALUES (@id,@version,0,'installed',@source,'',@api,@permissions,'[]',@manifest,@path,'',@now,@now)
|
|
146
|
+
ON CONFLICT(id) DO UPDATE SET version=excluded.version,source=CASE WHEN plugin_installs.source='dev' THEN plugin_installs.source ELSE excluded.source END,api=excluded.api,permissions=excluded.permissions,manifest=excluded.manifest,path=excluded.path,updated=excluded.updated
|
|
147
|
+
WHERE plugin_installs.source!='dev'`,
|
|
148
|
+
);
|
|
149
|
+
for (const item of found) {
|
|
150
|
+
const current = rowOf(this.db, item.manifest.id);
|
|
151
|
+
if (current?.source === "dev") continue;
|
|
152
|
+
upsert.run({
|
|
153
|
+
id: item.manifest.id,
|
|
154
|
+
version: item.manifest.version,
|
|
155
|
+
source:
|
|
156
|
+
current?.source && current.source !== "builtin"
|
|
157
|
+
? current.source
|
|
158
|
+
: item.source,
|
|
159
|
+
api: item.manifest.pluginApi,
|
|
160
|
+
permissions: JSON.stringify(item.manifest.permissions),
|
|
161
|
+
manifest: JSON.stringify(item.manifest),
|
|
162
|
+
path: item.path,
|
|
163
|
+
now: this.now(),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
const present = new Set(found.map((item) => item.manifest.id));
|
|
167
|
+
const dropInstall = this.db.prepare(
|
|
168
|
+
"DELETE FROM plugin_installs WHERE id=?",
|
|
169
|
+
);
|
|
170
|
+
const dropSettings = this.db.prepare(
|
|
171
|
+
"DELETE FROM plugin_settings WHERE plugin_id=?",
|
|
172
|
+
);
|
|
173
|
+
const dropKv = this.db.prepare("DELETE FROM plugin_kv WHERE plugin_id=?");
|
|
174
|
+
for (const row of this.db
|
|
175
|
+
.prepare("SELECT id FROM plugin_installs WHERE source='builtin'")
|
|
176
|
+
.all()) {
|
|
177
|
+
if (present.has(row.id)) continue;
|
|
178
|
+
dropInstall.run(row.id);
|
|
179
|
+
dropSettings.run(row.id);
|
|
180
|
+
dropKv.run(row.id);
|
|
181
|
+
}
|
|
182
|
+
return this.list();
|
|
183
|
+
}
|
|
184
|
+
list() {
|
|
185
|
+
return this.db
|
|
186
|
+
.prepare("SELECT * FROM plugin_installs ORDER BY id")
|
|
187
|
+
.all()
|
|
188
|
+
.map((row) => {
|
|
189
|
+
const item = decode(row);
|
|
190
|
+
const runtime = this.procs.get(item.id);
|
|
191
|
+
return {
|
|
192
|
+
...item,
|
|
193
|
+
running: !!runtime?.ready,
|
|
194
|
+
status: runtime?.status || null,
|
|
195
|
+
risk: item.permissions.map((name) => ({
|
|
196
|
+
name,
|
|
197
|
+
...(PERMISSIONS[name] || { risk: "high", label: name }),
|
|
198
|
+
})),
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
async enable(id) {
|
|
203
|
+
const row = decode(rowOf(this.db, id));
|
|
204
|
+
if (!row) throw Error("插件不存在");
|
|
205
|
+
const missing = row.permissions.filter(
|
|
206
|
+
(name) => !row.granted.includes(name),
|
|
207
|
+
);
|
|
208
|
+
if (missing.length) throw Error("还有权限没有同意");
|
|
209
|
+
this.db
|
|
210
|
+
.prepare(
|
|
211
|
+
"UPDATE plugin_installs SET enabled=1,state='enabled',error='',updated=? WHERE id=?",
|
|
212
|
+
)
|
|
213
|
+
.run(this.now(), id);
|
|
214
|
+
await this.launch(rowOf(this.db, id));
|
|
215
|
+
this.store.revision++;
|
|
216
|
+
return this.list().find((item) => item.id === id);
|
|
217
|
+
}
|
|
218
|
+
grant(id, permissions) {
|
|
219
|
+
const row = decode(rowOf(this.db, id));
|
|
220
|
+
if (!row) throw Error("插件不存在");
|
|
221
|
+
const wanted = new Set(row.permissions);
|
|
222
|
+
if ((permissions || []).some((name) => !wanted.has(name)))
|
|
223
|
+
throw Error("插件权限无效");
|
|
224
|
+
if (row.permissions.some((name) => !(permissions || []).includes(name)))
|
|
225
|
+
throw Error("还有权限没有同意");
|
|
226
|
+
this.db
|
|
227
|
+
.prepare("UPDATE plugin_installs SET granted=?,updated=? WHERE id=?")
|
|
228
|
+
.run(JSON.stringify(row.permissions), this.now(), id);
|
|
229
|
+
return this.list().find((item) => item.id === id);
|
|
230
|
+
}
|
|
231
|
+
async disable(id) {
|
|
232
|
+
await this.stop(id);
|
|
233
|
+
this.db
|
|
234
|
+
.prepare(
|
|
235
|
+
"UPDATE plugin_installs SET enabled=0,state='installed',updated=? WHERE id=?",
|
|
236
|
+
)
|
|
237
|
+
.run(this.now(), id);
|
|
238
|
+
this.store.revision++;
|
|
239
|
+
return this.list().find((item) => item.id === id);
|
|
240
|
+
}
|
|
241
|
+
settings(id) {
|
|
242
|
+
const row = decode(rowOf(this.db, id));
|
|
243
|
+
if (!row) throw Error("插件不存在");
|
|
244
|
+
const saved = Object.fromEntries(
|
|
245
|
+
this.db
|
|
246
|
+
.prepare(
|
|
247
|
+
"SELECT key,value,secret FROM plugin_settings WHERE plugin_id=?",
|
|
248
|
+
)
|
|
249
|
+
.all(id)
|
|
250
|
+
.map((item) => [item.key, item.secret ? null : JSON.parse(item.value)]),
|
|
251
|
+
);
|
|
252
|
+
return {
|
|
253
|
+
fields: row.manifest.settings || [],
|
|
254
|
+
values: saved,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
// What the plugin process itself may read, including secrets.
|
|
258
|
+
settingsForPlugin(id) {
|
|
259
|
+
const values = {};
|
|
260
|
+
for (const item of this.db
|
|
261
|
+
.prepare("SELECT key,value FROM plugin_settings WHERE plugin_id=?")
|
|
262
|
+
.all(id))
|
|
263
|
+
values[item.key] = JSON.parse(item.value);
|
|
264
|
+
const manifest = decode(rowOf(this.db, id))?.manifest;
|
|
265
|
+
for (const field of manifest?.settings || [])
|
|
266
|
+
if (!(field.key in values)) values[field.key] = field.default;
|
|
267
|
+
return values;
|
|
268
|
+
}
|
|
269
|
+
saveSettings(id, input) {
|
|
270
|
+
const row = decode(rowOf(this.db, id));
|
|
271
|
+
if (!row) throw Error("插件不存在");
|
|
272
|
+
const write = this.db.prepare(
|
|
273
|
+
"INSERT INTO plugin_settings(plugin_id,key,value,secret) VALUES (?,?,?,?) ON CONFLICT(plugin_id,key) DO UPDATE SET value=excluded.value,secret=excluded.secret",
|
|
274
|
+
);
|
|
275
|
+
for (const field of row.manifest.settings || []) {
|
|
276
|
+
if (!(field.key in (input || {}))) continue;
|
|
277
|
+
let value = input[field.key];
|
|
278
|
+
if (field.type === "secret" && (value === "" || value == null)) continue;
|
|
279
|
+
if (field.type === "boolean") value = !!value;
|
|
280
|
+
if (field.type === "number") {
|
|
281
|
+
value = Number(value);
|
|
282
|
+
if (!Number.isFinite(value)) throw Error("插件设置项无效");
|
|
283
|
+
}
|
|
284
|
+
if (field.type === "urls") {
|
|
285
|
+
const list = (
|
|
286
|
+
Array.isArray(value) ? value : String(value || "").split(/\s+/)
|
|
287
|
+
)
|
|
288
|
+
.map((item) => item.trim())
|
|
289
|
+
.filter(Boolean);
|
|
290
|
+
for (const item of list) {
|
|
291
|
+
const url = new URL(item);
|
|
292
|
+
if (!["https:", "http:"].includes(url.protocol))
|
|
293
|
+
throw Error("插件设置项无效");
|
|
294
|
+
}
|
|
295
|
+
value = list;
|
|
296
|
+
}
|
|
297
|
+
write.run(
|
|
298
|
+
id,
|
|
299
|
+
field.key,
|
|
300
|
+
JSON.stringify(value),
|
|
301
|
+
field.type === "secret" ? 1 : 0,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
this.store.revision++;
|
|
305
|
+
this.event(id, "settings", this.settingsForPlugin(id));
|
|
306
|
+
return this.settings(id);
|
|
307
|
+
}
|
|
308
|
+
hosts(id) {
|
|
309
|
+
const row = decode(rowOf(this.db, id));
|
|
310
|
+
const hosts = new Set(row?.manifest.network || []);
|
|
311
|
+
for (const field of row?.manifest.settings || []) {
|
|
312
|
+
if (!field.allowNetwork && field.type !== "urls") continue;
|
|
313
|
+
const value = this.settingsForPlugin(id)[field.key];
|
|
314
|
+
const list = Array.isArray(value) ? value : [];
|
|
315
|
+
for (const item of list) {
|
|
316
|
+
try {
|
|
317
|
+
hosts.add(new URL(item).hostname.toLowerCase());
|
|
318
|
+
} catch {
|
|
319
|
+
/* Invalid entries are rejected when the setting is saved. */
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return hosts;
|
|
324
|
+
}
|
|
325
|
+
async launch(row) {
|
|
326
|
+
const item = decode(row);
|
|
327
|
+
if (!item?.enabled || this.closed) return;
|
|
328
|
+
await this.stop(item.id);
|
|
329
|
+
const dataDir = join(this.dataRoot, item.id, "data");
|
|
330
|
+
mkdirSync(dataDir, { recursive: true });
|
|
331
|
+
const entry = join(item.path, item.manifest.entry);
|
|
332
|
+
if (!existsSync(entry)) throw Error("插件入口无效");
|
|
333
|
+
const raw = item.granted.includes("net.raw");
|
|
334
|
+
const child = spawn(
|
|
335
|
+
process.execPath,
|
|
336
|
+
[
|
|
337
|
+
"--permission",
|
|
338
|
+
`--allow-fs-read=${item.path}`,
|
|
339
|
+
`--allow-fs-read=${dirname(RUNNER)}`,
|
|
340
|
+
`--allow-fs-read=${dirname(process.execPath)}`,
|
|
341
|
+
`--allow-fs-write=${dataDir}`,
|
|
342
|
+
"--max-old-space-size=128",
|
|
343
|
+
RUNNER,
|
|
344
|
+
],
|
|
345
|
+
{
|
|
346
|
+
cwd: item.path,
|
|
347
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
348
|
+
env: {
|
|
349
|
+
LUCKYTRI_PLUGIN_ID: item.id,
|
|
350
|
+
LUCKYTRI_PLUGIN_VERSION: item.version,
|
|
351
|
+
LUCKYTRI_PLUGIN_ENTRY: entry,
|
|
352
|
+
LUCKYTRI_PLUGIN_DATA: dataDir,
|
|
353
|
+
LUCKYTRI_NET_RAW: raw ? "1" : "0",
|
|
354
|
+
LUCKYTRI_API_FEATURES: JSON.stringify(API_FEATURES),
|
|
355
|
+
SystemRoot: process.env.SystemRoot || "",
|
|
356
|
+
PATH: process.env.PATH || "",
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
const runtime = {
|
|
361
|
+
child,
|
|
362
|
+
ready: false,
|
|
363
|
+
pending: new Map(),
|
|
364
|
+
seq: 0,
|
|
365
|
+
status: null,
|
|
366
|
+
crashes: [],
|
|
367
|
+
channels: new Map(),
|
|
368
|
+
};
|
|
369
|
+
this.procs.set(item.id, runtime);
|
|
370
|
+
child.stdout.on("data", (chunk) =>
|
|
371
|
+
this.log(item.id, "info", chunk.toString()),
|
|
372
|
+
);
|
|
373
|
+
child.stderr.on("data", (chunk) =>
|
|
374
|
+
this.log(item.id, "warn", chunk.toString()),
|
|
375
|
+
);
|
|
376
|
+
child.on("message", (raw) => this.onMessage(item.id, raw));
|
|
377
|
+
child.on("exit", () => this.onExit(item.id));
|
|
378
|
+
try {
|
|
379
|
+
await this.request(item.id, "activate", {}, 20000);
|
|
380
|
+
runtime.ready = true;
|
|
381
|
+
this.db
|
|
382
|
+
.prepare(
|
|
383
|
+
"UPDATE plugin_installs SET state='running',error='' WHERE id=?",
|
|
384
|
+
)
|
|
385
|
+
.run(item.id);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
this.fail(item.id, error.message);
|
|
388
|
+
child.kill();
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
request(id, method, params, timeout = 15000) {
|
|
392
|
+
const runtime = this.procs.get(id);
|
|
393
|
+
if (!runtime) return Promise.reject(new Error("插件没有在运行"));
|
|
394
|
+
const mid = `h${++runtime.seq}`;
|
|
395
|
+
const encoded = JSON.stringify({
|
|
396
|
+
v: 1,
|
|
397
|
+
id: mid,
|
|
398
|
+
type: "req",
|
|
399
|
+
method,
|
|
400
|
+
params,
|
|
401
|
+
});
|
|
402
|
+
if (encoded.length > LIMIT)
|
|
403
|
+
return Promise.reject(new Error("插件消息过大"));
|
|
404
|
+
return new Promise((resolve, reject) => {
|
|
405
|
+
const timer = setTimeout(() => {
|
|
406
|
+
runtime.pending.delete(mid);
|
|
407
|
+
reject(new Error("插件请求超时"));
|
|
408
|
+
}, timeout);
|
|
409
|
+
runtime.pending.set(mid, { resolve, reject, timer });
|
|
410
|
+
runtime.child.send(encoded);
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
event(id, method, params) {
|
|
414
|
+
const runtime = this.procs.get(id);
|
|
415
|
+
if (!runtime?.ready) return;
|
|
416
|
+
const encoded = JSON.stringify({
|
|
417
|
+
v: 1,
|
|
418
|
+
id: `e${++runtime.seq}`,
|
|
419
|
+
type: "evt",
|
|
420
|
+
method,
|
|
421
|
+
params,
|
|
422
|
+
});
|
|
423
|
+
if (encoded.length <= LIMIT) runtime.child.send(encoded);
|
|
424
|
+
}
|
|
425
|
+
async dispatch(id, slot, input, timeout = 15000) {
|
|
426
|
+
return this.request(id, "dispatch", { slot, input }, timeout);
|
|
427
|
+
}
|
|
428
|
+
onMessage(id, raw) {
|
|
429
|
+
let message;
|
|
430
|
+
try {
|
|
431
|
+
message = JSON.parse(String(raw));
|
|
432
|
+
} catch {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (!message || message.v !== 1) return;
|
|
436
|
+
const runtime = this.procs.get(id);
|
|
437
|
+
if (!runtime) return;
|
|
438
|
+
if (message.type === "res") {
|
|
439
|
+
const waiting = runtime.pending.get(message.id);
|
|
440
|
+
if (!waiting) return;
|
|
441
|
+
clearTimeout(waiting.timer);
|
|
442
|
+
runtime.pending.delete(message.id);
|
|
443
|
+
message.ok
|
|
444
|
+
? waiting.resolve(message.result)
|
|
445
|
+
: waiting.reject(new Error(message.error || "插件调用失败"));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (message.type !== "req") return;
|
|
449
|
+
this.answer(id, message).then(
|
|
450
|
+
(result) =>
|
|
451
|
+
runtime.child.send(
|
|
452
|
+
JSON.stringify({
|
|
453
|
+
v: 1,
|
|
454
|
+
id: message.id,
|
|
455
|
+
type: "res",
|
|
456
|
+
ok: true,
|
|
457
|
+
result,
|
|
458
|
+
}),
|
|
459
|
+
),
|
|
460
|
+
(error) =>
|
|
461
|
+
runtime.child.send(
|
|
462
|
+
JSON.stringify({
|
|
463
|
+
v: 1,
|
|
464
|
+
id: message.id,
|
|
465
|
+
type: "res",
|
|
466
|
+
ok: false,
|
|
467
|
+
error: text(error.message, 300),
|
|
468
|
+
}),
|
|
469
|
+
),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
async answer(id, message) {
|
|
473
|
+
const { method, params = {} } = message;
|
|
474
|
+
if (method === "now") return this.now();
|
|
475
|
+
if (method === "log") {
|
|
476
|
+
this.log(id, params.level || "info", params.message || "");
|
|
477
|
+
return { ok: true };
|
|
478
|
+
}
|
|
479
|
+
if (method === "settings.get") return this.settingsForPlugin(id);
|
|
480
|
+
if (method === "storage.get") return this.kvGet(id, params.key);
|
|
481
|
+
if (method === "storage.set")
|
|
482
|
+
return this.kvSet(id, params.key, params.value);
|
|
483
|
+
if (method === "storage.delete") return this.kvDelete(id, params.key);
|
|
484
|
+
if (method === "storage.list") return this.kvList(id, params.prefix);
|
|
485
|
+
if (method === "status.set") {
|
|
486
|
+
const runtime = this.procs.get(id);
|
|
487
|
+
if (runtime)
|
|
488
|
+
runtime.status = {
|
|
489
|
+
text: text(params.text, 160),
|
|
490
|
+
tone: ["ok", "warn", "error"].includes(params.tone)
|
|
491
|
+
? params.tone
|
|
492
|
+
: "ok",
|
|
493
|
+
};
|
|
494
|
+
this.store.revision++;
|
|
495
|
+
return { ok: true };
|
|
496
|
+
}
|
|
497
|
+
if (method === "notify") {
|
|
498
|
+
this.need(id, "notify");
|
|
499
|
+
this.db
|
|
500
|
+
.prepare(
|
|
501
|
+
"INSERT INTO plugin_events(plugin_id,time,kind,level,message) VALUES (?,?,?,?,?)",
|
|
502
|
+
)
|
|
503
|
+
.run(
|
|
504
|
+
id,
|
|
505
|
+
this.now(),
|
|
506
|
+
"notify",
|
|
507
|
+
"info",
|
|
508
|
+
text(`${params.title || ""} ${params.text || ""}`, 300),
|
|
509
|
+
);
|
|
510
|
+
this.store.revision++;
|
|
511
|
+
return { ok: true };
|
|
512
|
+
}
|
|
513
|
+
if (method === "net.fetch") return this.fetch(id, params);
|
|
514
|
+
if (method === "mind.state") {
|
|
515
|
+
this.need(id, "mind.read");
|
|
516
|
+
return this.mindState();
|
|
517
|
+
}
|
|
518
|
+
if (method === "mind.listen") return { ok: true };
|
|
519
|
+
if (method === "observe") {
|
|
520
|
+
this.need(id, "observe");
|
|
521
|
+
return this.chat.mind.observations.add(
|
|
522
|
+
{ pluginId: id, ...params },
|
|
523
|
+
this.now(),
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (method === "senses.set") {
|
|
527
|
+
this.need(id, "senses");
|
|
528
|
+
this.chat.mind.senses.set(id, params.key, params.sense, this.now());
|
|
529
|
+
return { ok: true };
|
|
530
|
+
}
|
|
531
|
+
if (method === "senses.clear") {
|
|
532
|
+
this.need(id, "senses");
|
|
533
|
+
this.chat.mind.senses.clear(id, params.key);
|
|
534
|
+
return { ok: true };
|
|
535
|
+
}
|
|
536
|
+
if (method === "knowledge.offer") return this.offer(id, params);
|
|
537
|
+
if (method === "activities.handle")
|
|
538
|
+
return this.bindActivity(id, params.kind);
|
|
539
|
+
if (method === "activities.available") {
|
|
540
|
+
this.need(id, "activities");
|
|
541
|
+
const kind = `${id}.${params.kind}`;
|
|
542
|
+
setActivityEnabled(kind, !params.reason, params.reason);
|
|
543
|
+
if (!params.reason) wakeActivities(this.db, id, this.now());
|
|
544
|
+
return { ok: true };
|
|
545
|
+
}
|
|
546
|
+
if (method === "actions.handle") return this.bindAction(id, params.name);
|
|
547
|
+
if (method === "perceive.handle")
|
|
548
|
+
return this.bindPerceiver(id, params.type);
|
|
549
|
+
if (method === "channel.provide") return this.bindChannel(id, params.type);
|
|
550
|
+
if (method === "channel.receive") return this.receive(id, params);
|
|
551
|
+
if (method === "channel.status") return this.channelStatus(id, params);
|
|
552
|
+
if (method === "channel.reachable")
|
|
553
|
+
return this.channelReachable(id, params);
|
|
554
|
+
if (method === "models.call") return this.model(id, params);
|
|
555
|
+
if (method === "http.route") return this.bindRoute(id, params);
|
|
556
|
+
throw new Error("未知的插件调用");
|
|
557
|
+
}
|
|
558
|
+
need(id, permission) {
|
|
559
|
+
if (!this.allows(id, permission)) throw new Error("插件没有这个权限");
|
|
560
|
+
}
|
|
561
|
+
mindState() {
|
|
562
|
+
const now = this.now();
|
|
563
|
+
const phase = this.life?.phase?.(now);
|
|
564
|
+
const affect = this.chat.mind.affect.state(now);
|
|
565
|
+
const task = this.chat.mind.time.primary();
|
|
566
|
+
return {
|
|
567
|
+
phase: phase?.key || "awake",
|
|
568
|
+
mood: affect.mood,
|
|
569
|
+
energy: affect.energyLabel,
|
|
570
|
+
activity: task?.activity || "",
|
|
571
|
+
activityLabel: task?.title || "",
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
kvGet(id, key) {
|
|
575
|
+
const row = this.db
|
|
576
|
+
.prepare("SELECT value FROM plugin_kv WHERE plugin_id=? AND key=?")
|
|
577
|
+
.get(id, String(key || "").slice(0, 80));
|
|
578
|
+
return row ? JSON.parse(row.value) : null;
|
|
579
|
+
}
|
|
580
|
+
kvSet(id, key, value) {
|
|
581
|
+
const encoded = JSON.stringify(value ?? null);
|
|
582
|
+
if (encoded.length > 65536) throw new Error("插件保存的内容过大");
|
|
583
|
+
this.db
|
|
584
|
+
.prepare(
|
|
585
|
+
"INSERT INTO plugin_kv(plugin_id,key,value,updated) VALUES (?,?,?,?) ON CONFLICT(plugin_id,key) DO UPDATE SET value=excluded.value,updated=excluded.updated",
|
|
586
|
+
)
|
|
587
|
+
.run(id, String(key).slice(0, 80), encoded, this.now());
|
|
588
|
+
return { ok: true };
|
|
589
|
+
}
|
|
590
|
+
kvDelete(id, key) {
|
|
591
|
+
this.db
|
|
592
|
+
.prepare("DELETE FROM plugin_kv WHERE plugin_id=? AND key=?")
|
|
593
|
+
.run(id, String(key || "").slice(0, 80));
|
|
594
|
+
return { ok: true };
|
|
595
|
+
}
|
|
596
|
+
kvList(id, prefix = "") {
|
|
597
|
+
return this.db
|
|
598
|
+
.prepare(
|
|
599
|
+
"SELECT key FROM plugin_kv WHERE plugin_id=? AND substr(key,1,?)=? ORDER BY key LIMIT 200",
|
|
600
|
+
)
|
|
601
|
+
.all(id, String(prefix).length, String(prefix))
|
|
602
|
+
.map((row) => row.key);
|
|
603
|
+
}
|
|
604
|
+
async fetch(id, params) {
|
|
605
|
+
this.need(id, "net.fetch");
|
|
606
|
+
let current = String(params.url || "");
|
|
607
|
+
for (let hop = 0; hop < 3; hop++) {
|
|
608
|
+
const url = new URL(current);
|
|
609
|
+
if (
|
|
610
|
+
!["https:", "http:"].includes(url.protocol) ||
|
|
611
|
+
url.username ||
|
|
612
|
+
url.password
|
|
613
|
+
)
|
|
614
|
+
throw new Error("这个地址不能访问");
|
|
615
|
+
if (!this.hosts(id).has(url.hostname.toLowerCase()))
|
|
616
|
+
throw new Error("这个网站不在插件的允许范围内");
|
|
617
|
+
await assertPublicHost(url.hostname, lookup, "这个地址不能访问");
|
|
618
|
+
const response = await globalThis.fetch(url, {
|
|
619
|
+
method: params.method || "GET",
|
|
620
|
+
headers: params.headers || {},
|
|
621
|
+
body: params.body && params.method !== "GET" ? params.body : undefined,
|
|
622
|
+
redirect: "manual",
|
|
623
|
+
signal: AbortSignal.timeout(20000),
|
|
624
|
+
});
|
|
625
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
626
|
+
current = new URL(response.headers.get("location") || "", url).href;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const body = (await response.text()).slice(0, LIMIT);
|
|
630
|
+
return {
|
|
631
|
+
status: response.status,
|
|
632
|
+
headers: { "content-type": response.headers.get("content-type") || "" },
|
|
633
|
+
body,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
throw new Error("跳转过多");
|
|
637
|
+
}
|
|
638
|
+
async offer(id, doc) {
|
|
639
|
+
this.need(id, "knowledge.offer");
|
|
640
|
+
const day = this.now() - 86400000;
|
|
641
|
+
const used = this.db
|
|
642
|
+
.prepare(
|
|
643
|
+
"SELECT COUNT(*) n FROM plugin_events WHERE plugin_id=? AND kind='offer' AND time>?",
|
|
644
|
+
)
|
|
645
|
+
.get(id, day).n;
|
|
646
|
+
if (used >= 20) throw new Error("今天放到书架上的资料已经够多了");
|
|
647
|
+
const content = String(doc?.text || "");
|
|
648
|
+
if (!content.trim() || Buffer.byteLength(content) > 200000)
|
|
649
|
+
throw new Error("资料内容过大或是空的");
|
|
650
|
+
const collection = this.collection(id);
|
|
651
|
+
const saved = await this.chat.knowledge.ingest({
|
|
652
|
+
collectionId: collection,
|
|
653
|
+
title: text(doc.title, 200) || "未命名资料",
|
|
654
|
+
text: content,
|
|
655
|
+
source: `plugin:${id}`,
|
|
656
|
+
embed: false,
|
|
657
|
+
});
|
|
658
|
+
this.db
|
|
659
|
+
.prepare(
|
|
660
|
+
"INSERT INTO plugin_events(plugin_id,time,kind,level,message) VALUES (?,?,?,?,?)",
|
|
661
|
+
)
|
|
662
|
+
.run(id, this.now(), "offer", "info", saved.id);
|
|
663
|
+
return { id: saved.id };
|
|
664
|
+
}
|
|
665
|
+
collection(id) {
|
|
666
|
+
const key = `plugin:${id}`;
|
|
667
|
+
const found = this.db
|
|
668
|
+
.prepare("SELECT id FROM core_collections WHERE id=?")
|
|
669
|
+
.get(key);
|
|
670
|
+
if (found) return key;
|
|
671
|
+
const row = decode(rowOf(this.db, id));
|
|
672
|
+
this.db
|
|
673
|
+
.prepare(
|
|
674
|
+
"INSERT INTO core_collections(id,name,scope,created,updated) VALUES (?,?,?,?,?)",
|
|
675
|
+
)
|
|
676
|
+
.run(
|
|
677
|
+
key,
|
|
678
|
+
`来自插件:${row?.manifest.name || id}`,
|
|
679
|
+
"shared",
|
|
680
|
+
this.now(),
|
|
681
|
+
this.now(),
|
|
682
|
+
);
|
|
683
|
+
return key;
|
|
684
|
+
}
|
|
685
|
+
bindActivity(id, kind) {
|
|
686
|
+
this.need(id, "activities");
|
|
687
|
+
const full = `${id}.${kind}`;
|
|
688
|
+
const declared = decode(
|
|
689
|
+
rowOf(this.db, id),
|
|
690
|
+
)?.manifest.contributes.activities.find((item) => item.kind === full);
|
|
691
|
+
if (!declared) throw new Error("插件没有声明这个活动");
|
|
692
|
+
let keywords = null;
|
|
693
|
+
try {
|
|
694
|
+
keywords = declared.keywords ? new RegExp(declared.keywords, "i") : null;
|
|
695
|
+
} catch {
|
|
696
|
+
keywords = null;
|
|
697
|
+
}
|
|
698
|
+
registerActivity({
|
|
699
|
+
kind: full,
|
|
700
|
+
pluginId: id,
|
|
701
|
+
label: declared.label,
|
|
702
|
+
minutes: [declared.minutes, declared.describe || "本段活动"],
|
|
703
|
+
energy: declared.energy,
|
|
704
|
+
pattern: keywords,
|
|
705
|
+
prepare: (input) => this.dispatch(id, `activity:${kind}`, input, 20000),
|
|
706
|
+
});
|
|
707
|
+
wakeActivities(this.db, id, this.now());
|
|
708
|
+
return { ok: true };
|
|
709
|
+
}
|
|
710
|
+
bindAction(id, name) {
|
|
711
|
+
this.need(id, "actions");
|
|
712
|
+
const full = `${id}.${name}`;
|
|
713
|
+
const declared = decode(
|
|
714
|
+
rowOf(this.db, id),
|
|
715
|
+
)?.manifest.contributes.actions.find((item) => item.name === full);
|
|
716
|
+
if (!declared) throw new Error("插件没有声明这个行动");
|
|
717
|
+
this.chat.capabilities.register({ ...declared, pluginId: id }, (input) =>
|
|
718
|
+
this.dispatch(id, `action:${name}`, input, 15000),
|
|
719
|
+
);
|
|
720
|
+
return { ok: true };
|
|
721
|
+
}
|
|
722
|
+
bindPerceiver(id, type) {
|
|
723
|
+
this.need(id, "perceive");
|
|
724
|
+
const declared =
|
|
725
|
+
decode(rowOf(this.db, id))?.manifest.contributes.perceive || [];
|
|
726
|
+
if (!declared.includes(type)) throw new Error("插件没有声明这种感知");
|
|
727
|
+
this.chat.perceivers.register(type, id, (input) =>
|
|
728
|
+
this.dispatch(id, `perceive:${type}`, input, 15000),
|
|
729
|
+
);
|
|
730
|
+
return { ok: true };
|
|
731
|
+
}
|
|
732
|
+
bindChannel(id, type) {
|
|
733
|
+
this.need(id, "channel");
|
|
734
|
+
const declared = decode(
|
|
735
|
+
rowOf(this.db, id),
|
|
736
|
+
)?.manifest.contributes.channels.find((item) => item.type === type);
|
|
737
|
+
if (!declared) throw new Error("插件没有声明这个通道");
|
|
738
|
+
const runtime = this.procs.get(id);
|
|
739
|
+
const cache = { online: false, reachable: new Map() };
|
|
740
|
+
runtime.channels.set(type, cache);
|
|
741
|
+
const channel = defineChannel({
|
|
742
|
+
type,
|
|
743
|
+
capabilities: {
|
|
744
|
+
mention: !!declared.capabilities.mention,
|
|
745
|
+
quote: !!declared.capabilities.quote,
|
|
746
|
+
image: !!declared.capabilities.image,
|
|
747
|
+
thread: !!declared.capabilities.thread,
|
|
748
|
+
sticker: !!declared.capabilities.sticker,
|
|
749
|
+
},
|
|
750
|
+
attach: () => {},
|
|
751
|
+
start: () => this.dispatch(id, `channel:${type}:start`, {}, 20000),
|
|
752
|
+
stop: () =>
|
|
753
|
+
this.dispatch(id, `channel:${type}:stop`, {}, 10000).catch(() => {}),
|
|
754
|
+
send: (message, text) =>
|
|
755
|
+
this.dispatch(id, `channel:${type}:send`, { message, text }, 15000),
|
|
756
|
+
fetchQuoted: (message) =>
|
|
757
|
+
this.dispatch(id, `channel:${type}:fetchQuoted`, { message }, 8000),
|
|
758
|
+
fetchImage: (file) =>
|
|
759
|
+
this.dispatch(id, `channel:${type}:fetchImage`, { file }, 8000),
|
|
760
|
+
fetchMedia: (file, mediaType) =>
|
|
761
|
+
this.dispatch(
|
|
762
|
+
id,
|
|
763
|
+
`channel:${type}:fetchMedia`,
|
|
764
|
+
{ file, type: mediaType },
|
|
765
|
+
8000,
|
|
766
|
+
),
|
|
767
|
+
refreshDirectory: () =>
|
|
768
|
+
this.dispatch(id, `channel:${type}:refreshDirectory`, {}, 8000).catch(
|
|
769
|
+
() => 0,
|
|
770
|
+
),
|
|
771
|
+
canReach: (sessionId) => cache.reachable.get(sessionId) === true,
|
|
772
|
+
status: () => ({ type, online: cache.online }),
|
|
773
|
+
close: () => {},
|
|
774
|
+
});
|
|
775
|
+
this.channel.register(channel);
|
|
776
|
+
return { ok: true };
|
|
777
|
+
}
|
|
778
|
+
receive(id, { type, message }) {
|
|
779
|
+
this.need(id, "channel");
|
|
780
|
+
const runtime = this.procs.get(id);
|
|
781
|
+
if (!runtime?.channels.has(type)) throw new Error("插件没有声明这个通道");
|
|
782
|
+
const kind = message?.kind === "private" ? "private" : "group";
|
|
783
|
+
const nativeId = text(message?.nativeId, 80);
|
|
784
|
+
const userId = text(message?.userId, 80);
|
|
785
|
+
const body = text(message?.text, 4000);
|
|
786
|
+
if (!nativeId || !userId || !body) throw new Error("消息不完整");
|
|
787
|
+
const accountId = text(message?.accountId, 80) || id;
|
|
788
|
+
this.chat.receive({
|
|
789
|
+
sessionId: formatSessionKey({ channel: type, accountId, kind, nativeId }),
|
|
790
|
+
channel: type,
|
|
791
|
+
accountId,
|
|
792
|
+
nativeId,
|
|
793
|
+
kind,
|
|
794
|
+
userId: userId.startsWith(`${type}:`) ? userId : `${type}:${userId}`,
|
|
795
|
+
name: text(message?.name, 100) || userId,
|
|
796
|
+
text: body,
|
|
797
|
+
mentioned: !!message?.mentioned,
|
|
798
|
+
platformId: text(message?.platformId, 80) || randomUUID(),
|
|
799
|
+
time: Number(message?.time) || this.now(),
|
|
800
|
+
mentions: Array.isArray(message?.mentions)
|
|
801
|
+
? message.mentions.map(String).slice(0, 20)
|
|
802
|
+
: [],
|
|
803
|
+
replyId: text(message?.replyId, 80),
|
|
804
|
+
attachments: Array.isArray(message?.attachments)
|
|
805
|
+
? message.attachments.slice(0, 8)
|
|
806
|
+
: [],
|
|
807
|
+
eventId: `${type}:${accountId}:${nativeId}:${message?.platformId || randomUUID()}`,
|
|
808
|
+
});
|
|
809
|
+
return { ok: true };
|
|
810
|
+
}
|
|
811
|
+
channelStatus(id, { type, status }) {
|
|
812
|
+
this.procs.get(id)?.channels.get(type) &&
|
|
813
|
+
(this.procs.get(id).channels.get(type).online = !!status?.online);
|
|
814
|
+
this.store.revision++;
|
|
815
|
+
return { ok: true };
|
|
816
|
+
}
|
|
817
|
+
channelReachable(id, { type, sessionKey, ok }) {
|
|
818
|
+
this.procs.get(id)?.channels.get(type)?.reachable.set(sessionKey, !!ok);
|
|
819
|
+
return { ok: true };
|
|
820
|
+
}
|
|
821
|
+
async model(id, params) {
|
|
822
|
+
this.need(id, "models");
|
|
823
|
+
const now = this.now();
|
|
824
|
+
if (!this.chat.mind.budget.allows("plugins", now))
|
|
825
|
+
throw new Error("插件预算已经用完");
|
|
826
|
+
const system = text(params.system, 4000);
|
|
827
|
+
const input = params.input ?? {};
|
|
828
|
+
if (JSON.stringify(input).length > 60000) throw new Error("插件消息过大");
|
|
829
|
+
const profile = this.chat.models.profile();
|
|
830
|
+
const trace = this.chat.repo.trace("__plugin__", "plugin");
|
|
831
|
+
try {
|
|
832
|
+
const result = await withFallback(
|
|
833
|
+
this.chat.models,
|
|
834
|
+
this.chat.fallbackFor(null, profile, trace),
|
|
835
|
+
).call(profile, "plugin", system, input, trace);
|
|
836
|
+
this.chat.finishQuietly(trace, "complete");
|
|
837
|
+
return result;
|
|
838
|
+
} catch (error) {
|
|
839
|
+
trace.error = error.message;
|
|
840
|
+
this.chat.finishQuietly(trace, "error");
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
bindRoute(id, { method, path, public: open }) {
|
|
845
|
+
this.need(id, open ? "http.public" : "http.routes");
|
|
846
|
+
const verb = String(method || "GET").toUpperCase();
|
|
847
|
+
if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(verb))
|
|
848
|
+
throw new Error("插件接口无效");
|
|
849
|
+
const route = String(path || "");
|
|
850
|
+
if (!/^\/[a-z0-9/_-]{0,80}$/i.test(route)) throw new Error("插件接口无效");
|
|
851
|
+
this.routes = this.routes.filter(
|
|
852
|
+
(item) =>
|
|
853
|
+
!(item.id === id && item.method === verb && item.path === route),
|
|
854
|
+
);
|
|
855
|
+
this.routes.push({ id, method: verb, path: route, open: !!open });
|
|
856
|
+
return { ok: true };
|
|
857
|
+
}
|
|
858
|
+
async handleHttp(id, method, path, body) {
|
|
859
|
+
const route = this.routes.find(
|
|
860
|
+
(item) => item.id === id && item.method === method && item.path === path,
|
|
861
|
+
);
|
|
862
|
+
if (!route) return null;
|
|
863
|
+
return this.dispatch(id, `http:${method}:${path}`, { body }, 15000);
|
|
864
|
+
}
|
|
865
|
+
detach(id) {
|
|
866
|
+
unregisterActivities(id);
|
|
867
|
+
this.chat.capabilities.unregister(id);
|
|
868
|
+
this.chat.perceivers.unregister(id);
|
|
869
|
+
this.chat.mind.senses.clearPlugin(id);
|
|
870
|
+
this.routes = this.routes.filter((item) => item.id !== id);
|
|
871
|
+
for (const type of [...(this.procs.get(id)?.channels.keys() || [])])
|
|
872
|
+
this.channel.unregister(type);
|
|
873
|
+
}
|
|
874
|
+
async stop(id) {
|
|
875
|
+
const runtime = this.procs.get(id);
|
|
876
|
+
if (runtime) {
|
|
877
|
+
try {
|
|
878
|
+
await this.request(id, "deactivate", {}, 3000);
|
|
879
|
+
} catch {
|
|
880
|
+
/* The process may already be gone. */
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
this.detach(id);
|
|
884
|
+
if (runtime) {
|
|
885
|
+
this.procs.delete(id);
|
|
886
|
+
await new Promise((resolve) => {
|
|
887
|
+
const timer = setTimeout(resolve, 2000);
|
|
888
|
+
runtime.child.once("exit", () => {
|
|
889
|
+
clearTimeout(timer);
|
|
890
|
+
resolve();
|
|
891
|
+
});
|
|
892
|
+
runtime.child.kill();
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
onExit(id) {
|
|
897
|
+
const runtime = this.procs.get(id);
|
|
898
|
+
if (!runtime || this.closed) return;
|
|
899
|
+
this.detach(id);
|
|
900
|
+
this.procs.delete(id);
|
|
901
|
+
const now = this.now();
|
|
902
|
+
runtime.crashes = (runtime.crashes || []).filter((at) => now - at < 600000);
|
|
903
|
+
runtime.crashes.push(now);
|
|
904
|
+
if (runtime.crashes.length >= 3) {
|
|
905
|
+
this.fail(id, "插件反复退出,已先停用");
|
|
906
|
+
this.db
|
|
907
|
+
.prepare(
|
|
908
|
+
"UPDATE plugin_installs SET enabled=0,state='error' WHERE id=?",
|
|
909
|
+
)
|
|
910
|
+
.run(id);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const wait =
|
|
914
|
+
BACKOFF[Math.min(runtime.crashes.length - 1, BACKOFF.length - 1)];
|
|
915
|
+
setTimeout(() => {
|
|
916
|
+
const row = rowOf(this.db, id);
|
|
917
|
+
if (row?.enabled && !this.closed) this.launch(row).catch(() => {});
|
|
918
|
+
}, wait).unref?.();
|
|
919
|
+
}
|
|
920
|
+
fail(id, message) {
|
|
921
|
+
this.db
|
|
922
|
+
.prepare("UPDATE plugin_installs SET state='error',error=? WHERE id=?")
|
|
923
|
+
.run(text(message, 300), id);
|
|
924
|
+
this.log(id, "error", message);
|
|
925
|
+
}
|
|
926
|
+
beat() {
|
|
927
|
+
if (this.closed || !this.life) return;
|
|
928
|
+
const now = this.now();
|
|
929
|
+
let state;
|
|
930
|
+
try {
|
|
931
|
+
state = this.mindState();
|
|
932
|
+
} catch {
|
|
933
|
+
state = null;
|
|
934
|
+
}
|
|
935
|
+
if (state) {
|
|
936
|
+
for (const [key, event] of [
|
|
937
|
+
["phase", "life.phase"],
|
|
938
|
+
["mood", "mind.mood"],
|
|
939
|
+
["activity", "life.activity"],
|
|
940
|
+
]) {
|
|
941
|
+
if (state[key] !== this.seen[key]) {
|
|
942
|
+
this.seen[key] = state[key];
|
|
943
|
+
for (const id of this.procs.keys())
|
|
944
|
+
if (this.allows(id, "mind.read")) this.event(id, event, state);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const id of this.procs.keys())
|
|
949
|
+
this.request(id, "ping", {}, 10000).catch(() =>
|
|
950
|
+
this.procs.get(id)?.child.kill(),
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
async start() {
|
|
954
|
+
this.scan();
|
|
955
|
+
for (const row of this.db
|
|
956
|
+
.prepare("SELECT * FROM plugin_installs WHERE enabled=1")
|
|
957
|
+
.all())
|
|
958
|
+
await this.launch(row);
|
|
959
|
+
this.timer = setInterval(() => this.beat(), 30000);
|
|
960
|
+
this.timer.unref?.();
|
|
961
|
+
}
|
|
962
|
+
async close() {
|
|
963
|
+
this.closed = true;
|
|
964
|
+
clearInterval(this.timer);
|
|
965
|
+
for (const id of [...this.procs.keys()]) await this.stop(id);
|
|
966
|
+
}
|
|
967
|
+
}
|