@koishi-ce/plugin-config 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/dist/index.css +2 -0
- package/dist/index.js +5 -0
- package/lib/index.d.ts +234 -0
- package/lib/index.mjs +518 -0
- package/package.json +78 -0
- package/src/browser/index.ts +36 -0
- package/src/browser/packages.ts +7 -0
- package/src/index.ts +2 -0
- package/src/node/index.ts +42 -0
- package/src/node/packages.ts +64 -0
- package/src/shared/index.ts +25 -0
- package/src/shared/packages.ts +245 -0
- package/src/shared/services.ts +51 -0
- package/src/shared/writer.ts +390 -0
package/lib/index.mjs
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { Context, Logger, Schema, remove } from "@koishi-ce/koishi";
|
|
4
|
+
import { DataService } from "@koishi-ce/console";
|
|
5
|
+
import { Loader } from "@koishi-ce/loader";
|
|
6
|
+
import { LocalScanner } from "@koishi-ce/registry";
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/shared/packages.ts
|
|
11
|
+
/**
|
|
12
|
+
* 插件包元数据提供器(服务端共享实现)。
|
|
13
|
+
*
|
|
14
|
+
* 以 `packages` 数据服务的形式向浏览器端提供本机可用的插件包列表及
|
|
15
|
+
* 各自的运行时信息(Config schema、用法文档、注入的服务依赖、fork
|
|
16
|
+
* 状态等)。运行时信息通过解析插件模块的导出(parseExports)获得并
|
|
17
|
+
* 缓存在 cache 中,监听 runtime/fork/status 等内部事件按需刷新。
|
|
18
|
+
*
|
|
19
|
+
* 抽象类只定义"运行时信息"这一半;列表如何收集(本机扫描还是市场
|
|
20
|
+
* 数据)分别由 node / browser 两个运行环境的子类实现。
|
|
21
|
+
*/
|
|
22
|
+
const logger$2 = new Logger("config");
|
|
23
|
+
var PackageProvider = class extends DataService {
|
|
24
|
+
cache = {};
|
|
25
|
+
debouncedRefresh;
|
|
26
|
+
ctx;
|
|
27
|
+
/**
|
|
28
|
+
* @param ctx 应用上下文
|
|
29
|
+
*/
|
|
30
|
+
constructor(ctx) {
|
|
31
|
+
super(ctx, "packages", { authority: 4 });
|
|
32
|
+
this.ctx = ctx;
|
|
33
|
+
this.debouncedRefresh = ctx.debounce(() => this.refresh(false), 0);
|
|
34
|
+
ctx.on("internal/runtime", (scope) => this.update(scope.runtime.plugin));
|
|
35
|
+
ctx.on("internal/fork", (scope) => this.update(scope.runtime.plugin));
|
|
36
|
+
ctx.on("internal/status", (scope) => this.update(scope.runtime.plugin));
|
|
37
|
+
ctx.on("hmr/reload", (reloads) => {
|
|
38
|
+
for (const [plugin] of reloads) this.update(plugin);
|
|
39
|
+
});
|
|
40
|
+
ctx.console.addListener("config/request-runtime", async (name) => {
|
|
41
|
+
name = name.replace(/(koishi-|^@koishijs\/)plugin-/, "");
|
|
42
|
+
this.cache[name] = await this.parseExports(name);
|
|
43
|
+
this.refresh(false);
|
|
44
|
+
}, { authority: 4 });
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 某插件的运行时状态发生变化时,重新解析其导出并去抖刷新服务。
|
|
48
|
+
*
|
|
49
|
+
* @param plugin 发生变化的插件对象
|
|
50
|
+
*/
|
|
51
|
+
async update(plugin) {
|
|
52
|
+
const name = this.ctx.loader.keyFor(plugin);
|
|
53
|
+
if (!name || !this.cache[name]) return;
|
|
54
|
+
this.cache[name] = await this.parseExports(name);
|
|
55
|
+
this.debouncedRefresh();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 从插件主作用域提取 fork 运行状态,写入 result 的 id/forkable/forks 字段。
|
|
59
|
+
*
|
|
60
|
+
* @param state 插件的主作用域
|
|
61
|
+
* @param result 待填充的运行时数据(原地修改)
|
|
62
|
+
*/
|
|
63
|
+
parseRuntime(state, result) {
|
|
64
|
+
if (state.runtime.uid !== null) result.id = state.runtime.uid;
|
|
65
|
+
result.forkable = state.runtime.isForkable;
|
|
66
|
+
const forks = {};
|
|
67
|
+
for (const fork of state.children) {
|
|
68
|
+
const key = fork.key;
|
|
69
|
+
if (!key) continue;
|
|
70
|
+
forks[key] = { status: fork.status };
|
|
71
|
+
}
|
|
72
|
+
result.forks = forks;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 数据服务读取入口:收集包列表,合并缓存的运行时信息,
|
|
76
|
+
* 并在最前面附加一个 name 为空串的"应用全局设置"条目。
|
|
77
|
+
*/
|
|
78
|
+
async get(forced = false) {
|
|
79
|
+
const objects = (await this.collect(forced)).slice();
|
|
80
|
+
for (const object of objects) {
|
|
81
|
+
object.name = object.package?.name || "";
|
|
82
|
+
const cached = this.cache[object.shortname];
|
|
83
|
+
if (!cached) continue;
|
|
84
|
+
object.runtime = cached;
|
|
85
|
+
}
|
|
86
|
+
objects.unshift({
|
|
87
|
+
name: "",
|
|
88
|
+
runtime: { schema: Context.Config },
|
|
89
|
+
package: { name: "" }
|
|
90
|
+
});
|
|
91
|
+
return Object.fromEntries(objects.map((data) => [data.name, data]));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 解析插件模块的导出,提取配置 schema、用法文档、过滤器开关与
|
|
95
|
+
* 注入的服务依赖(required / optional),并附带运行时 fork 状态。
|
|
96
|
+
* 解析失败时返回 `{ failed: true }` 而不是抛错。
|
|
97
|
+
*
|
|
98
|
+
* @param name 插件短名(不含 koishi-plugin- 前缀)
|
|
99
|
+
* @returns 可序列化的运行时数据
|
|
100
|
+
*/
|
|
101
|
+
async parseExports(name) {
|
|
102
|
+
try {
|
|
103
|
+
const exports = await this.ctx.loader.resolve(name);
|
|
104
|
+
const result = {};
|
|
105
|
+
const schema = exports?.Config || exports?.schema;
|
|
106
|
+
if (schema) result.schema = schema;
|
|
107
|
+
if (exports?.usage) result.usage = exports.usage;
|
|
108
|
+
if (exports?.filter !== void 0) result.filter = exports.filter;
|
|
109
|
+
const inject = exports?.using || exports?.inject || [];
|
|
110
|
+
if (Array.isArray(inject)) {
|
|
111
|
+
result.required = inject;
|
|
112
|
+
result.optional = [];
|
|
113
|
+
} else {
|
|
114
|
+
result.required = inject.required || [];
|
|
115
|
+
result.optional = inject.optional || [];
|
|
116
|
+
}
|
|
117
|
+
JSON.stringify(result);
|
|
118
|
+
const runtime = exports ? this.ctx.registry.get(exports) : void 0;
|
|
119
|
+
if (runtime) this.parseRuntime(runtime, result);
|
|
120
|
+
return result;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger$2.warn("failed to load %c", name);
|
|
123
|
+
logger$2.warn(error);
|
|
124
|
+
return { failed: true };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/shared/services.ts
|
|
130
|
+
/**
|
|
131
|
+
* 服务状态提供器:以 `services` 数据服务的形式向浏览器端上报当前
|
|
132
|
+
* 应用中每个已注册服务的提供者信息(服务名 → 提供该服务的上下文 uid)。
|
|
133
|
+
* 前端据此判断某插件"必需/可选注入的服务"是否已可用。
|
|
134
|
+
*
|
|
135
|
+
* 遍历方式:从根上下文的 internal 对象出发沿原型链逐层收集类型为
|
|
136
|
+
* "service" 的属性,再通过 Context.current 访问器反查服务实例所属的
|
|
137
|
+
* 上下文,取其作用域 uid。
|
|
138
|
+
*/
|
|
139
|
+
var ServiceProvider = class extends DataService {
|
|
140
|
+
constructor(ctx) {
|
|
141
|
+
super(ctx, "services");
|
|
142
|
+
ctx.on("internal/service", () => this.refresh());
|
|
143
|
+
}
|
|
144
|
+
async get() {
|
|
145
|
+
const services = {};
|
|
146
|
+
/**
|
|
147
|
+
* 递归遍历 internal 对象的原型链,收集所有服务属性。
|
|
148
|
+
*
|
|
149
|
+
* @param internal 当前层级的 internal 对象
|
|
150
|
+
*/
|
|
151
|
+
const attach = (internal) => {
|
|
152
|
+
if (!internal) return;
|
|
153
|
+
attach(Object.getPrototypeOf(internal));
|
|
154
|
+
for (const [key, { type }] of Object.entries(internal)) {
|
|
155
|
+
if (type !== "service") continue;
|
|
156
|
+
const instance = this.ctx.get(key);
|
|
157
|
+
if (!(instance instanceof Object)) continue;
|
|
158
|
+
const ctx = Reflect.getOwnPropertyDescriptor(instance, Context.current)?.value;
|
|
159
|
+
if (!ctx) continue;
|
|
160
|
+
const name = key.replace(/^__/, "").replace(/__$/, "");
|
|
161
|
+
if (ctx.scope.uid !== null) services[name] = ctx.scope.uid;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
attach(this.ctx.root[Context.internal]);
|
|
165
|
+
return services;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region src/shared/writer.ts
|
|
170
|
+
/**
|
|
171
|
+
* 配置写入器(服务端共享实现)。
|
|
172
|
+
*
|
|
173
|
+
* 本模块是 config 插件的核心:以 `config` 数据服务的形式把 loader 中的
|
|
174
|
+
* 应用配置(koishi.yml / koishi.json)推送到浏览器端控制台,并把前端
|
|
175
|
+
* 发来的 `manager/*` 事件(重载、停用、移除、改名、拖拽移动等)落实为
|
|
176
|
+
* 对内存中配置对象的修改,再由 loader.writeConfig() 持久化回配置文件。
|
|
177
|
+
*
|
|
178
|
+
* 由于 YAML/JSON 对象的键顺序即插件在配置文件中的书写顺序,本模块中的
|
|
179
|
+
* 多个辅助函数(insertKey / rename / dropKey)都围绕"在保持键顺序的
|
|
180
|
+
* 前提下增删改键"这一目标实现。
|
|
181
|
+
*/
|
|
182
|
+
const logger$1 = new Logger("loader");
|
|
183
|
+
/**
|
|
184
|
+
* 把 temp 中的键插入到 object 的开头(原有 rest 键依次后移),用于在
|
|
185
|
+
* 不破坏其它键相对顺序的前提下完成"插入到指定位置"的配置操作。
|
|
186
|
+
*
|
|
187
|
+
* 实现方式是先把 rest 中的键全部搬到 temp 里,再用 Object.assign 合并回来。
|
|
188
|
+
*
|
|
189
|
+
* @param object 目标配置对象(会被原地修改)
|
|
190
|
+
* @param temp 待插入的键值对
|
|
191
|
+
* @param rest 需要让位的既有键名列表
|
|
192
|
+
*/
|
|
193
|
+
function insertKey(object, temp, rest) {
|
|
194
|
+
for (const key of rest) {
|
|
195
|
+
temp[key] = object[key];
|
|
196
|
+
delete object[key];
|
|
197
|
+
}
|
|
198
|
+
Object.assign(object, temp);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 在对象中把键 old 重命名为 neo(并可顺带替换其值),且保持键原有的位置。
|
|
202
|
+
*
|
|
203
|
+
* @param object 目标配置对象
|
|
204
|
+
* @param old 旧键名(可为 `"~" + name` 形式的停用态键)
|
|
205
|
+
* @param neo 新键名
|
|
206
|
+
* @param value 新键对应的值
|
|
207
|
+
*/
|
|
208
|
+
function rename(object, old, neo, value) {
|
|
209
|
+
const keys = Object.keys(object);
|
|
210
|
+
const index = keys.findIndex((key) => key === old || key === `~${old}`);
|
|
211
|
+
const rest = index < 0 ? [] : keys.slice(index + 1);
|
|
212
|
+
const temp = { [neo]: value };
|
|
213
|
+
delete object[old];
|
|
214
|
+
delete object[`~${old}`];
|
|
215
|
+
insertKey(object, temp, rest);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* 从 plugins 配置中摘除指定的键(兼容 `~` 前缀的停用态键),返回以该键
|
|
219
|
+
* 为唯一成员的对象,便于随后通过 insertKey 插入到别处(teleport 场景)。
|
|
220
|
+
*
|
|
221
|
+
* @param plugins 插件配置对象
|
|
222
|
+
* @param name 插件键名
|
|
223
|
+
*/
|
|
224
|
+
function dropKey(plugins, name) {
|
|
225
|
+
if (!(name in plugins)) name = `~${name}`;
|
|
226
|
+
const value = plugins[name];
|
|
227
|
+
delete plugins[name];
|
|
228
|
+
return { [name]: value };
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* 配置写入器服务。以 `config` 为服务名向控制台暴露当前应用配置,
|
|
232
|
+
* 并接收前端 `manager/*` 事件来修改配置文件。所有事件均要求
|
|
233
|
+
* authority 达到 4(管理员)。
|
|
234
|
+
*/
|
|
235
|
+
var ConfigWriter = class extends DataService {
|
|
236
|
+
loader;
|
|
237
|
+
constructor(ctx) {
|
|
238
|
+
super(ctx, "config", { authority: 4 });
|
|
239
|
+
this.loader = ctx.loader;
|
|
240
|
+
ctx.console.addListener("manager/app-reload", (config) => {
|
|
241
|
+
return this.reloadApp(config);
|
|
242
|
+
}, { authority: 4 });
|
|
243
|
+
for (const key of [
|
|
244
|
+
"teleport",
|
|
245
|
+
"reload",
|
|
246
|
+
"unload",
|
|
247
|
+
"remove",
|
|
248
|
+
"meta"
|
|
249
|
+
]) ctx.console.addListener(`manager/${key}`, async (...args) => {
|
|
250
|
+
try {
|
|
251
|
+
await this[key].apply(this, args);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
logger$1.error(error);
|
|
254
|
+
throw new Error("failed");
|
|
255
|
+
}
|
|
256
|
+
}, { authority: 4 });
|
|
257
|
+
ctx.on("config", () => this.refresh());
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 递归过滤并整理某层上下文的插件配置,得到可安全下发给前端的副本。
|
|
261
|
+
*
|
|
262
|
+
* - 过滤掉内部保留键(`$` 开头,如 `$folded`);
|
|
263
|
+
* - 剔除 `$if` 条件不成立的插件项(停用插件以 `~` 前缀 + `$if-disabled`
|
|
264
|
+
* 表示,`isTruthyLike` 为假说明该项已不被加载);
|
|
265
|
+
* - 对 `group:xxx` 分组键递归处理其子插件。
|
|
266
|
+
*
|
|
267
|
+
* @param plugins 某层上下文的 plugins 配置对象
|
|
268
|
+
* @param ctx 该层对应的运行时上下文(用于查找 fork 记录)
|
|
269
|
+
*/
|
|
270
|
+
getGroup(plugins, ctx) {
|
|
271
|
+
const result = { ...plugins };
|
|
272
|
+
for (const key in plugins ?? {}) {
|
|
273
|
+
if (key.startsWith("$")) continue;
|
|
274
|
+
const value = plugins?.[key];
|
|
275
|
+
const name = (key.split(":", 1)[0] ?? "").replace(/^~/, "");
|
|
276
|
+
if (!this.loader.isTruthyLike(value?.["$if"])) {
|
|
277
|
+
delete result[key];
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const fork = ctx.scope[Loader.kRecord]?.[key];
|
|
281
|
+
if (!fork) continue;
|
|
282
|
+
if (name === "group") result[key] = this.getGroup(value, fork.ctx);
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
/** 数据服务读取入口:返回去除敏感内部键后的整份应用配置。 */
|
|
287
|
+
async get() {
|
|
288
|
+
const result = { ...this.loader.config };
|
|
289
|
+
result.plugins = this.getGroup(result.plugins, this.loader.entry);
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* 保存全局设置:用前端提交的整份配置替换当前配置(保留原有的 plugins
|
|
294
|
+
* 部分),写回配置文件后执行全量重载(进程内重启全部插件)。
|
|
295
|
+
*
|
|
296
|
+
* @param config 前端提交的应用级配置(不含 plugins)
|
|
297
|
+
*/
|
|
298
|
+
async reloadApp(config) {
|
|
299
|
+
delete config["$paths"];
|
|
300
|
+
const plugins = this.loader.config.plugins;
|
|
301
|
+
this.loader.config = config;
|
|
302
|
+
Object.assign(this.loader.config, { plugins });
|
|
303
|
+
await this.loader.writeConfig();
|
|
304
|
+
this.loader.fullReload();
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* 根据路径标识查找对应的 fork 作用域。
|
|
308
|
+
*
|
|
309
|
+
* 前端用"分组路径"(如 `group:abc` 的 `abc`)标识每个上下文:空串代表
|
|
310
|
+
* 根作用域,否则在所有已注册插件的子作用域中按 fork.key 匹配。
|
|
311
|
+
*
|
|
312
|
+
* @param ident 分组路径(空串表示根)
|
|
313
|
+
* @returns 对应的 ForkScope,找不到时返回 undefined
|
|
314
|
+
*/
|
|
315
|
+
resolveFork(ident) {
|
|
316
|
+
if (!ident) return this.loader.entry.scope;
|
|
317
|
+
for (const main of this.ctx.registry.values()) for (const fork of main.children) if (fork.key === ident) return fork;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* 在配置文件中按路径标识递归定位插件条目。
|
|
321
|
+
*
|
|
322
|
+
* @param ident 插件的路径标识(键去掉 `name:` 前缀后的部分)
|
|
323
|
+
* @param config 当前层级的 plugins 配置(递归时传入分组内部)
|
|
324
|
+
* @returns [所在配置对象, 完整键名] 二元组
|
|
325
|
+
* @throws 找不到时抛出 "plugin not found"
|
|
326
|
+
*/
|
|
327
|
+
resolveConfig(ident, config = this.loader.config.plugins) {
|
|
328
|
+
if (config) for (const key in config) {
|
|
329
|
+
const name = key.split(":", 1)[0] ?? "";
|
|
330
|
+
if (key.slice(name.length + 1) === ident) return [config, key];
|
|
331
|
+
if (name === "group" || name === "~group") try {
|
|
332
|
+
return this.resolveConfig(ident, config[key]);
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
throw new Error("plugin not found");
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* 更新插件的元数据键(`$` 开头的内部控制字段,如 `$label`、`$collapsed`)。
|
|
339
|
+
*
|
|
340
|
+
* config 中值为 null 的键表示删除;其余键覆盖写入并置于该插件配置的
|
|
341
|
+
* 开头位置。仅写盘(writeConfig(true) 触发静默刷新),不重载插件。
|
|
342
|
+
*
|
|
343
|
+
* @param ident 插件路径标识
|
|
344
|
+
* @param config 待合并的元数据键值对
|
|
345
|
+
*/
|
|
346
|
+
async meta(ident, config) {
|
|
347
|
+
const [parent, key] = this.resolveConfig(ident);
|
|
348
|
+
const target = parent[key];
|
|
349
|
+
for (const key of Object.keys(config)) {
|
|
350
|
+
delete target[key];
|
|
351
|
+
if (config[key] === null) delete config[key];
|
|
352
|
+
}
|
|
353
|
+
insertKey(target, config, Object.keys(target));
|
|
354
|
+
await this.loader.writeConfig(true);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* (重新)加载插件:用提交的配置替换内存中的插件配置,先热重载再写盘。
|
|
358
|
+
*
|
|
359
|
+
* @param parent 所属分组的路径标识(空串为根)
|
|
360
|
+
* @param key 插件键名(形如 `name:ident`)
|
|
361
|
+
* @param config 新的插件配置
|
|
362
|
+
*/
|
|
363
|
+
async reload(parent, key, config) {
|
|
364
|
+
const scope = this.resolveFork(parent);
|
|
365
|
+
if (!scope) throw new Error("plugin not found");
|
|
366
|
+
await this.loader.reload(scope.ctx, key, config);
|
|
367
|
+
rename(scope.config, key, key, config);
|
|
368
|
+
await this.loader.writeConfig();
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* 停用插件:卸载运行时并把配置键改写为 `~` 前缀(保留配置以便再启用)。
|
|
372
|
+
*
|
|
373
|
+
* 传入 index 时(克隆配置场景)把停用键插入到指定位置而非原位重命名。
|
|
374
|
+
*
|
|
375
|
+
* @param parent 所属分组的路径标识
|
|
376
|
+
* @param key 插件键名
|
|
377
|
+
* @param config 停用时保留的插件配置
|
|
378
|
+
* @param index 可选的目标插入位置(在父配置的键序列中的序号)
|
|
379
|
+
*/
|
|
380
|
+
async unload(parent, key, config = {}, index) {
|
|
381
|
+
const scope = this.resolveFork(parent);
|
|
382
|
+
if (!scope) throw new Error("plugin not found");
|
|
383
|
+
this.loader.unload(scope.ctx, key);
|
|
384
|
+
if (index) {
|
|
385
|
+
const rest = Object.keys(scope.config).slice(index);
|
|
386
|
+
insertKey(scope.config, { [`~${key}`]: config }, rest);
|
|
387
|
+
} else rename(scope.config, key, `~${key}`, config);
|
|
388
|
+
await this.loader.writeConfig();
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* 移除插件:卸载运行时并从配置文件中彻底删除对应键(含 `~` 停用态)。
|
|
392
|
+
*
|
|
393
|
+
* @param parent 所属分组的路径标识
|
|
394
|
+
* @param key 插件键名
|
|
395
|
+
*/
|
|
396
|
+
async remove(parent, key) {
|
|
397
|
+
const scope = this.resolveFork(parent);
|
|
398
|
+
if (!scope) throw new Error("plugin not found");
|
|
399
|
+
this.loader.unload(scope.ctx, key);
|
|
400
|
+
const config = scope.config;
|
|
401
|
+
delete config[key];
|
|
402
|
+
delete config[`~${key}`];
|
|
403
|
+
await this.loader.writeConfig();
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* 拖拽移动插件:把插件从 source 分组搬到 target 分组的 index 位置。
|
|
407
|
+
*
|
|
408
|
+
* 分两步:
|
|
409
|
+
* 1. 迁移运行时 fork(改挂父作用域、迁移 disposables、修正原型链,
|
|
410
|
+
* 若注入的服务在两个分组中取值不同则重启该 fork);
|
|
411
|
+
* 2. 迁移配置键(dropKey 摘出后 insertKey 插入目标位置)。
|
|
412
|
+
*
|
|
413
|
+
* @param source 源分组的路径标识(空串为根)
|
|
414
|
+
* @param key 被移动的插件键名
|
|
415
|
+
* @param target 目标分组的路径标识
|
|
416
|
+
* @param index 目标插入位置(目标分组键序列中的序号)
|
|
417
|
+
*/
|
|
418
|
+
async teleport(source, key, target, index) {
|
|
419
|
+
const parentS = this.resolveFork(source);
|
|
420
|
+
const parentT = this.resolveFork(target);
|
|
421
|
+
if (!parentS || !parentT) throw new Error("plugin not found");
|
|
422
|
+
const fork = parentS[Loader.kRecord]?.[key];
|
|
423
|
+
if (fork && parentS !== parentT) {
|
|
424
|
+
const sourceRecord = parentS[Loader.kRecord] ??= Object.create(null);
|
|
425
|
+
delete sourceRecord[key];
|
|
426
|
+
const targetRecord = parentT[Loader.kRecord] ??= Object.create(null);
|
|
427
|
+
targetRecord[key] = fork;
|
|
428
|
+
remove(parentS.disposables, fork.dispose);
|
|
429
|
+
parentT.disposables.push(fork.dispose);
|
|
430
|
+
fork.parent = parentT.ctx;
|
|
431
|
+
Object.setPrototypeOf(fork.ctx, parentT.ctx);
|
|
432
|
+
fork.ctx.emit("internal/fork", fork);
|
|
433
|
+
if (Object.keys(fork.runtime.inject).some((name) => Reflect.get(parentS, name) !== Reflect.get(parentT, name))) fork.restart();
|
|
434
|
+
}
|
|
435
|
+
const temp = dropKey(parentS.config, key);
|
|
436
|
+
const rest = Object.keys(parentT.config).slice(index);
|
|
437
|
+
insertKey(parentT.config, temp, rest);
|
|
438
|
+
await this.loader.writeConfig();
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/node/packages.ts
|
|
443
|
+
/**
|
|
444
|
+
* PackageProvider 的 Node 端实现:基于 LocalScanner 扫描本机
|
|
445
|
+
* node_modules 中的插件包,并在扫描过程中顺带解析已加载插件的
|
|
446
|
+
* 运行时信息写入共享 cache。
|
|
447
|
+
*/
|
|
448
|
+
const logger = new Logger("config");
|
|
449
|
+
/**
|
|
450
|
+
* 本机插件扫描器:在 LocalScanner 的基础上扩展 parsePackage,
|
|
451
|
+
* 让"扫描到某个包"与"解析该插件的运行时信息"在同一次遍历中完成。
|
|
452
|
+
*/
|
|
453
|
+
var PackageScanner = class extends LocalScanner {
|
|
454
|
+
service;
|
|
455
|
+
constructor(service) {
|
|
456
|
+
super(service.ctx.baseDir);
|
|
457
|
+
this.service = service;
|
|
458
|
+
}
|
|
459
|
+
/** 单个包解析失败时仅记录警告,不中断整体扫描。 */
|
|
460
|
+
async onError(error, name) {
|
|
461
|
+
logger.warn("failed to resolve %c", name);
|
|
462
|
+
logger.warn(error);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* 解析包元数据后,若该插件的入口模块已被 require 进内存,
|
|
466
|
+
* 则顺带解析其导出并写入服务缓存,供前端立即取用。
|
|
467
|
+
*/
|
|
468
|
+
async parsePackage(name) {
|
|
469
|
+
const result = await super.parsePackage(name);
|
|
470
|
+
try {
|
|
471
|
+
const entry = __require.resolve(name);
|
|
472
|
+
if (__require.cache[entry]) {
|
|
473
|
+
name = name.replace(/(koishi-|^@koishijs\/)plugin-/, "");
|
|
474
|
+
this.service.cache[name] = await this.service.parseExports(name);
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
this.onError(error, name);
|
|
478
|
+
}
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
/** Node 端包提供器:collect 委托给本机扫描器。 */
|
|
483
|
+
var PackageProvider$1 = class extends PackageProvider {
|
|
484
|
+
scanner = new PackageScanner(this);
|
|
485
|
+
/**
|
|
486
|
+
* 扫描本机插件包并返回扫描结果。
|
|
487
|
+
*
|
|
488
|
+
* @param forced 为 true 时强制重新扫描(不走缓存)
|
|
489
|
+
*/
|
|
490
|
+
async collect(forced) {
|
|
491
|
+
await this.scanner.collect(forced);
|
|
492
|
+
return this.scanner.objects;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/node/index.ts
|
|
497
|
+
/**
|
|
498
|
+
* config 插件的 Node 端入口。
|
|
499
|
+
*
|
|
500
|
+
* 前置检查 loader 的可写性(只有 koishi.yml / koishi.json 这类文件型
|
|
501
|
+
* 配置才支持网页编辑),随后挂载三个数据服务(插件包元数据、服务状态、
|
|
502
|
+
* 配置写入器),并把 client 目录注册为控制台前端入口。
|
|
503
|
+
*/
|
|
504
|
+
const name = "config";
|
|
505
|
+
const inject = ["console"];
|
|
506
|
+
const Config = Schema.object({});
|
|
507
|
+
function apply(ctx) {
|
|
508
|
+
if (!ctx.loader?.writable) return ctx.logger("app").warn("@koishi-ce/plugin-config is only available for json/yaml config file");
|
|
509
|
+
ctx.plugin(PackageProvider$1);
|
|
510
|
+
ctx.plugin(ServiceProvider);
|
|
511
|
+
ctx.plugin(ConfigWriter);
|
|
512
|
+
ctx.console.addEntry({
|
|
513
|
+
dev: resolve(__dirname, "../../client/index.ts"),
|
|
514
|
+
prod: resolve(__dirname, "../../dist")
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
//#endregion
|
|
518
|
+
export { Config, ConfigWriter, PackageProvider, ServiceProvider, apply, inject, name };
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@koishi-ce/plugin-config",
|
|
3
|
+
"description": "Manage your bots and plugins with console",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.mjs",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"source": "./src/index.ts",
|
|
11
|
+
"types": "./lib/index.d.ts",
|
|
12
|
+
"import": "./lib/index.mjs",
|
|
13
|
+
"default": "./lib/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"./shared": {
|
|
16
|
+
"import": "./lib/shared/index.mjs",
|
|
17
|
+
"default": "./lib/shared/index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"./src/*": "./src/*",
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"lib",
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"contributors": [
|
|
28
|
+
"Shigma <shigma10826@gmail.com>",
|
|
29
|
+
"Oppenheymu <oppenheymu@gmail.com>"
|
|
30
|
+
],
|
|
31
|
+
"license": "AGPL-3.0",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Koishi-CE/koishi.git",
|
|
35
|
+
"directory": "plugins/webui/config"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Koishi-CE/koishi/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://koishi.chat/plugins/console/config.html",
|
|
41
|
+
"keywords": [
|
|
42
|
+
"bot",
|
|
43
|
+
"chatbot",
|
|
44
|
+
"koishi",
|
|
45
|
+
"plugin",
|
|
46
|
+
"config",
|
|
47
|
+
"manager",
|
|
48
|
+
"server"
|
|
49
|
+
],
|
|
50
|
+
"koishi": {
|
|
51
|
+
"public": [
|
|
52
|
+
"dist"
|
|
53
|
+
],
|
|
54
|
+
"description": {
|
|
55
|
+
"en": "Configure your plugins with console",
|
|
56
|
+
"zh": "使用控制台查看、配置你的插件"
|
|
57
|
+
},
|
|
58
|
+
"service": {
|
|
59
|
+
"required": [
|
|
60
|
+
"console"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@koishijs/loader": "^4.6.11",
|
|
66
|
+
"@koishijs/plugin-console": "^5.30.11",
|
|
67
|
+
"koishi": "^4.18.11"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@koishi-ce/client": "^1.0.0",
|
|
71
|
+
"@koishi-ce/plugin-hmr": "^1.0.0"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"@koishi-ce/console": "^1.0.0",
|
|
75
|
+
"@koishi-ce/loader": "^1.0.0",
|
|
76
|
+
"@koishi-ce/registry": "^1.0.0"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config 插件的浏览器端入口。
|
|
3
|
+
*
|
|
4
|
+
* 与 node 入口共用 shared 的三个数据服务,差别在于:
|
|
5
|
+
* 1. 额外注入 loader 服务;
|
|
6
|
+
* 2. 前端资源不走 addEntry 的 dev/prod 约定,而是显式给出资源 URL——
|
|
7
|
+
* 设置了 KOISHI_BASE 环境变量时从远端加载打包产物,否则回退到
|
|
8
|
+
* client 源码(供开发调试用)。
|
|
9
|
+
*/
|
|
10
|
+
import { type Context, Schema } from "@koishi-ce/koishi";
|
|
11
|
+
import { ConfigWriter, ServiceProvider } from "../shared/index.ts";
|
|
12
|
+
import { PackageProvider } from "./packages.ts";
|
|
13
|
+
|
|
14
|
+
export * from "../shared/index.ts";
|
|
15
|
+
|
|
16
|
+
export const name = "config";
|
|
17
|
+
export const inject = ["console", "loader"];
|
|
18
|
+
|
|
19
|
+
export type Config = Record<never, never>;
|
|
20
|
+
|
|
21
|
+
export const Config: Schema<Config> = Schema.object({});
|
|
22
|
+
|
|
23
|
+
export function apply(ctx: Context) {
|
|
24
|
+
ctx.plugin(PackageProvider);
|
|
25
|
+
ctx.plugin(ConfigWriter);
|
|
26
|
+
ctx.plugin(ServiceProvider);
|
|
27
|
+
|
|
28
|
+
ctx.console.addEntry(
|
|
29
|
+
process.env["KOISHI_BASE"]
|
|
30
|
+
? [
|
|
31
|
+
`${process.env["KOISHI_BASE"]}/dist/index.js`,
|
|
32
|
+
`${process.env["KOISHI_BASE"]}/dist/style.css`,
|
|
33
|
+
]
|
|
34
|
+
: [import.meta.url.replace(/\/src\/[^/]+\/[^/]+$/, "/client/index.ts")],
|
|
35
|
+
);
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config 插件的 Node 端入口。
|
|
3
|
+
*
|
|
4
|
+
* 前置检查 loader 的可写性(只有 koishi.yml / koishi.json 这类文件型
|
|
5
|
+
* 配置才支持网页编辑),随后挂载三个数据服务(插件包元数据、服务状态、
|
|
6
|
+
* 配置写入器),并把 client 目录注册为控制台前端入口。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { type Context, Schema } from "@koishi-ce/koishi";
|
|
11
|
+
import { ConfigWriter, ServiceProvider } from "../shared/index.ts";
|
|
12
|
+
import { PackageProvider } from "./packages.ts";
|
|
13
|
+
|
|
14
|
+
export * from "../shared/index.ts";
|
|
15
|
+
|
|
16
|
+
export const name = "config";
|
|
17
|
+
export const inject = ["console"];
|
|
18
|
+
|
|
19
|
+
export type Config = Record<never, never>;
|
|
20
|
+
|
|
21
|
+
export const Config: Schema<Config> = Schema.object({});
|
|
22
|
+
|
|
23
|
+
export function apply(ctx: Context) {
|
|
24
|
+
// 仅文件型配置(可写)支持本插件,其余环境(如环境变量驱动)直接跳过
|
|
25
|
+
if (!ctx.loader?.writable) {
|
|
26
|
+
return ctx
|
|
27
|
+
.logger("app")
|
|
28
|
+
.warn(
|
|
29
|
+
"@koishi-ce/plugin-config is only available for json/yaml config file",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ctx.plugin(PackageProvider);
|
|
34
|
+
ctx.plugin(ServiceProvider);
|
|
35
|
+
ctx.plugin(ConfigWriter);
|
|
36
|
+
|
|
37
|
+
// 注册控制台前端资源:dev 模式直接指向 client 源码,prod 指向打包产物
|
|
38
|
+
ctx.console.addEntry({
|
|
39
|
+
dev: resolve(__dirname, "../../client/index.ts"),
|
|
40
|
+
prod: resolve(__dirname, "../../dist"),
|
|
41
|
+
});
|
|
42
|
+
}
|