@koishi-ce/plugin-config 1.0.7 → 1.0.9
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/lib/index.d.ts +14 -2
- package/lib/index.mjs +18 -10
- package/package.json +8 -5
- package/src/__tests__/index.test.ts +417 -105
- package/src/browser/index.ts +10 -2
- package/src/node/index.ts +10 -2
- package/src/node/packages.ts +22 -7
- package/src/shared/packages.ts +74 -28
- package/src/shared/services.ts +17 -8
- package/src/shared/writer.ts +88 -27
package/src/browser/index.ts
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
* client 源码(供开发调试用)。
|
|
13
13
|
*/
|
|
14
14
|
import { type Context, Schema } from "@koishi-ce/koishi";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
ConfigWriter,
|
|
17
|
+
ServiceProvider,
|
|
18
|
+
} from "../shared/index.ts";
|
|
16
19
|
import { PackageProvider } from "./packages.ts";
|
|
17
20
|
|
|
18
21
|
export * from "../shared/index.ts";
|
|
@@ -35,6 +38,11 @@ export function apply(ctx: Context) {
|
|
|
35
38
|
`${process.env["KOISHI_BASE"]}/dist/index.js`,
|
|
36
39
|
`${process.env["KOISHI_BASE"]}/dist/style.css`,
|
|
37
40
|
]
|
|
38
|
-
: [
|
|
41
|
+
: [
|
|
42
|
+
import.meta.url.replace(
|
|
43
|
+
/\/src\/[^/]+\/[^/]+$/,
|
|
44
|
+
"/client/index.ts",
|
|
45
|
+
),
|
|
46
|
+
],
|
|
39
47
|
);
|
|
40
48
|
}
|
package/src/node/index.ts
CHANGED
|
@@ -12,13 +12,21 @@
|
|
|
12
12
|
|
|
13
13
|
import { resolve } from "node:path";
|
|
14
14
|
import { type Context, Schema } from "@koishi-ce/koishi";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
ConfigWriter,
|
|
17
|
+
ServiceProvider,
|
|
18
|
+
} from "../shared/index.ts";
|
|
16
19
|
import { PackageProvider } from "./packages.ts";
|
|
17
20
|
|
|
18
21
|
export * from "../shared/index.ts";
|
|
19
22
|
|
|
20
23
|
export const name = "config";
|
|
21
|
-
|
|
24
|
+
// loader 由宿主以 builtin 服务提供,生产恒存在;测试等裸 App 环境可缺席,
|
|
25
|
+
// 声明为非必需注入以免 cordis 报「property loader is not registered」
|
|
26
|
+
export const inject = {
|
|
27
|
+
console: { required: true },
|
|
28
|
+
loader: { required: false },
|
|
29
|
+
};
|
|
22
30
|
|
|
23
31
|
export type Config = Record<never, never>;
|
|
24
32
|
|
package/src/node/packages.ts
CHANGED
|
@@ -90,13 +90,21 @@ export class PackageProvider extends shared.PackageProvider {
|
|
|
90
90
|
shared.PackageProvider.Data[]
|
|
91
91
|
> {
|
|
92
92
|
// 以完整包名为键索引全部条目(浅拷贝,避免跨 forced 扫描累积 paths)
|
|
93
|
-
const index = new Map<
|
|
93
|
+
const index = new Map<
|
|
94
|
+
string,
|
|
95
|
+
shared.PackageProvider.Data
|
|
96
|
+
>();
|
|
94
97
|
for (const object of this.scanner.objects) {
|
|
95
|
-
index.set(object.package.name, {
|
|
98
|
+
index.set(object.package.name, {
|
|
99
|
+
...object,
|
|
100
|
+
paths: [],
|
|
101
|
+
});
|
|
96
102
|
}
|
|
97
103
|
try {
|
|
98
104
|
await walkPlugins(
|
|
99
|
-
this.ctx.loader?.config?.plugins as
|
|
105
|
+
this.ctx.loader?.config?.plugins as
|
|
106
|
+
| Record<string, unknown>
|
|
107
|
+
| undefined,
|
|
100
108
|
async (key) => {
|
|
101
109
|
// 配置键形如 ./plugins/webui/config:uid,取 : 前的路径部分
|
|
102
110
|
const path = key.split(":", 1)[0];
|
|
@@ -106,14 +114,19 @@ export class PackageProvider extends shared.PackageProvider {
|
|
|
106
114
|
);
|
|
107
115
|
if (!object) return;
|
|
108
116
|
const name = object.package.name;
|
|
109
|
-
const entry = index.get(name) ?? {
|
|
117
|
+
const entry = index.get(name) ?? {
|
|
118
|
+
...object,
|
|
119
|
+
paths: [],
|
|
120
|
+
};
|
|
110
121
|
entry.paths ||= [];
|
|
111
|
-
if (!entry.paths.includes(path))
|
|
122
|
+
if (!entry.paths.includes(path))
|
|
123
|
+
entry.paths.push(path);
|
|
112
124
|
index.set(name, entry);
|
|
113
125
|
// 登记「包名 → 配置键」反查,供 request-runtime 按路径解析
|
|
114
126
|
this.pathKeys[name] ||= path;
|
|
115
127
|
// 顺带解析运行时信息(loader.resolve 对路径键原生支持)
|
|
116
|
-
this.cache[path] ||=
|
|
128
|
+
this.cache[path] ||=
|
|
129
|
+
await this.parseExports(path);
|
|
117
130
|
},
|
|
118
131
|
);
|
|
119
132
|
} catch (error) {
|
|
@@ -136,7 +149,9 @@ async function walkPlugins(
|
|
|
136
149
|
if (key.split(":", 1)[0] === "group") {
|
|
137
150
|
const value = plugins?.[key];
|
|
138
151
|
await walkPlugins(
|
|
139
|
-
(value ?? {}) as
|
|
152
|
+
(value ?? {}) as
|
|
153
|
+
| Record<string, unknown>
|
|
154
|
+
| undefined,
|
|
140
155
|
handler,
|
|
141
156
|
);
|
|
142
157
|
} else {
|
package/src/shared/packages.ts
CHANGED
|
@@ -50,6 +50,8 @@ export abstract class PackageProvider extends DataService<
|
|
|
50
50
|
Dict<PackageProvider.Data>
|
|
51
51
|
> {
|
|
52
52
|
cache: Dict<PackageProvider.RuntimeData> = {};
|
|
53
|
+
/** 进行中的解析(键 → 共享 promise),同键并发请求合并为一次模块加载 */
|
|
54
|
+
inflight: Dict<Promise<PackageProvider.RuntimeData>> = {};
|
|
53
55
|
debouncedRefresh: () => void;
|
|
54
56
|
|
|
55
57
|
/** 完整包名 → 引用它的首个配置键(workspace 相对路径),由 node 端 collect 填充 */
|
|
@@ -64,11 +66,20 @@ export abstract class PackageProvider extends DataService<
|
|
|
64
66
|
super(ctx, "packages", { authority: 4 });
|
|
65
67
|
this.ctx = ctx;
|
|
66
68
|
|
|
67
|
-
this.debouncedRefresh = ctx.debounce(
|
|
69
|
+
this.debouncedRefresh = ctx.debounce(
|
|
70
|
+
() => this.refresh(false),
|
|
71
|
+
0,
|
|
72
|
+
);
|
|
68
73
|
// 插件的加载 / fork / 状态变化都可能影响运行时信息,统一走 update
|
|
69
|
-
ctx.on("internal/runtime", (scope) =>
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
ctx.on("internal/runtime", (scope) =>
|
|
75
|
+
this.update(scope.runtime.plugin),
|
|
76
|
+
);
|
|
77
|
+
ctx.on("internal/fork", (scope) =>
|
|
78
|
+
this.update(scope.runtime.plugin),
|
|
79
|
+
);
|
|
80
|
+
ctx.on("internal/status", (scope) =>
|
|
81
|
+
this.update(scope.runtime.plugin),
|
|
82
|
+
);
|
|
72
83
|
ctx.on("hmr/reload", (reloads) => {
|
|
73
84
|
for (const [plugin] of reloads) {
|
|
74
85
|
this.update(plugin);
|
|
@@ -76,25 +87,27 @@ export abstract class PackageProvider extends DataService<
|
|
|
76
87
|
});
|
|
77
88
|
|
|
78
89
|
// 前端按需请求:某插件缺少运行时信息时(如刚安装尚未解析),
|
|
79
|
-
// 按其配置键(workspace
|
|
90
|
+
// 按其配置键(workspace 相对路径)或短名解析其导出并立即推送。
|
|
91
|
+
// 解析失败同样写入缓存(failed 标记随数据下发):前端展示失败
|
|
92
|
+
// 提示后不再重发请求——否则「请求 → 失败 → 广播 → 前端再请求」
|
|
93
|
+
// 互相喂养成活锁,加载即崩的插件会把日志瞬间打爆
|
|
80
94
|
ctx.console.addListener(
|
|
81
95
|
"config/request-runtime",
|
|
82
96
|
async (name) => {
|
|
83
|
-
// 依次尝试:路径配置键 → 短名 →
|
|
97
|
+
// 依次尝试:路径配置键 → 短名 → 原名,首个解析成功的为准;
|
|
98
|
+
// 已有结果(含失败标记)的键直接复用,不重新解析
|
|
84
99
|
const candidates = [
|
|
85
100
|
this.pathKeys[name],
|
|
86
101
|
getPluginShortname(name),
|
|
87
102
|
name,
|
|
88
103
|
];
|
|
89
|
-
for (const key of candidates) {
|
|
104
|
+
for (const key of new Set(candidates)) {
|
|
90
105
|
if (!key) continue;
|
|
91
|
-
const result =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
106
|
+
const result = (this.cache[key] ??=
|
|
107
|
+
await this.parseExports(key));
|
|
108
|
+
if (!result.failed) break;
|
|
96
109
|
}
|
|
97
|
-
this.
|
|
110
|
+
this.debouncedRefresh();
|
|
98
111
|
},
|
|
99
112
|
{ authority: 4 },
|
|
100
113
|
);
|
|
@@ -106,7 +119,9 @@ export abstract class PackageProvider extends DataService<
|
|
|
106
119
|
*
|
|
107
120
|
* @param forced 是否强制重新收集(绕过缓存)
|
|
108
121
|
*/
|
|
109
|
-
abstract collect(
|
|
122
|
+
abstract collect(
|
|
123
|
+
forced: boolean,
|
|
124
|
+
): Promise<PackageProvider.Data[]>;
|
|
110
125
|
|
|
111
126
|
/**
|
|
112
127
|
* 某插件的运行时状态发生变化时,重新解析其导出并去抖刷新服务。
|
|
@@ -126,7 +141,10 @@ export abstract class PackageProvider extends DataService<
|
|
|
126
141
|
* @param state 插件的主作用域
|
|
127
142
|
* @param result 待填充的运行时数据(原地修改)
|
|
128
143
|
*/
|
|
129
|
-
parseRuntime(
|
|
144
|
+
parseRuntime(
|
|
145
|
+
state: MainScope,
|
|
146
|
+
result: PackageProvider.RuntimeData,
|
|
147
|
+
) {
|
|
130
148
|
// 已销毁的 runtime(uid 为 null)不展示 id
|
|
131
149
|
if (state.runtime.uid !== null) {
|
|
132
150
|
result.id = state.runtime.uid;
|
|
@@ -152,7 +170,10 @@ export abstract class PackageProvider extends DataService<
|
|
|
152
170
|
object.name = object.package?.name || "";
|
|
153
171
|
// 运行时缓存的键与写入方一致:workspace 包用配置键(./...),
|
|
154
172
|
// npm 包用短名;paths 为空时退回短名查找
|
|
155
|
-
const cached = [
|
|
173
|
+
const cached = [
|
|
174
|
+
...(object.paths ?? []),
|
|
175
|
+
object.shortname,
|
|
176
|
+
]
|
|
156
177
|
.map((key) => this.cache[key])
|
|
157
178
|
.find(Boolean);
|
|
158
179
|
if (!cached) continue;
|
|
@@ -168,18 +189,32 @@ export abstract class PackageProvider extends DataService<
|
|
|
168
189
|
package: { name: "" },
|
|
169
190
|
// 全局设置条目不带市场元数据字段,用双断言放宽必填约束
|
|
170
191
|
} as unknown as PackageProvider.Data);
|
|
171
|
-
return Object.fromEntries(
|
|
192
|
+
return Object.fromEntries(
|
|
193
|
+
objects.map((data) => [data.name, data]),
|
|
194
|
+
);
|
|
172
195
|
}
|
|
173
196
|
|
|
174
197
|
/**
|
|
175
198
|
* 解析插件模块的导出,提取配置 schema、用法文档、过滤器开关与
|
|
176
199
|
* 注入的服务依赖(required / optional),并附带运行时 fork 状态。
|
|
177
|
-
* 解析失败时返回 `{ failed: true }`
|
|
200
|
+
* 解析失败时返回 `{ failed: true }` 而不是抛错。同键并发调用共享
|
|
201
|
+
* 一次模块加载(抛错的模块不进 require.cache,每次都重新求值,
|
|
202
|
+
* 并发即多份重复求值与告警)。
|
|
178
203
|
*
|
|
179
204
|
* @param name 插件短名(不含 koishi-plugin- 前缀)
|
|
180
205
|
* @returns 可序列化的运行时数据
|
|
181
206
|
*/
|
|
182
|
-
|
|
207
|
+
parseExports(
|
|
208
|
+
name: string,
|
|
209
|
+
): Promise<PackageProvider.RuntimeData> {
|
|
210
|
+
const pending = (this.inflight[name] ??=
|
|
211
|
+
this.resolveExports(name).finally(() => {
|
|
212
|
+
delete this.inflight[name];
|
|
213
|
+
}));
|
|
214
|
+
return pending;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private async resolveExports(name: string) {
|
|
183
218
|
try {
|
|
184
219
|
// 插件本体之外的附加导出(配置面板消费的元信息)
|
|
185
220
|
type Extras = {
|
|
@@ -187,19 +222,25 @@ export abstract class PackageProvider extends DataService<
|
|
|
187
222
|
schema?: Schema;
|
|
188
223
|
usage?: string;
|
|
189
224
|
filter?: boolean;
|
|
190
|
-
using?:
|
|
191
|
-
|
|
225
|
+
using?:
|
|
226
|
+
| string[]
|
|
227
|
+
| { required?: string[]; optional?: string[] };
|
|
228
|
+
inject?:
|
|
229
|
+
| string[]
|
|
230
|
+
| { required?: string[]; optional?: string[] };
|
|
192
231
|
};
|
|
193
|
-
const exports = (await this.ctx.loader.resolve(
|
|
194
|
-
|
|
195
|
-
|
|
232
|
+
const exports = (await this.ctx.loader.resolve(
|
|
233
|
+
name,
|
|
234
|
+
)) as (Plugin & Extras) | undefined;
|
|
196
235
|
const result: PackageProvider.RuntimeData = {};
|
|
197
236
|
const schema = exports?.Config || exports?.schema;
|
|
198
237
|
if (schema) result.schema = schema;
|
|
199
238
|
if (exports?.usage) result.usage = exports.usage;
|
|
200
|
-
if (exports?.filter !== undefined)
|
|
239
|
+
if (exports?.filter !== undefined)
|
|
240
|
+
result.filter = exports.filter;
|
|
201
241
|
// using 与 inject 是新旧两种声明注入服务的方式
|
|
202
|
-
const inject =
|
|
242
|
+
const inject =
|
|
243
|
+
exports?.using || exports?.inject || [];
|
|
203
244
|
if (Array.isArray(inject)) {
|
|
204
245
|
result.required = inject;
|
|
205
246
|
result.optional = [];
|
|
@@ -211,7 +252,9 @@ export abstract class PackageProvider extends DataService<
|
|
|
211
252
|
// 确保 result 可以被 JSON 序列化(schema 中可能混入不可序列化的值)
|
|
212
253
|
JSON.stringify(result);
|
|
213
254
|
|
|
214
|
-
const runtime = exports
|
|
255
|
+
const runtime = exports
|
|
256
|
+
? this.ctx.registry.get(exports)
|
|
257
|
+
: undefined;
|
|
215
258
|
if (runtime) this.parseRuntime(runtime, result);
|
|
216
259
|
return result;
|
|
217
260
|
} catch (error) {
|
|
@@ -241,7 +284,10 @@ export namespace PackageProvider {
|
|
|
241
284
|
runtime?: RuntimeData;
|
|
242
285
|
package: Pick<
|
|
243
286
|
PackageJson,
|
|
244
|
-
|
|
287
|
+
| "name"
|
|
288
|
+
| "version"
|
|
289
|
+
| "peerDependencies"
|
|
290
|
+
| "peerDependenciesMeta"
|
|
245
291
|
>;
|
|
246
292
|
}
|
|
247
293
|
|
package/src/shared/services.ts
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
import { DataService } from "@koishi-ce/console";
|
|
15
15
|
import { Context, type Dict } from "@koishi-ce/koishi";
|
|
16
16
|
|
|
17
|
-
export class ServiceProvider extends DataService<
|
|
17
|
+
export class ServiceProvider extends DataService<
|
|
18
|
+
Dict<number>
|
|
19
|
+
> {
|
|
18
20
|
constructor(ctx: Context) {
|
|
19
21
|
super(ctx, "services");
|
|
20
22
|
// 任何服务的注册 / 注销都会改变统计结果
|
|
@@ -28,21 +30,28 @@ export class ServiceProvider extends DataService<Dict<number>> {
|
|
|
28
30
|
*
|
|
29
31
|
* @param internal 当前层级的 internal 对象
|
|
30
32
|
*/
|
|
31
|
-
const attach = (
|
|
33
|
+
const attach = (
|
|
34
|
+
internal: Context[typeof Context.internal],
|
|
35
|
+
) => {
|
|
32
36
|
if (!internal) return;
|
|
33
37
|
attach(Object.getPrototypeOf(internal));
|
|
34
|
-
for (const [key, { type }] of Object.entries(
|
|
38
|
+
for (const [key, { type }] of Object.entries(
|
|
39
|
+
internal,
|
|
40
|
+
)) {
|
|
35
41
|
if (type !== "service") continue;
|
|
36
42
|
const instance = this.ctx.get(key);
|
|
37
43
|
if (!(instance instanceof Object)) continue;
|
|
38
44
|
// Context.current 是个访问器属性,其值即服务实例所属的上下文
|
|
39
|
-
const ctx: Context =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
const ctx: Context =
|
|
46
|
+
Reflect.getOwnPropertyDescriptor(
|
|
47
|
+
instance,
|
|
48
|
+
Context.current,
|
|
49
|
+
)?.value;
|
|
43
50
|
if (!ctx) continue;
|
|
44
51
|
// 服务内部名形如 __foo__,对外展示时去掉首尾下划线
|
|
45
|
-
const name = key
|
|
52
|
+
const name = key
|
|
53
|
+
.replace(/^__/, "")
|
|
54
|
+
.replace(/__$/, "");
|
|
46
55
|
// 已销毁的 scope(uid 为 null)不参与统计
|
|
47
56
|
if (ctx.scope.uid !== null) {
|
|
48
57
|
services[name] = ctx.scope.uid;
|
package/src/shared/writer.ts
CHANGED
|
@@ -15,8 +15,15 @@
|
|
|
15
15
|
* 前提下增删改键"这一目标实现。
|
|
16
16
|
*/
|
|
17
17
|
import { DataService } from "@koishi-ce/console";
|
|
18
|
-
import {
|
|
19
|
-
|
|
18
|
+
import {
|
|
19
|
+
type Context,
|
|
20
|
+
Logger,
|
|
21
|
+
remove,
|
|
22
|
+
} from "@koishi-ce/koishi";
|
|
23
|
+
import {
|
|
24
|
+
Loader,
|
|
25
|
+
type LoaderScope,
|
|
26
|
+
} from "@koishi-ce/loader";
|
|
20
27
|
|
|
21
28
|
// 声明合并:浏览器端通过 WebSocket 发送的 manager/* 事件及其载荷类型。
|
|
22
29
|
// 服务端在构造函数中为每个事件注册监听器,见下方 ConfigWriter 构造函数。
|
|
@@ -29,7 +36,11 @@ declare module "@koishi-ce/console" {
|
|
|
29
36
|
target: string,
|
|
30
37
|
index: number,
|
|
31
38
|
): void;
|
|
32
|
-
"manager/reload"(
|
|
39
|
+
"manager/reload"(
|
|
40
|
+
parent: string,
|
|
41
|
+
key: string,
|
|
42
|
+
config: unknown,
|
|
43
|
+
): void;
|
|
33
44
|
"manager/unload"(
|
|
34
45
|
parent: string,
|
|
35
46
|
key: string,
|
|
@@ -80,7 +91,9 @@ function rename(
|
|
|
80
91
|
value: unknown,
|
|
81
92
|
) {
|
|
82
93
|
const keys = Object.keys(object);
|
|
83
|
-
const index = keys.findIndex(
|
|
94
|
+
const index = keys.findIndex(
|
|
95
|
+
(key) => key === old || key === `~${old}`,
|
|
96
|
+
);
|
|
84
97
|
const rest = index < 0 ? [] : keys.slice(index + 1);
|
|
85
98
|
const temp = { [neo]: value };
|
|
86
99
|
delete object[old];
|
|
@@ -95,7 +108,10 @@ function rename(
|
|
|
95
108
|
* @param plugins 插件配置对象
|
|
96
109
|
* @param name 插件键名
|
|
97
110
|
*/
|
|
98
|
-
function dropKey(
|
|
111
|
+
function dropKey(
|
|
112
|
+
plugins: Record<string, unknown>,
|
|
113
|
+
name: string,
|
|
114
|
+
) {
|
|
99
115
|
if (!(name in plugins)) {
|
|
100
116
|
name = `~${name}`;
|
|
101
117
|
}
|
|
@@ -139,7 +155,9 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
139
155
|
async (...args: unknown[]) => {
|
|
140
156
|
try {
|
|
141
157
|
// 五个方法签名各异,统一视为泛化调用目标后 apply
|
|
142
|
-
const action = this[key] as (
|
|
158
|
+
const action = this[key] as (
|
|
159
|
+
...args: unknown[]
|
|
160
|
+
) => Promise<void>;
|
|
143
161
|
await action.apply(this, args);
|
|
144
162
|
} catch (error) {
|
|
145
163
|
logger.error(error);
|
|
@@ -164,13 +182,21 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
164
182
|
* @param plugins 某层上下文的 plugins 配置对象
|
|
165
183
|
* @param ctx 该层对应的运行时上下文(用于查找 fork 记录)
|
|
166
184
|
*/
|
|
167
|
-
getGroup(
|
|
185
|
+
getGroup(
|
|
186
|
+
plugins: Record<string, unknown> | undefined,
|
|
187
|
+
ctx: Context,
|
|
188
|
+
) {
|
|
168
189
|
const result = { ...plugins };
|
|
169
190
|
for (const key in plugins ?? {}) {
|
|
170
191
|
if (key.startsWith("$")) continue;
|
|
171
192
|
// 配置值本质是任意 JSON 对象(插件配置或嵌套分组),此处按字典收窄使用
|
|
172
|
-
const value = plugins?.[key] as
|
|
173
|
-
|
|
193
|
+
const value = plugins?.[key] as
|
|
194
|
+
| Record<string, unknown>
|
|
195
|
+
| undefined;
|
|
196
|
+
const name = (key.split(":", 1)[0] ?? "").replace(
|
|
197
|
+
/^~/,
|
|
198
|
+
"",
|
|
199
|
+
);
|
|
174
200
|
|
|
175
201
|
if (!this.loader.isTruthyLike(value?.["$if"])) {
|
|
176
202
|
// $if-disabled 的插件不应显示在配置树中
|
|
@@ -180,7 +206,9 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
180
206
|
}
|
|
181
207
|
|
|
182
208
|
// 处理插件分组:分组本身是个子上下文,需要递归
|
|
183
|
-
const fork = (ctx.scope as LoaderScope)[
|
|
209
|
+
const fork = (ctx.scope as LoaderScope)[
|
|
210
|
+
Loader.kRecord
|
|
211
|
+
]?.[key];
|
|
184
212
|
if (!fork) continue;
|
|
185
213
|
if (name === "group") {
|
|
186
214
|
result[key] = this.getGroup(value, fork.ctx);
|
|
@@ -191,8 +219,13 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
191
219
|
|
|
192
220
|
/** 数据服务读取入口:返回去除敏感内部键后的整份应用配置。 */
|
|
193
221
|
override async get() {
|
|
194
|
-
const result: Context.Config = {
|
|
195
|
-
|
|
222
|
+
const result: Context.Config = {
|
|
223
|
+
...this.loader.config,
|
|
224
|
+
};
|
|
225
|
+
result.plugins = this.getGroup(
|
|
226
|
+
result.plugins,
|
|
227
|
+
this.loader.entry,
|
|
228
|
+
);
|
|
196
229
|
return result;
|
|
197
230
|
}
|
|
198
231
|
|
|
@@ -227,7 +260,8 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
227
260
|
for (const main of this.ctx.registry.values()) {
|
|
228
261
|
for (const fork of main.children) {
|
|
229
262
|
// fork 的 key 由 loader 写入(ForkScope 类型上没有该属性)
|
|
230
|
-
if ((fork as LoaderScope).key === ident)
|
|
263
|
+
if ((fork as LoaderScope).key === ident)
|
|
264
|
+
return fork;
|
|
231
265
|
}
|
|
232
266
|
}
|
|
233
267
|
return;
|
|
@@ -243,17 +277,21 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
243
277
|
*/
|
|
244
278
|
private resolveConfig(
|
|
245
279
|
ident: string,
|
|
246
|
-
config: Record<string, unknown> | undefined = this
|
|
280
|
+
config: Record<string, unknown> | undefined = this
|
|
281
|
+
.loader.config.plugins,
|
|
247
282
|
): [Record<string, unknown>, string] {
|
|
248
283
|
if (config) {
|
|
249
284
|
for (const key in config) {
|
|
250
285
|
const name = key.split(":", 1)[0] ?? "";
|
|
251
|
-
if (key.slice(name.length + 1) === ident)
|
|
286
|
+
if (key.slice(name.length + 1) === ident)
|
|
287
|
+
return [config, key];
|
|
252
288
|
if (name === "group" || name === "~group") {
|
|
253
289
|
try {
|
|
254
290
|
return this.resolveConfig(
|
|
255
291
|
ident,
|
|
256
|
-
config[key] as
|
|
292
|
+
config[key] as
|
|
293
|
+
| Record<string, unknown>
|
|
294
|
+
| undefined,
|
|
257
295
|
);
|
|
258
296
|
} catch {}
|
|
259
297
|
}
|
|
@@ -271,7 +309,10 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
271
309
|
* @param ident 插件路径标识
|
|
272
310
|
* @param config 待合并的元数据键值对
|
|
273
311
|
*/
|
|
274
|
-
async meta(
|
|
312
|
+
async meta(
|
|
313
|
+
ident: string,
|
|
314
|
+
config: Record<string, unknown>,
|
|
315
|
+
) {
|
|
275
316
|
const [parent, key] = this.resolveConfig(ident);
|
|
276
317
|
const target = parent[key] as Record<string, unknown>;
|
|
277
318
|
for (const key of Object.keys(config)) {
|
|
@@ -291,7 +332,11 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
291
332
|
* @param key 插件键名(形如 `name:ident`)
|
|
292
333
|
* @param config 新的插件配置
|
|
293
334
|
*/
|
|
294
|
-
async reload(
|
|
335
|
+
async reload(
|
|
336
|
+
parent: string,
|
|
337
|
+
key: string,
|
|
338
|
+
config: unknown,
|
|
339
|
+
) {
|
|
295
340
|
const scope = this.resolveFork(parent);
|
|
296
341
|
if (!scope) throw new Error("plugin not found");
|
|
297
342
|
await this.loader.reload(scope.ctx, key, config);
|
|
@@ -320,7 +365,11 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
320
365
|
this.loader.unload(scope.ctx, key);
|
|
321
366
|
if (index) {
|
|
322
367
|
const rest = Object.keys(scope.config).slice(index);
|
|
323
|
-
insertKey(
|
|
368
|
+
insertKey(
|
|
369
|
+
scope.config,
|
|
370
|
+
{ [`~${key}`]: config },
|
|
371
|
+
rest,
|
|
372
|
+
);
|
|
324
373
|
} else {
|
|
325
374
|
rename(scope.config, key, `~${key}`, config);
|
|
326
375
|
}
|
|
@@ -356,19 +405,29 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
356
405
|
* @param target 目标分组的路径标识
|
|
357
406
|
* @param index 目标插入位置(目标分组键序列中的序号)
|
|
358
407
|
*/
|
|
359
|
-
async teleport(
|
|
408
|
+
async teleport(
|
|
409
|
+
source: string,
|
|
410
|
+
key: string,
|
|
411
|
+
target: string,
|
|
412
|
+
index: number,
|
|
413
|
+
) {
|
|
360
414
|
const parentS = this.resolveFork(source);
|
|
361
415
|
const parentT = this.resolveFork(target);
|
|
362
|
-
if (!parentS || !parentT)
|
|
416
|
+
if (!parentS || !parentT)
|
|
417
|
+
throw new Error("plugin not found");
|
|
363
418
|
|
|
364
419
|
// 第一步:迁移运行时 fork(仅在跨分组时需要)
|
|
365
|
-
const fork = (parentS as LoaderScope)[Loader.kRecord]?.[
|
|
420
|
+
const fork = (parentS as LoaderScope)[Loader.kRecord]?.[
|
|
421
|
+
key
|
|
422
|
+
];
|
|
366
423
|
if (fork && parentS !== parentT) {
|
|
367
|
-
const sourceRecord = ((parentS as LoaderScope)[
|
|
368
|
-
|
|
424
|
+
const sourceRecord = ((parentS as LoaderScope)[
|
|
425
|
+
Loader.kRecord
|
|
426
|
+
] ??= Object.create(null));
|
|
369
427
|
delete sourceRecord[key];
|
|
370
|
-
const targetRecord = ((parentT as LoaderScope)[
|
|
371
|
-
|
|
428
|
+
const targetRecord = ((parentT as LoaderScope)[
|
|
429
|
+
Loader.kRecord
|
|
430
|
+
] ??= Object.create(null));
|
|
372
431
|
targetRecord[key] = fork;
|
|
373
432
|
remove(parentS.disposables, fork.dispose);
|
|
374
433
|
parentT.disposables.push(fork.dispose);
|
|
@@ -378,7 +437,9 @@ export class ConfigWriter extends DataService<Context.Config> {
|
|
|
378
437
|
// scope 实例是转发到 config 的 Proxy,用 Reflect.get 保持原读取语义
|
|
379
438
|
if (
|
|
380
439
|
Object.keys(fork.runtime.inject).some(
|
|
381
|
-
(name) =>
|
|
440
|
+
(name) =>
|
|
441
|
+
Reflect.get(parentS, name) !==
|
|
442
|
+
Reflect.get(parentT, name),
|
|
382
443
|
)
|
|
383
444
|
) {
|
|
384
445
|
fork.restart();
|