@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.
Files changed (79) hide show
  1. package/dist/attention.d.ts +2 -2
  2. package/dist/attention.d.ts.map +1 -1
  3. package/dist/attention.js +5 -4
  4. package/dist/attention.js.map +1 -1
  5. package/dist/auth.d.ts +13 -4
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/auth.js +77 -27
  8. package/dist/auth.js.map +1 -1
  9. package/dist/client-switch-page.d.ts +25 -3
  10. package/dist/client-switch-page.d.ts.map +1 -1
  11. package/dist/client-switch-page.js +49 -19
  12. package/dist/client-switch-page.js.map +1 -1
  13. package/dist/gate.d.ts.map +1 -1
  14. package/dist/gate.js +2 -1
  15. package/dist/gate.js.map +1 -1
  16. package/dist/html.d.ts +15 -0
  17. package/dist/html.d.ts.map +1 -0
  18. package/dist/html.js +38 -0
  19. package/dist/html.js.map +1 -0
  20. package/dist/index.d.ts +4 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +223 -244
  23. package/dist/index.js.map +1 -1
  24. package/dist/layout.d.ts +4 -3
  25. package/dist/layout.d.ts.map +1 -1
  26. package/dist/layout.js +5 -3
  27. package/dist/layout.js.map +1 -1
  28. package/dist/locales/en.d.ts +192 -0
  29. package/dist/locales/en.d.ts.map +1 -0
  30. package/dist/locales/en.js +192 -0
  31. package/dist/locales/en.js.map +1 -0
  32. package/dist/localize.d.ts +24 -0
  33. package/dist/localize.d.ts.map +1 -0
  34. package/dist/localize.js +68 -0
  35. package/dist/localize.js.map +1 -0
  36. package/dist/page-check.d.ts.map +1 -1
  37. package/dist/page-check.js +32 -25
  38. package/dist/page-check.js.map +1 -1
  39. package/dist/pages.d.ts +9 -7
  40. package/dist/pages.d.ts.map +1 -1
  41. package/dist/pages.js +77 -61
  42. package/dist/pages.js.map +1 -1
  43. package/dist/route.d.ts +5 -2
  44. package/dist/route.d.ts.map +1 -1
  45. package/dist/route.js +5 -2
  46. package/dist/route.js.map +1 -1
  47. package/dist/routes/files.d.ts.map +1 -1
  48. package/dist/routes/files.js +4 -3
  49. package/dist/routes/files.js.map +1 -1
  50. package/dist/routes/frames.js +3 -3
  51. package/dist/routes/frames.js.map +1 -1
  52. package/dist/routes/i18n.d.ts +20 -0
  53. package/dist/routes/i18n.d.ts.map +1 -0
  54. package/dist/routes/i18n.js +78 -0
  55. package/dist/routes/i18n.js.map +1 -0
  56. package/dist/routes/marketplace.d.ts.map +1 -1
  57. package/dist/routes/marketplace.js +27 -29
  58. package/dist/routes/marketplace.js.map +1 -1
  59. package/dist/routes/media.d.ts +3 -0
  60. package/dist/routes/media.d.ts.map +1 -1
  61. package/dist/routes/media.js +29 -26
  62. package/dist/routes/media.js.map +1 -1
  63. package/dist/routes/pages.d.ts +4 -2
  64. package/dist/routes/pages.d.ts.map +1 -1
  65. package/dist/routes/pages.js +36 -18
  66. package/dist/routes/pages.js.map +1 -1
  67. package/dist/routes/plugins.d.ts +5 -9
  68. package/dist/routes/plugins.d.ts.map +1 -1
  69. package/dist/routes/plugins.js +114 -129
  70. package/dist/routes/plugins.js.map +1 -1
  71. package/dist/routes/proxy.d.ts +3 -2
  72. package/dist/routes/proxy.d.ts.map +1 -1
  73. package/dist/routes/proxy.js +62 -41
  74. package/dist/routes/proxy.js.map +1 -1
  75. package/dist/texts.d.ts +383 -0
  76. package/dist/texts.d.ts.map +1 -0
  77. package/dist/texts.js +195 -0
  78. package/dist/texts.js.map +1 -0
  79. package/package.json +26 -23
