@aalis/plugin-webui-server 0.15.0 → 0.17.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/attention.d.ts +2 -2
- package/dist/attention.d.ts.map +1 -1
- package/dist/attention.js +5 -4
- package/dist/attention.js.map +1 -1
- package/dist/auth.d.ts +13 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +77 -27
- package/dist/auth.js.map +1 -1
- package/dist/client-switch-page.d.ts +25 -3
- package/dist/client-switch-page.d.ts.map +1 -1
- package/dist/client-switch-page.js +49 -19
- package/dist/client-switch-page.js.map +1 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +2 -1
- package/dist/gate.js.map +1 -1
- package/dist/html.d.ts +15 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +38 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +223 -244
- package/dist/index.js.map +1 -1
- package/dist/layout.d.ts +4 -3
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +5 -3
- package/dist/layout.js.map +1 -1
- package/dist/locales/en.d.ts +192 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/en.js +192 -0
- package/dist/locales/en.js.map +1 -0
- package/dist/localize.d.ts +24 -0
- package/dist/localize.d.ts.map +1 -0
- package/dist/localize.js +68 -0
- package/dist/localize.js.map +1 -0
- package/dist/page-check.d.ts.map +1 -1
- package/dist/page-check.js +32 -25
- package/dist/page-check.js.map +1 -1
- package/dist/pages.d.ts +9 -7
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +77 -61
- package/dist/pages.js.map +1 -1
- package/dist/route.d.ts +5 -2
- package/dist/route.d.ts.map +1 -1
- package/dist/route.js +5 -2
- package/dist/route.js.map +1 -1
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +4 -3
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/frames.js +3 -3
- package/dist/routes/frames.js.map +1 -1
- package/dist/routes/i18n.d.ts +20 -0
- package/dist/routes/i18n.d.ts.map +1 -0
- package/dist/routes/i18n.js +78 -0
- package/dist/routes/i18n.js.map +1 -0
- package/dist/routes/marketplace.d.ts.map +1 -1
- package/dist/routes/marketplace.js +27 -29
- package/dist/routes/marketplace.js.map +1 -1
- package/dist/routes/media.d.ts +3 -0
- package/dist/routes/media.d.ts.map +1 -1
- package/dist/routes/media.js +29 -26
- package/dist/routes/media.js.map +1 -1
- package/dist/routes/pages.d.ts +4 -2
- package/dist/routes/pages.d.ts.map +1 -1
- package/dist/routes/pages.js +36 -18
- package/dist/routes/pages.js.map +1 -1
- package/dist/routes/plugins.d.ts +5 -9
- package/dist/routes/plugins.d.ts.map +1 -1
- package/dist/routes/plugins.js +114 -129
- package/dist/routes/plugins.js.map +1 -1
- package/dist/routes/proxy.d.ts +3 -2
- package/dist/routes/proxy.d.ts.map +1 -1
- package/dist/routes/proxy.js +62 -41
- package/dist/routes/proxy.js.map +1 -1
- package/dist/texts.d.ts +383 -0
- package/dist/texts.d.ts.map +1 -0
- package/dist/texts.js +195 -0
- package/dist/texts.js.map +1 -0
- package/package.json +26 -23
package/dist/routes/plugins.js
CHANGED
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
-
import { describeUnmet, unmetDependencies } from '@aalis/api-doctor';
|
|
3
|
-
import { isConfigSaveRefused } from '@aalis/api-host-config';
|
|
2
|
+
import { describeUnmet, failureText, unmetDependencies } from '@aalis/api-doctor';
|
|
3
|
+
import { isConfigSaveRefused, saveFailureText, } from '@aalis/api-host-config';
|
|
4
4
|
import { parseInstanceId } from '@aalis/core';
|
|
5
|
-
import { CORE_CONFIG_SCHEMA, deepMergeDefaults, defaultsFrom, isSecretRef, mapSecretFields, parseConfig, removeExtraFields, validateConfig, } from '@aalis/schema-config';
|
|
5
|
+
import { CORE_CONFIG_SCHEMA, CORE_RESTART_KEYS, deepMergeDefaults, defaultsFrom, isSecretRef, mapSecretFields, parseConfig, removeExtraFields, validateConfig, } from '@aalis/schema-config';
|
|
6
|
+
import { asText, textClauses, textOf } from '@aalis/schema-i18n';
|
|
6
7
|
import { blockedStatus } from '../attention.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { renderWith, skipSchemaDefaults } from '../localize.js';
|
|
9
|
+
import { issueViews, route } from '../route.js';
|
|
10
|
+
import { t } from '../texts.js';
|
|
11
|
+
/** 没过校验的各项写成一句:逐项写路径与原因,以分号相连 */
|
|
12
|
+
const issuesText = (issues) => textClauses(issues.map(issue => t('issue', { path: issue.path, reason: issue.text })));
|
|
13
|
+
/** 回执的主句后接一句插件没落在预期状态的说明(以分号相连),没有时只是主句 */
|
|
14
|
+
const withAside = (text, aside) => (aside ? textClauses([text, aside]) : text);
|
|
15
|
+
/** 插件失败的说明:没有时写「详见日志」 */
|
|
16
|
+
const reasonOf = (entry) => failureText(entry) ?? t('seeLog');
|
|
17
|
+
/** 裁掉的未声明字段的两句括注,没有的为空 */
|
|
18
|
+
function fieldNotes(ignored, removed = []) {
|
|
19
|
+
return {
|
|
20
|
+
ignored: ignored.length > 0 ? t('ignoredNote', { fields: ignored.join(', ') }) : '',
|
|
21
|
+
removed: removed.length > 0 ? t('removedNote', { fields: removed.join(', ') }) : '',
|
|
22
|
+
};
|
|
10
23
|
}
|
|
11
|
-
/**
|
|
12
|
-
* 落盘被拒后运行态怎样与文件对齐:插件配置、新建的实例与服务偏好随配置文件热重载(文件里没有配置段的后缀实例会被卸载);
|
|
13
|
-
* 启停只在重启时按文件登记。被拒的可能是配置文件,也可能是密钥文件,拒写原因里点了名,这里不指具体哪个文件
|
|
14
|
-
*/
|
|
15
|
-
const RECONCILE = {
|
|
16
|
-
reload: '修好该文件后会按文件内容重载',
|
|
17
|
-
restart: '修好该文件后,重启时以文件内容为准',
|
|
18
|
-
};
|
|
19
24
|
/**
|
|
20
25
|
* 管理动作改完运行态之后落盘;失败时回 `applied: true` 并返回 false:改动已在运行态生效、只是没写进文件,
|
|
21
26
|
* 调用方别当成「没改成」去重试。宿主拒写(配置文件有尚未生效的外部修改)回 409,之后以文件为准对账;
|
|
22
27
|
* 其它失败(权限、磁盘写满等)回 500,文件没变也不会触发重载,改动留在文档里,下一次成功保存时一并写入。
|
|
28
|
+
* `reconcile` 是落盘被拒后运行态怎样与文件对齐:插件配置、新建的实例与服务偏好随配置文件热重载(reload,文件里没有配置段的
|
|
29
|
+
* 后缀实例会被卸载);启停只在重启时按文件登记(restart)。被拒的可能是配置文件,也可能是密钥文件,拒写原因里点了名,
|
|
30
|
+
* 这里不指具体哪个文件。
|
|
23
31
|
* `outcome` 是动作之后插件没有落在预期状态的说明(如激活失败转 error、仍在激活、保持禁用):落盘也失败时放在回执开头,
|
|
24
32
|
* 两件事一并说明。
|
|
25
33
|
*/
|
|
@@ -35,22 +43,11 @@ export async function saveAfterApply(doc, res, reconcile, outcome) {
|
|
|
35
43
|
}
|
|
36
44
|
/** 落盘失败的回执,口径见 {@link saveAfterApply};applyPluginConfig 带回的 saveError 同样用它 */
|
|
37
45
|
function reportSaveFailure(res, err, reconcile, outcome) {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
applied: true,
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
res.status(500).json({
|
|
47
|
-
error: `${lead}已在运行态生效,但写入配置文件失败(${errorMessage(err)});改动保留在文档里,下次保存成功时一并写入`,
|
|
48
|
-
applied: true,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
46
|
+
const error = saveFailureText(err, reconcile);
|
|
47
|
+
res
|
|
48
|
+
.status(isConfigSaveRefused(err) ? 409 : 500)
|
|
49
|
+
.json({ error: outcome ? textClauses([outcome, error]) : error, applied: true });
|
|
51
50
|
}
|
|
52
|
-
/** 宿主配置里要重启才生效的键:runtime 与 core 只在启动时读它们。name 只是显示用的应用名称,保存即生效 */
|
|
53
|
-
const RESTART_KEYS = ['logLevel', 'slowThresholdMs'];
|
|
54
51
|
/** 文档里每个密钥键被引用的处数:表单据此提示「替换会同时改到另外 N 处」 */
|
|
55
52
|
function countSecretRefs(value, into = {}) {
|
|
56
53
|
if (isSecretRef(value))
|
|
@@ -78,14 +75,17 @@ async function settledEntry(pm, instanceId) {
|
|
|
78
75
|
*/
|
|
79
76
|
function notYetActive(pm, services, instanceId, state) {
|
|
80
77
|
if (state === 'activating')
|
|
81
|
-
return '
|
|
78
|
+
return t('stillActivating');
|
|
82
79
|
if (state !== 'pending')
|
|
83
80
|
return undefined;
|
|
84
81
|
const status = pm.getStatus();
|
|
85
82
|
const unmet = unmetDependencies(status, services.names()).get(instanceId);
|
|
86
83
|
const lingering = !unmet?.lacking.length && status.find(entry => entry.instanceId === instanceId)?.lingering;
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
if (lingering)
|
|
85
|
+
return t('notYetActiveBecause', { why: t('waitingOld', { instance: instanceId }) });
|
|
86
|
+
return unmet && unmet.lacking.length + unmet.waiting.length > 0
|
|
87
|
+
? t('notYetActiveBecause', { why: describeUnmet(unmet) })
|
|
88
|
+
: t('notYetActive');
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
91
|
* 管理动作返回 false 时的回执。条目还在注册表里,说明是停机中或正在卸载拒绝了这次操作,回 409;
|
|
@@ -93,10 +93,10 @@ function notYetActive(pm, services, instanceId, state) {
|
|
|
93
93
|
*/
|
|
94
94
|
function refuse(pm, res, instanceId, action) {
|
|
95
95
|
if (pm.getPlugin(instanceId)) {
|
|
96
|
-
res.status(409).json({ error:
|
|
96
|
+
res.status(409).json({ error: t('refused', { instance: instanceId, action }) });
|
|
97
97
|
}
|
|
98
98
|
else {
|
|
99
|
-
res.status(404).json({ error:
|
|
99
|
+
res.status(404).json({ error: t('pluginMissing', { instance: instanceId }) });
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
/** 注册插件管理 + 全局配置相关 REST 路由 */
|
|
@@ -105,14 +105,14 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
105
105
|
const docOr503 = (res) => {
|
|
106
106
|
const doc = caps.hostConfig.present ? caps.hostConfig.use : undefined;
|
|
107
107
|
if (!doc)
|
|
108
|
-
res.status(503).json({ error: '
|
|
108
|
+
res.status(503).json({ error: t('noHostConfig') });
|
|
109
109
|
return doc;
|
|
110
110
|
};
|
|
111
111
|
/** 提供这个界面的插件经界面停掉就回不来了:拒绝停用它 */
|
|
112
112
|
const refuseSelf = (res, name) => {
|
|
113
113
|
if (name !== caps.self)
|
|
114
114
|
return false;
|
|
115
|
-
res.status(409).json({ error:
|
|
115
|
+
res.status(409).json({ error: t('selfDisable', { name }) });
|
|
116
116
|
return true;
|
|
117
117
|
};
|
|
118
118
|
/**
|
|
@@ -161,8 +161,8 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
161
161
|
// 反向索引:instanceId -> tools / commands。生产登记的 pluginName 就是 contextId(= instanceId);
|
|
162
162
|
// 按 definition.name 索引会让 name:suffix 误挂主实例的工具
|
|
163
163
|
const toolsByPlugin = new Map();
|
|
164
|
-
for (const
|
|
165
|
-
toolsByPlugin.set(
|
|
164
|
+
for (const tool of caps.tools.present ? caps.tools.use.getAll() : []) {
|
|
165
|
+
toolsByPlugin.set(tool.pluginName, [...(toolsByPlugin.get(tool.pluginName) ?? []), tool.name]);
|
|
166
166
|
}
|
|
167
167
|
const commandsByPlugin = new Map();
|
|
168
168
|
for (const c of caps.commands.present ? caps.commands.use.getAll() : []) {
|
|
@@ -182,7 +182,7 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
182
182
|
return {
|
|
183
183
|
name: p.name,
|
|
184
184
|
instanceId: p.instanceId,
|
|
185
|
-
displayName: p.displayName,
|
|
185
|
+
displayName: asText(p.displayName),
|
|
186
186
|
// 展示元数据:子系统按插件声明原样给,归哪组、组名与顺序由界面定;版本取已装的包
|
|
187
187
|
subsystem: entry?.definition?.subsystem,
|
|
188
188
|
version: versionOf(p.name),
|
|
@@ -216,9 +216,15 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
216
216
|
config: doc ? doc.getPluginConfig(p.instanceId) : mapSecretFields(schema, entry?.config ?? {}, () => undefined),
|
|
217
217
|
configSchema: schema,
|
|
218
218
|
defaultConfig: defaultsFrom(schema),
|
|
219
|
-
error: p
|
|
219
|
+
error: failureText(p),
|
|
220
220
|
};
|
|
221
221
|
});
|
|
222
|
+
// 配置值与 schema 的默认值是原样数据:形如文字对象的值换成译文,表单读回再保存时就把它改写成了字符串
|
|
223
|
+
const items = new Set(plugins);
|
|
224
|
+
const defaults = skipSchemaDefaults(plugins.map(plugin => plugin.configSchema));
|
|
225
|
+
renderWith(res, {
|
|
226
|
+
skip: (key, parent) => (items.has(parent) && (key === 'config' || key === 'defaultConfig')) || defaults(key, parent),
|
|
227
|
+
});
|
|
222
228
|
res.json({ plugins, secretRefCounts: doc ? countSecretRefs(doc.getAll().plugins) : {} });
|
|
223
229
|
});
|
|
224
230
|
// 宿主配置:文档里写了的核心键、它们的 schema、要重启才生效的键与家目录
|
|
@@ -230,13 +236,14 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
230
236
|
const config = Object.fromEntries(Object.keys(CORE_CONFIG_SCHEMA)
|
|
231
237
|
.filter(key => all[key] !== undefined)
|
|
232
238
|
.map(key => [key, all[key]]));
|
|
233
|
-
res
|
|
239
|
+
renderWith(res, { skip: skipSchemaDefaults([CORE_CONFIG_SCHEMA]) });
|
|
240
|
+
res.json({ config, schema: CORE_CONFIG_SCHEMA, restartKeys: [...CORE_RESTART_KEYS], home: caps.home });
|
|
234
241
|
});
|
|
235
242
|
// 改宿主配置的核心键:只保存,要重启才生效的键改了时回执写明,由界面出「重启以生效」,重启走 POST /api/restart
|
|
236
243
|
route(expressApp, 'PUT /api/config', gate(), async (req, res) => {
|
|
237
244
|
const updates = req.body;
|
|
238
245
|
if (!updates || typeof updates !== 'object') {
|
|
239
|
-
res.status(400).json({ error: '
|
|
246
|
+
res.status(400).json({ error: t('bodyNotObject') });
|
|
240
247
|
return;
|
|
241
248
|
}
|
|
242
249
|
const doc = docOr503(res);
|
|
@@ -246,7 +253,7 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
246
253
|
const allowed = Object.keys(CORE_CONFIG_SCHEMA);
|
|
247
254
|
const foreign = Object.keys(updates).filter(k => !allowed.includes(k));
|
|
248
255
|
if (foreign.length > 0) {
|
|
249
|
-
res.status(400).json({ error:
|
|
256
|
+
res.status(400).json({ error: t('foreignKeys', { allowed, foreign }) });
|
|
250
257
|
return;
|
|
251
258
|
}
|
|
252
259
|
const current = doc.getAll();
|
|
@@ -258,9 +265,9 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
258
265
|
const changed = allowed.filter(k => k in updates && !isDeepStrictEqual(submittedOf(k), currentOf(k)));
|
|
259
266
|
// 与插件配置路径同一把尺子:类型不符、越界或不在选项里的值(如 logLevel 传 nope)不落盘。
|
|
260
267
|
const picked = Object.fromEntries(changed.map(k => [k, submittedOf(k)]));
|
|
261
|
-
const invalid = validateConfig(CORE_CONFIG_SCHEMA, picked)
|
|
268
|
+
const invalid = validateConfig(CORE_CONFIG_SCHEMA, picked);
|
|
262
269
|
if (invalid.length > 0) {
|
|
263
|
-
res.status(400).json({ error: invalid
|
|
270
|
+
res.status(400).json({ error: issuesText(invalid) });
|
|
264
271
|
return;
|
|
265
272
|
}
|
|
266
273
|
// 写进文档的是按词汇换算后的值(加了引号的数字转成数字、写成数字的名称转成字符串):runtime 启动时
|
|
@@ -270,7 +277,7 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
270
277
|
// 提交 null 的键置为 undefined,写文件时省略
|
|
271
278
|
for (const key of changed)
|
|
272
279
|
doc.set(key, updates[key] === null ? undefined : values[key]);
|
|
273
|
-
const restartNeeded = changed.some(k =>
|
|
280
|
+
const restartNeeded = changed.some(k => CORE_RESTART_KEYS.includes(k));
|
|
274
281
|
try {
|
|
275
282
|
await doc.save();
|
|
276
283
|
}
|
|
@@ -278,14 +285,12 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
278
285
|
// 拒写与写入失败都撤回文档里的改动,免得下一次任意保存把这次没存下的修改写进文件
|
|
279
286
|
for (const key of changed)
|
|
280
287
|
doc.set(key, previous[key]);
|
|
281
|
-
res
|
|
282
|
-
.status(isConfigSaveRefused(err) ? 409 : 500)
|
|
283
|
-
.json({ error: `未写入配置文件(${errorMessage(err)}),本次修改已撤回` });
|
|
288
|
+
res.status(isConfigSaveRefused(err) ? 409 : 500).json({ error: t('configNotSaved', { error: textOf(err) }) });
|
|
284
289
|
return;
|
|
285
290
|
}
|
|
286
291
|
res.json({
|
|
287
292
|
ok: true,
|
|
288
|
-
message: restartNeeded ? '
|
|
293
|
+
message: restartNeeded ? t('savedRestart') : t('saved'),
|
|
289
294
|
restartNeeded,
|
|
290
295
|
});
|
|
291
296
|
});
|
|
@@ -294,31 +299,31 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
294
299
|
route(expressApp, 'POST /api/restart', gate(), (_req, res) => {
|
|
295
300
|
const app = caps.app.present ? caps.app.use : undefined;
|
|
296
301
|
if (!app) {
|
|
297
|
-
res.status(500).json({ error: '
|
|
302
|
+
res.status(500).json({ error: t('noApp') });
|
|
298
303
|
return;
|
|
299
304
|
}
|
|
300
305
|
try {
|
|
301
306
|
app.restart();
|
|
302
307
|
}
|
|
303
308
|
catch (err) {
|
|
304
|
-
const reason =
|
|
305
|
-
caps.logger?.error(reason);
|
|
309
|
+
const reason = t('restartFailed', { error: textOf(err) });
|
|
310
|
+
caps.logger?.error(reason.source);
|
|
306
311
|
res.status(500).json({ error: reason });
|
|
307
312
|
return;
|
|
308
313
|
}
|
|
309
|
-
res.json({ ok: true, message: '
|
|
314
|
+
res.json({ ok: true, message: t('restarting') });
|
|
310
315
|
});
|
|
311
316
|
// 更新插件配置
|
|
312
317
|
route(expressApp, 'PUT /api/plugins/:name/config', gate(), async (req, res) => {
|
|
313
318
|
const pluginName = req.params.name;
|
|
314
319
|
const newConfig = req.body?.config;
|
|
315
320
|
if (!newConfig || typeof newConfig !== 'object' || Array.isArray(newConfig)) {
|
|
316
|
-
res.status(400).json({ error: '
|
|
321
|
+
res.status(400).json({ error: t('configNotObject') });
|
|
317
322
|
return;
|
|
318
323
|
}
|
|
319
324
|
const pm = caps.plugins.present ? caps.plugins.use : undefined;
|
|
320
325
|
if (!pm) {
|
|
321
|
-
res.status(500).json({ error: '
|
|
326
|
+
res.status(500).json({ error: t('noPluginManager') });
|
|
322
327
|
return;
|
|
323
328
|
}
|
|
324
329
|
const doc = docOr503(res);
|
|
@@ -343,16 +348,15 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
343
348
|
({ merged, ignored, removed } = mergeSubmittedConfig(doc, pluginName, schema, docConfig, newConfig));
|
|
344
349
|
}
|
|
345
350
|
catch (err) {
|
|
346
|
-
res.status(400).json({ error:
|
|
351
|
+
res.status(400).json({ error: textOf(err) });
|
|
347
352
|
return;
|
|
348
353
|
}
|
|
349
354
|
// 没登记的实例不经这里新建(宿主的 applyPluginConfig 会按同模块定义登记它),建实例走 POST instances
|
|
350
355
|
if (!entry) {
|
|
351
|
-
res.status(404).json({ error:
|
|
356
|
+
res.status(404).json({ error: t('pluginMissing', { instance: pluginName }) });
|
|
352
357
|
return;
|
|
353
358
|
}
|
|
354
|
-
const
|
|
355
|
-
(removed.length > 0 ? `(已从配置文件移除未声明的字段: ${removed.join(', ')})` : '');
|
|
359
|
+
const notes = fieldNotes(ignored, removed);
|
|
356
360
|
// 保存前校验,拦新不追旧:只拒绝本次编辑**新引入**的 invalid(配错了)。
|
|
357
361
|
// 存量问题放行——否则带着历史脏值(或 schema 表达不了的多态字段,如字符串与数组两种写法都收的
|
|
358
362
|
// 字段)的插件会在 WebUI 永久存不了任何字段;启动侧 config-sync
|
|
@@ -375,13 +379,10 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
375
379
|
return left <= 0;
|
|
376
380
|
});
|
|
377
381
|
if (issues.length > 0) {
|
|
378
|
-
res.status(400).json({
|
|
379
|
-
error: `配置校验未通过:${issues.map(i => `${i.path}: ${i.message}`).join(';')}`,
|
|
380
|
-
issues,
|
|
381
|
-
});
|
|
382
|
+
res.status(400).json({ error: t('configInvalid', { issues: issuesText(issues) }), issues: issueViews(issues) });
|
|
382
383
|
return;
|
|
383
384
|
}
|
|
384
|
-
//
|
|
385
|
+
// 运行态、文档、密钥与落盘由宿主一步完成:解析出的配置与运行态相同、实例不在 error 态时不重建插件
|
|
385
386
|
// (updateConfig 即 bounce,会连带重启依赖它所提供服务的下游),照常写回文档并落盘:上一次保存可能写文件失败(500),
|
|
386
387
|
// 原样重试要能补写;自写回不会触发 watch 重载。error 态插件靠原样保存来重试激活
|
|
387
388
|
let result;
|
|
@@ -389,11 +390,11 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
389
390
|
result = await doc.applyPluginConfig(pluginName, merged);
|
|
390
391
|
}
|
|
391
392
|
catch (err) {
|
|
392
|
-
res.status(400).json({ error:
|
|
393
|
+
res.status(400).json({ error: textOf(err) });
|
|
393
394
|
return;
|
|
394
395
|
}
|
|
395
396
|
if (result.status === 'refused') {
|
|
396
|
-
refuse(pm, res, pluginName, '
|
|
397
|
+
refuse(pm, res, pluginName, 'config');
|
|
397
398
|
return;
|
|
398
399
|
}
|
|
399
400
|
// 运行态与文档都没变:回「无改动」。只有运行态没变(例如文档里补了配置段)照常按已更新回报,插件没有重建
|
|
@@ -402,18 +403,14 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
402
403
|
reportSaveFailure(res, result.saveError, 'reload');
|
|
403
404
|
return;
|
|
404
405
|
}
|
|
405
|
-
res.json({ ok: true, message:
|
|
406
|
+
res.json({ ok: true, message: t('unchanged', { name: pluginName, ...notes }), ignored, removed });
|
|
406
407
|
return;
|
|
407
408
|
}
|
|
408
409
|
// 按新配置重新激活失败、转为 error:照样写入配置(原样再存即重试激活),回执按实际状态说明。
|
|
409
410
|
// 禁用态只换上新配置、保持禁用,启用时按它激活
|
|
410
411
|
const after = await settledEntry(pm, pluginName);
|
|
411
|
-
const failure = after?.state === 'error'
|
|
412
|
-
|
|
413
|
-
: undefined;
|
|
414
|
-
const aside = after?.state === 'disabled'
|
|
415
|
-
? '插件已禁用,启用时按新配置激活'
|
|
416
|
-
: notYetActive(pm, caps.services, pluginName, after?.state);
|
|
412
|
+
const failure = after?.state === 'error' ? t('reactivateFailed', { name: pluginName, reason: reasonOf(after) }) : undefined;
|
|
413
|
+
const aside = after?.state === 'disabled' ? t('disabledNewConfig') : notYetActive(pm, caps.services, pluginName, after?.state);
|
|
417
414
|
if (result.saveError !== undefined) {
|
|
418
415
|
reportSaveFailure(res, result.saveError, 'reload', failure ?? aside);
|
|
419
416
|
return;
|
|
@@ -423,8 +420,8 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
423
420
|
res.json({
|
|
424
421
|
ok: true,
|
|
425
422
|
message: failure !== undefined
|
|
426
|
-
?
|
|
427
|
-
:
|
|
423
|
+
? textClauses([failure, t('configWritten', notes)])
|
|
424
|
+
: withAside(t('configUpdated', { name: pluginName, ...notes }), aside),
|
|
428
425
|
ignored,
|
|
429
426
|
removed,
|
|
430
427
|
});
|
|
@@ -434,7 +431,7 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
434
431
|
const pluginName = req.params.name;
|
|
435
432
|
const pm = caps.plugins.present ? caps.plugins.use : undefined;
|
|
436
433
|
if (!pm) {
|
|
437
|
-
res.status(500).json({ error: '
|
|
434
|
+
res.status(500).json({ error: t('noPluginManager') });
|
|
438
435
|
return;
|
|
439
436
|
}
|
|
440
437
|
const doc = docOr503(res);
|
|
@@ -445,28 +442,26 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
445
442
|
success = await pm.enable(pluginName);
|
|
446
443
|
}
|
|
447
444
|
catch (err) {
|
|
448
|
-
res.status(400).json({ error:
|
|
445
|
+
res.status(400).json({ error: textOf(err) });
|
|
449
446
|
return;
|
|
450
447
|
}
|
|
451
448
|
if (success) {
|
|
452
449
|
doc.setPluginEnabled(pluginName, true);
|
|
453
450
|
// 激活失败、转为 error:照样记为启用(重启时再激活),回执按实际状态说明。写进了配置文件就回 2xx,口径同改配置
|
|
454
451
|
const after = await settledEntry(pm, pluginName);
|
|
455
|
-
const failure = after?.state === 'error'
|
|
456
|
-
? `插件 ${pluginName} 激活失败,已转为 error 态(${after.error ?? '详见日志'})`
|
|
457
|
-
: undefined;
|
|
452
|
+
const failure = after?.state === 'error' ? t('activateFailed', { name: pluginName, reason: reasonOf(after) }) : undefined;
|
|
458
453
|
const aside = notYetActive(pm, caps.services, pluginName, after?.state);
|
|
459
454
|
if (!(await saveAfterApply(doc, res, 'restart', failure ?? aside)))
|
|
460
455
|
return;
|
|
461
456
|
res.json({
|
|
462
457
|
ok: true,
|
|
463
458
|
message: failure !== undefined
|
|
464
|
-
?
|
|
465
|
-
:
|
|
459
|
+
? textClauses([failure, t('markedEnabled')])
|
|
460
|
+
: withAside(t('enabled', { name: pluginName }), aside),
|
|
466
461
|
});
|
|
467
462
|
}
|
|
468
463
|
else {
|
|
469
|
-
refuse(pm, res, pluginName, '
|
|
464
|
+
refuse(pm, res, pluginName, 'enable');
|
|
470
465
|
}
|
|
471
466
|
});
|
|
472
467
|
// 禁用插件
|
|
@@ -476,7 +471,7 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
476
471
|
return;
|
|
477
472
|
const pm = caps.plugins.present ? caps.plugins.use : undefined;
|
|
478
473
|
if (!pm) {
|
|
479
|
-
res.status(500).json({ error: '
|
|
474
|
+
res.status(500).json({ error: t('noPluginManager') });
|
|
480
475
|
return;
|
|
481
476
|
}
|
|
482
477
|
const doc = docOr503(res);
|
|
@@ -487,24 +482,24 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
487
482
|
success = await pm.disable(pluginName);
|
|
488
483
|
}
|
|
489
484
|
catch (err) {
|
|
490
|
-
res.status(400).json({ error:
|
|
485
|
+
res.status(400).json({ error: textOf(err) });
|
|
491
486
|
return;
|
|
492
487
|
}
|
|
493
488
|
if (success) {
|
|
494
489
|
// 已禁用,但旧实例(如不响应 abort 的初始化)超过宽限仍没停下:回执说明这一点。写进了配置文件就回 2xx,口径同改配置
|
|
495
490
|
const stuck = pm.getStatus().find(entry => entry.instanceId === pluginName)?.lingering
|
|
496
|
-
?
|
|
491
|
+
? t('disabledStuck', { name: pluginName })
|
|
497
492
|
: undefined;
|
|
498
493
|
doc.setPluginEnabled(pluginName, false);
|
|
499
494
|
if (!(await saveAfterApply(doc, res, 'restart', stuck)))
|
|
500
495
|
return;
|
|
501
496
|
res.json({
|
|
502
497
|
ok: true,
|
|
503
|
-
message: stuck !== undefined ?
|
|
498
|
+
message: stuck !== undefined ? textClauses([stuck, t('markedDisabled')]) : t('disabled', { name: pluginName }),
|
|
504
499
|
});
|
|
505
500
|
}
|
|
506
501
|
else {
|
|
507
|
-
refuse(pm, res, pluginName, '
|
|
502
|
+
refuse(pm, res, pluginName, 'disable');
|
|
508
503
|
}
|
|
509
504
|
});
|
|
510
505
|
// 重载:拆掉当前激活再按原配置重新激活,不写配置文件;失败的插件重载即重试
|
|
@@ -512,11 +507,11 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
512
507
|
const pluginName = req.params.name;
|
|
513
508
|
const pm = caps.plugins.present ? caps.plugins.use : undefined;
|
|
514
509
|
if (!pm) {
|
|
515
|
-
res.status(500).json({ error: '
|
|
510
|
+
res.status(500).json({ error: t('noPluginManager') });
|
|
516
511
|
return;
|
|
517
512
|
}
|
|
518
513
|
if (pm.getPlugin(pluginName)?.state === 'disabled') {
|
|
519
|
-
res.status(409).json({ error:
|
|
514
|
+
res.status(409).json({ error: t('reloadDisabled', { name: pluginName }) });
|
|
520
515
|
return;
|
|
521
516
|
}
|
|
522
517
|
let success;
|
|
@@ -524,11 +519,11 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
524
519
|
success = await pm.bounce(pluginName);
|
|
525
520
|
}
|
|
526
521
|
catch (err) {
|
|
527
|
-
res.status(400).json({ error:
|
|
522
|
+
res.status(400).json({ error: textOf(err) });
|
|
528
523
|
return;
|
|
529
524
|
}
|
|
530
525
|
if (!success) {
|
|
531
|
-
refuse(pm, res, pluginName, '
|
|
526
|
+
refuse(pm, res, pluginName, 'reload');
|
|
532
527
|
return;
|
|
533
528
|
}
|
|
534
529
|
const after = await settledEntry(pm, pluginName);
|
|
@@ -536,24 +531,23 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
536
531
|
res.json({
|
|
537
532
|
ok: true,
|
|
538
533
|
message: after?.state === 'error'
|
|
539
|
-
?
|
|
540
|
-
:
|
|
534
|
+
? t('reloadFailed', { name: pluginName, reason: reasonOf(after) })
|
|
535
|
+
: withAside(t('reloaded', { name: pluginName }), aside),
|
|
541
536
|
});
|
|
542
537
|
});
|
|
543
538
|
// 经宿主 plugin-source 重新扫描并加载新插件
|
|
544
539
|
route(expressApp, 'POST /api/plugins/scan', gate(), async (_req, res) => {
|
|
545
540
|
if (!caps.source.present) {
|
|
546
|
-
res.status(503).json({ error: '
|
|
541
|
+
res.status(503).json({ error: t('noPluginSource') });
|
|
547
542
|
return;
|
|
548
543
|
}
|
|
549
544
|
const source = caps.source.use;
|
|
550
545
|
try {
|
|
551
546
|
const loaded = await source.rescan();
|
|
552
|
-
res.json({ ok: true, loaded, message:
|
|
547
|
+
res.json({ ok: true, loaded, message: t('scanned', { count: loaded.length }) });
|
|
553
548
|
}
|
|
554
549
|
catch (err) {
|
|
555
|
-
|
|
556
|
-
res.status(500).json({ error: msg });
|
|
550
|
+
res.status(500).json({ error: textOf(err) });
|
|
557
551
|
}
|
|
558
552
|
});
|
|
559
553
|
// 创建插件多实例
|
|
@@ -562,20 +556,20 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
562
556
|
const suffix = req.body?.suffix;
|
|
563
557
|
const config = req.body?.config ?? {};
|
|
564
558
|
if (!suffix || typeof suffix !== 'string') {
|
|
565
|
-
res.status(400).json({ error: '
|
|
559
|
+
res.status(400).json({ error: t('suffixRequired') });
|
|
566
560
|
return;
|
|
567
561
|
}
|
|
568
562
|
if (!/^[\w-]+$/.test(suffix)) {
|
|
569
|
-
res.status(400).json({ error: '
|
|
563
|
+
res.status(400).json({ error: t('suffixInvalid') });
|
|
570
564
|
return;
|
|
571
565
|
}
|
|
572
566
|
if (typeof config !== 'object' || Array.isArray(config)) {
|
|
573
|
-
res.status(400).json({ error: '
|
|
567
|
+
res.status(400).json({ error: t('configNotObject') });
|
|
574
568
|
return;
|
|
575
569
|
}
|
|
576
570
|
const pm = caps.plugins.present ? caps.plugins.use : undefined;
|
|
577
571
|
if (!pm) {
|
|
578
|
-
res.status(500).json({ error: '
|
|
572
|
+
res.status(500).json({ error: t('noPluginManager') });
|
|
579
573
|
return;
|
|
580
574
|
}
|
|
581
575
|
const doc = docOr503(res);
|
|
@@ -591,16 +585,16 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
591
585
|
.map(p => pm.getPlugin(p.instanceId)?.definition)
|
|
592
586
|
.find(m => m?.name === moduleName);
|
|
593
587
|
if (!sourceModule) {
|
|
594
|
-
res.status(400).json({ error:
|
|
588
|
+
res.status(400).json({ error: t('moduleMissing', { module: moduleName }) });
|
|
595
589
|
return;
|
|
596
590
|
}
|
|
597
591
|
if (!sourceModule.reusable) {
|
|
598
|
-
res.status(400).json({ error:
|
|
592
|
+
res.status(400).json({ error: t('moduleNotReusable', { module: moduleName }) });
|
|
599
593
|
return;
|
|
600
594
|
}
|
|
601
595
|
const instanceId = `${moduleName}:${suffix}`;
|
|
602
596
|
if (pm.getPlugin(instanceId)) {
|
|
603
|
-
res.status(400).json({ error:
|
|
597
|
+
res.status(400).json({ error: t('instanceExists', { instance: instanceId }) });
|
|
604
598
|
return;
|
|
605
599
|
}
|
|
606
600
|
// 与改配置同一套合并与裁剪,基线是空对象。校验不分存量:新实例没有历史配置,类型不符的值一律拒绝,
|
|
@@ -609,36 +603,29 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
609
603
|
const { merged: mergedConfig, ignored } = mergeSubmittedConfig(doc, instanceId, schema, {}, config);
|
|
610
604
|
const issues = validateConfig(schema, mergedConfig).filter(i => i.kind === 'invalid');
|
|
611
605
|
if (issues.length > 0) {
|
|
612
|
-
res.status(400).json({
|
|
613
|
-
error: `配置校验未通过:${issues.map(i => `${i.path}: ${i.message}`).join(';')}`,
|
|
614
|
-
issues,
|
|
615
|
-
});
|
|
606
|
+
res.status(400).json({ error: t('configInvalid', { issues: issuesText(issues) }), issues: issueViews(issues) });
|
|
616
607
|
return;
|
|
617
608
|
}
|
|
618
|
-
const
|
|
609
|
+
const notes = fieldNotes(ignored);
|
|
619
610
|
// 宿主先写文档再登记(文档里残留的禁用标记照旧生效,以禁用态登记),没登记上时撤回文档与密钥
|
|
620
611
|
let result;
|
|
621
612
|
try {
|
|
622
613
|
result = await doc.applyPluginConfig(instanceId, mergedConfig);
|
|
623
614
|
}
|
|
624
615
|
catch (err) {
|
|
625
|
-
res.status(400).json({ error:
|
|
616
|
+
res.status(400).json({ error: textOf(err) });
|
|
626
617
|
return;
|
|
627
618
|
}
|
|
628
619
|
if (result.status === 'refused') {
|
|
629
620
|
// 前面已查过重名与 reusable,走到这里的实际只有停机进行中:配置不落盘,免得下次启动按文件登记
|
|
630
|
-
res.status(409).json({ error:
|
|
621
|
+
res.status(409).json({ error: t('instanceRefused', { instance: instanceId }) });
|
|
631
622
|
return;
|
|
632
623
|
}
|
|
633
624
|
// 激活失败、转为 error:实例与配置照样保留并落盘(改配置保存即重试激活),回执按实际状态说明。
|
|
634
625
|
// 写进了配置文件就回 2xx,口径同改配置:实例已登记,表单该收起,留着再点「创建」只会得到「已存在」
|
|
635
626
|
const after = await settledEntry(pm, instanceId);
|
|
636
|
-
const failure = after?.state === 'error'
|
|
637
|
-
|
|
638
|
-
: undefined;
|
|
639
|
-
const aside = after?.state === 'disabled'
|
|
640
|
-
? '配置文件的 disabledPlugins 里有它,已按禁用态登记,启用后激活'
|
|
641
|
-
: notYetActive(pm, caps.services, instanceId, after?.state);
|
|
627
|
+
const failure = after?.state === 'error' ? t('instanceFailed', { instance: instanceId, reason: reasonOf(after) }) : undefined;
|
|
628
|
+
const aside = after?.state === 'disabled' ? t('instanceDisabled') : notYetActive(pm, caps.services, instanceId, after?.state);
|
|
642
629
|
if (result.saveError !== undefined) {
|
|
643
630
|
reportSaveFailure(res, result.saveError, 'reload', failure ?? aside);
|
|
644
631
|
return;
|
|
@@ -647,8 +634,8 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
647
634
|
ok: true,
|
|
648
635
|
instanceId,
|
|
649
636
|
message: failure !== undefined
|
|
650
|
-
?
|
|
651
|
-
:
|
|
637
|
+
? textClauses([failure, t('configWritten', notes)])
|
|
638
|
+
: withAside(t('instanceCreated', { instance: instanceId, ignored: notes.ignored }), aside),
|
|
652
639
|
ignored,
|
|
653
640
|
});
|
|
654
641
|
});
|
|
@@ -657,7 +644,7 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
657
644
|
const instanceId = req.params.instanceId;
|
|
658
645
|
const pm = caps.plugins.present ? caps.plugins.use : undefined;
|
|
659
646
|
if (!pm) {
|
|
660
|
-
res.status(500).json({ error: '
|
|
647
|
+
res.status(500).json({ error: t('noPluginManager') });
|
|
661
648
|
return;
|
|
662
649
|
}
|
|
663
650
|
const doc = docOr503(res);
|
|
@@ -668,7 +655,7 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
668
655
|
// 触发的热重载会按仍带配置段的文件把实例重新登记,落盘随后删掉配置段,留下文件里没有的在跑实例
|
|
669
656
|
const { suffix } = parseInstanceId(instanceId);
|
|
670
657
|
if (!suffix || !pm.getPlugin(instanceId)) {
|
|
671
|
-
res.status(400).json({ error:
|
|
658
|
+
res.status(400).json({ error: t('cannotDelete') });
|
|
672
659
|
return;
|
|
673
660
|
}
|
|
674
661
|
const previous = Object.hasOwn(doc.getAll().plugins, instanceId) ? doc.getPluginConfig(instanceId) : undefined;
|
|
@@ -685,7 +672,7 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
685
672
|
doc.setPluginEnabled(instanceId, !wasDisabled);
|
|
686
673
|
res
|
|
687
674
|
.status(isConfigSaveRefused(err) ? 409 : 500)
|
|
688
|
-
.json({ error:
|
|
675
|
+
.json({ error: t('deleteNotSaved', { instance: instanceId, error: textOf(err) }) });
|
|
689
676
|
return;
|
|
690
677
|
}
|
|
691
678
|
let unloaded;
|
|
@@ -693,17 +680,15 @@ export function registerPluginRoutes(expressApp, caps, gate) {
|
|
|
693
680
|
unloaded = await pm.unload(instanceId);
|
|
694
681
|
}
|
|
695
682
|
catch (err) {
|
|
696
|
-
res.status(500).json({
|
|
697
|
-
error: `已从配置文件删除实例 ${instanceId},但卸载失败(${errorMessage(err)});重启后不再登记`,
|
|
698
|
-
});
|
|
683
|
+
res.status(500).json({ error: t('unloadFailed', { instance: instanceId, error: textOf(err) }) });
|
|
699
684
|
return;
|
|
700
685
|
}
|
|
701
686
|
// 返回 false 而条目还在:停机中拒绝了卸载,配置段已删、实例仍在跑到停机为止。条目已不在的是别处先卸掉了,照常回已删除
|
|
702
687
|
if (!unloaded && pm.getPlugin(instanceId)) {
|
|
703
|
-
res.json({ ok: true, message:
|
|
688
|
+
res.json({ ok: true, message: t('deletedShuttingDown', { instance: instanceId }) });
|
|
704
689
|
return;
|
|
705
690
|
}
|
|
706
|
-
res.json({ ok: true, message:
|
|
691
|
+
res.json({ ok: true, message: t('deleted', { instance: instanceId }) });
|
|
707
692
|
});
|
|
708
693
|
}
|
|
709
694
|
//# sourceMappingURL=plugins.js.map
|