@@ -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 { route } from '../route.js';
8
- function errorMessage(err) {
9
- return err instanceof Error ? err.message : String(err);
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 lead = outcome ? `${outcome};` : '';
39
- if (isConfigSaveRefused(err)) {
40
- res.status(409).json({
41
- error: `${lead}已在运行态生效,但未写入配置文件(${errorMessage(err)});${RECONCILE[reconcile]}`,
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
- const why = lingering ? `等待 ${instanceId} 的旧实例停下` : unmet && describeUnmet(unmet);
88
- return why ? `尚未激活:${why}` : '尚未激活,结果以插件列表为准';
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: `插件 ${instanceId} 当前不接受${action}(停机中或正在卸载),配置未更改` });
96
+ res.status(409).json({ error: t('refused', { instance: instanceId, action }) });
97
97
  }
98
98
  else {
99
- res.status(404).json({ error: `插件 ${instanceId} 不存在` });
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: '宿主未提供配置文档(host-config),无法读写配置' });
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: `插件 ${name} 提供这个界面,不能从界面停用` });
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 t of caps.tools.present ? caps.tools.use.getAll() : []) {
165
- toolsByPlugin.set(t.pluginName, [...(toolsByPlugin.get(t.pluginName) ?? []), t.name]);
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.error,
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.json({ config, schema: CORE_CONFIG_SCHEMA, restartKeys: [...RESTART_KEYS], home: caps.home });
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: `只能改核心键(${allowed.join('、')}),不收:${foreign.join('、')}` });
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).map(i => `${i.path}: ${i.message}`);
268
+ const invalid = validateConfig(CORE_CONFIG_SCHEMA, picked);
262
269
  if (invalid.length > 0) {
263
- res.status(400).json({ error: invalid.join('; ') });
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 => RESTART_KEYS.includes(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: 'App 不可用' });
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 = `重启失败(${errorMessage(err)}),改动在下次启动时生效`;
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: 'config 字段必须是对象' });
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: errorMessage(err) });
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: `插件 ${pluginName} 不存在` });
356
+ res.status(404).json({ error: t('pluginMissing', { instance: pluginName }) });
352
357
  return;
353
358
  }
354
- const note = (ignored.length > 0 ? `(已忽略未声明的配置字段: ${ignored.join(', ')})` : '') +
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
- // 运行态、文档、密钥与落盘由宿主一步完成:解析出的配置与运行态相同、实例在 active 时不重建插件
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: errorMessage(err) });
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: `插件 ${pluginName} 配置无改动,已写回配置文件${note}`, ignored, removed });
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
- ? `插件 ${pluginName} 按新配置重新激活失败,已转为 error 态(${after.error ?? '详见日志'})`
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
- ? `${failure};配置已写入配置文件${note}`
427
- : `插件 ${pluginName} 配置已更新${note}${aside ? `;${aside}` : ''}`,
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: errorMessage(err) });
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
- ? `${failure};配置文件已记为启用`
465
- : `插件 ${pluginName} 已启用${aside ? `;${aside}` : ''}`,
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: errorMessage(err) });
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
- ? `插件 ${pluginName} 已禁用,但超过宽限仍未停下,详见日志`
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 ? `${stuck};配置文件已记为禁用` : `插件 ${pluginName} 已禁用`,
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: `插件 ${pluginName} 已停用,启用后才能重载` });
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: errorMessage(err) });
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
- ? `插件 ${pluginName} 重新激活失败,已转为 error 态(${after.error ?? '详见日志'})`
540
- : `插件 ${pluginName} 已重载${aside ? `;${aside}` : ''}`,
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: loaded.length > 0 ? `新加载 ${loaded.length} 个插件` : '无新插件' });
547
+ res.json({ ok: true, loaded, message: t('scanned', { count: loaded.length }) });
553
548
  }
554
549
  catch (err) {
555
- const msg = err instanceof Error ? err.message : String(err);
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: 'suffix 必须是非空字符串' });
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: 'suffix 只能包含字母、数字、下划线和连字符' });
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: 'config 字段必须是对象' });
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: `无法创建实例:模块 "${moduleName}" 未找到` });
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: `无法创建实例:模块 "${moduleName}" 未声明 reusable` });
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: `无法创建实例:"${instanceId}" 已存在` });
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 note = ignored.length > 0 ? `(已忽略未声明的配置字段: ${ignored.join(', ')})` : '';
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: errorMessage(err) });
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: `实例 ${instanceId} 未登记(停机中或被拒,见日志),配置未写入` });
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
- ? `已创建实例 ${instanceId},但激活失败,已转为 error 态(${after.error ?? '详见日志'})`
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
- ? `${failure};配置已写入配置文件${note}`
651
- : `已创建实例 ${instanceId}${note}${aside ? `;${aside}` : ''}`,
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: `未删除实例 ${instanceId}:未写入配置文件(${errorMessage(err)})` });
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: `已从配置文件删除实例 ${instanceId};停机中,未卸载运行中的实例` });
688
+ res.json({ ok: true, message: t('deletedShuttingDown', { instance: instanceId }) });
704
689
  return;
705
690
  }
706
- res.json({ ok: true, message: `已删除实例 ${instanceId}` });
691
+ res.json({ ok: true, message: t('deleted', { instance: instanceId }) });
707
692
  });
708
693
  }
709
694
  //# sourceMappingURL=plugins.js.map