mioku-service-webui 4.12.0 → 4.14.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/database.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { getConfigDefaults, withConfigDefaults } from "mioku";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import { Database } from "bun:sqlite";
@@ -359,7 +360,12 @@ export function getPluginConfigs(pluginName: string): Record<string, any> {
359
360
 
360
361
  for (const file of files) {
361
362
  const filePath = path.join(pluginDir, file);
362
- result[path.basename(file, ".json")] = readJsonFile(filePath, {});
363
+ const configName = path.basename(file, ".json");
364
+ // 磁盘值覆盖插件登记的默认值,缺字段时页面也能拿到完整配置
365
+ result[configName] = withConfigDefaults(
366
+ readJsonFile(filePath, {}),
367
+ getConfigDefaults("plugin", pluginName, configName),
368
+ );
363
369
  }
364
370
 
365
371
  return result;
package/index.ts CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  createDataManagementRoutes,
38
38
  createStrangerRoutes,
39
39
  createAccessControlRoutes,
40
+ createMcpRoutes,
40
41
  } from "./routes";
41
42
  import { freePort } from "./port-utils";
42
43
 
@@ -132,6 +133,7 @@ class WebUIRuntime {
132
133
  this.app.route("/api/data-management", createDataManagementRoutes());
133
134
  this.app.route("/api/stranger", createStrangerRoutes());
134
135
  this.app.route("/api/access-control", createAccessControlRoutes());
136
+ this.app.route("/api/mcp", createMcpRoutes());
135
137
 
136
138
  this.app.get(
137
139
  "/api/ws/logs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-service-webui",
3
- "version": "4.12.0",
3
+ "version": "4.14.0",
4
4
  "description": "Mioku WebUI 服务",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -26,11 +26,18 @@
26
26
  "systeminformation": "^5.31.6"
27
27
  },
28
28
  "peerDependencies": {
29
- "mioku-service-ai": "^4.0.0"
29
+ "mioku-service-ai": "^4.0.0",
30
+ "mioku-service-mcp": "^0.1.0"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/bun": "^1.3.14",
33
34
  "mioku": "workspace:*",
34
- "mioku-service-ai": "workspace:*"
35
+ "mioku-service-ai": "workspace:*",
36
+ "mioku-service-mcp": "workspace:*"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "mioku-service-mcp": {
40
+ "optional": true
41
+ }
35
42
  }
36
43
  }
package/routes/agent.ts CHANGED
@@ -31,74 +31,44 @@ function sanitizeAccess(input: any): any {
31
31
  };
32
32
  }
33
33
 
34
+ /**
35
+ * 保存时只做「不该写进这个文件」的剔除,不补默认值。
36
+ *
37
+ * 默认值由 agent 插件在 `registerConfig` 时登记、读取时深合并
38
+ * (`mergeAgentConfig`)——这里再抄一份,加字段时就会两边漂移。
39
+ */
34
40
  function sanitizeBaseForSave(body: any): any {
35
41
  if (!body || typeof body !== "object") return body;
36
42
  const next = { ...body };
37
43
  delete next.persona;
38
44
  delete next.roleBindings;
39
- next.access = sanitizeAccess(next.access);
40
- next.workspaceDir = String(next.workspaceDir ?? "").trim();
41
- next.permissionLevel = PERMISSION_LEVELS.has(next.permissionLevel)
42
- ? next.permissionLevel
43
- : "workspace-write";
44
- next.model = String(next.model ?? "").trim();
45
+ if (next.access !== undefined) next.access = sanitizeAccess(next.access);
46
+ if (typeof next.workspaceDir === "string") {
47
+ next.workspaceDir = next.workspaceDir.trim();
48
+ }
49
+ if (typeof next.model === "string") next.model = next.model.trim();
50
+ if (!PERMISSION_LEVELS.has(next.permissionLevel)) delete next.permissionLevel;
45
51
  return next;
46
52
  }
47
53
 
48
54
  function sanitizeSettingsForSave(body: any): any {
49
55
  if (!body || typeof body !== "object") return body;
50
56
  const next = { ...body };
51
- const positive = (value: unknown, fallback: number) => {
52
- const num = Math.floor(Number(value));
53
- return Number.isFinite(num) && num > 0 ? num : fallback;
54
- };
55
- const nonNegative = (value: unknown, fallback: number) => {
56
- const num = Math.floor(Number(value));
57
- return Number.isFinite(num) && num >= 0 ? num : fallback;
58
- };
57
+ // 这两个属于 chat 插件的 base 配置,老版本 UI 可能还会带上来
59
58
  delete next.temperature;
60
59
  delete next.enableMediaRecognition;
61
- next.maxIterations = positive(next.maxIterations, 500);
62
- next.maxContextTokens = positive(next.maxContextTokens, 524288);
63
- next.stream = next.stream !== false;
64
- next.enableMarkdownScreenshot = next.enableMarkdownScreenshot !== false;
65
- next.compaction = {
66
- enabled: next.compaction?.enabled !== false,
67
- keepRecentMessages: positive(next.compaction?.keepRecentMessages, 20),
68
- };
69
- next.webSearch = {
70
- enabled: next.webSearch?.enabled !== false,
71
- baseUrl: String(next.webSearch?.baseUrl ?? "").trim(),
72
- timeoutMs: positive(next.webSearch?.timeoutMs, 8000),
73
- defaultLimit: positive(next.webSearch?.defaultLimit, 5),
74
- maxLimit: positive(next.webSearch?.maxLimit, 8),
75
- maxSearchCount: positive(next.webSearch?.maxSearchCount, 50),
76
- };
77
- next.webFetch = {
78
- enabled: next.webFetch?.enabled !== false,
79
- timeoutMs: positive(next.webFetch?.timeoutMs, 15000),
80
- maxChars: positive(next.webFetch?.maxChars, 12000),
81
- };
82
- next.bash = {
83
- enabled: next.bash?.enabled !== false,
84
- timeoutMs: positive(next.bash?.timeoutMs, 120000),
85
- approvalTimeoutMs: positive(next.bash?.approvalTimeoutMs, 300000),
86
- };
87
- next.skills = {
88
- enabled: next.skills?.enabled !== false,
89
- extraRoots: Array.isArray(next.skills?.extraRoots)
90
- ? next.skills.extraRoots
91
- .map((item: unknown) => String(item ?? "").trim())
92
- .filter(Boolean)
93
- : [],
94
- maxInPrompt: positive(next.skills?.maxInPrompt, 50),
95
- descriptionMaxLength: positive(next.skills?.descriptionMaxLength, 500),
96
- activationTtlMs: positive(next.skills?.activationTtlMs, 3_600_000),
97
- rescanIntervalMs: nonNegative(next.skills?.rescanIntervalMs, 2000),
98
- maxResourceFiles: positive(next.skills?.maxResourceFiles, 200),
99
- };
100
- next.dataCollection = { enabled: next.dataCollection?.enabled !== false };
101
- next.debug = Boolean(next.debug);
60
+ if (
61
+ next.skills &&
62
+ typeof next.skills === "object" &&
63
+ Array.isArray(next.skills.extraRoots)
64
+ ) {
65
+ next.skills = {
66
+ ...next.skills,
67
+ extraRoots: next.skills.extraRoots
68
+ .map((item: unknown) => String(item ?? "").trim())
69
+ .filter(Boolean),
70
+ };
71
+ }
102
72
  return next;
103
73
  }
104
74
 
@@ -123,6 +93,20 @@ export function createAgentRoutes() {
123
93
  return c.json({ ok: true, data: updateAgentConfig("base.json", body) });
124
94
  });
125
95
 
96
+ // 记忆配置由 agent 插件自己归一化,这里只做读写
97
+ app.get("/memory", (c) =>
98
+ c.json({ ok: true, data: getAgentConfig("memory.json") }),
99
+ );
100
+
101
+ app.put("/memory", async (c) => {
102
+ const body = await c.req.json();
103
+ logger.info(`[webui-action] agent.memory.update`, {
104
+ enabled: body?.enabled,
105
+ retentionMonths: body?.retentionMonths,
106
+ });
107
+ return c.json({ ok: true, data: updateAgentConfig("memory.json", body) });
108
+ });
109
+
126
110
  app.get("/settings", (c) =>
127
111
  c.json({ ok: true, data: getAgentConfig("settings.json") }),
128
112
  );
package/routes/ai.ts CHANGED
@@ -5,12 +5,6 @@ import { listStickerCatalog } from "../database";
5
5
  import aiService from "mioku-service-ai";
6
6
  import type { AIModelRole, AIProtocol, AIUsageRange } from "mioku";
7
7
 
8
- const DEFAULT_MEMORY_GROUP_HISTORY_LIMIT = 300;
9
- const DEFAULT_MEMORY_USER_HISTORY_LIMIT = 100;
10
- const DEFAULT_TOPIC_WINDOW_HOURS = 5;
11
- const DEFAULT_TOPIC_HISTORY_WINDOW_COUNT = 3;
12
- const DEFAULT_EXPRESSION_LEARN_AFTER_MESSAGES = 100;
13
- const DEFAULT_EXPRESSION_SAMPLE_SIZE = 8;
14
8
  const usageRanges = new Set<AIUsageRange>(["today", "7d", "30d"]);
15
9
  const protocols = new Set<AIProtocol>([
16
10
  "openai-chat",
@@ -24,111 +18,50 @@ function getAIApi() {
24
18
  return aiService?.api as any;
25
19
  }
26
20
 
27
- function normalizePersonalizationConfig(input: any): any {
28
- const data =
29
- input && typeof input === "object" && !Array.isArray(input)
30
- ? { ...input }
31
- : {};
32
-
33
- const rawMemory =
34
- data.memory && typeof data.memory === "object" && !Array.isArray(data.memory)
35
- ? { ...data.memory }
36
- : {};
37
-
38
- delete rawMemory.maxIterations;
39
- delete rawMemory.timeoutMs;
40
-
41
- const groupHistoryLimit = Number(rawMemory.groupHistoryLimit);
42
- const userHistoryLimit = Number(rawMemory.userHistoryLimit);
43
-
44
- rawMemory.groupHistoryLimit =
45
- Number.isFinite(groupHistoryLimit) && groupHistoryLimit > 0
46
- ? Math.floor(groupHistoryLimit)
47
- : DEFAULT_MEMORY_GROUP_HISTORY_LIMIT;
48
- rawMemory.userHistoryLimit =
49
- Number.isFinite(userHistoryLimit) && userHistoryLimit > 0
50
- ? Math.floor(userHistoryLimit)
51
- : DEFAULT_MEMORY_USER_HISTORY_LIMIT;
52
- rawMemory.enabled =
53
- typeof rawMemory.enabled === "boolean" ? rawMemory.enabled : true;
54
-
55
- data.memory = rawMemory;
56
-
57
- const rawTopic =
58
- data.topic && typeof data.topic === "object" && !Array.isArray(data.topic)
59
- ? { ...data.topic }
60
- : {};
61
-
62
- const windowHours = Number(rawTopic.windowHours);
63
- const legacyTimeThresholdMs = Number(rawTopic.timeThresholdMs);
64
- const historyWindowCount = Number(rawTopic.historyWindowCount);
65
-
66
- rawTopic.enabled =
67
- typeof rawTopic.enabled === "boolean" ? rawTopic.enabled : true;
68
- rawTopic.windowHours =
69
- Number.isFinite(windowHours) && windowHours > 0
70
- ? Math.floor(windowHours)
71
- : Number.isFinite(legacyTimeThresholdMs) && legacyTimeThresholdMs > 0
72
- ? Math.max(1, Math.floor(legacyTimeThresholdMs / 3600_000))
73
- : DEFAULT_TOPIC_WINDOW_HOURS;
74
- rawTopic.historyWindowCount =
75
- Number.isFinite(historyWindowCount) && historyWindowCount > 0
76
- ? Math.floor(historyWindowCount)
77
- : DEFAULT_TOPIC_HISTORY_WINDOW_COUNT;
78
-
79
- delete rawTopic.messageThreshold;
80
- delete rawTopic.timeThresholdMs;
81
- delete rawTopic.maxTopicsPerSession;
82
-
83
- data.topic = rawTopic;
84
-
85
- const rawExpression =
21
+ function stripDeadPersonalizationKeys(input: any): any {
22
+ if (!input || typeof input !== "object" || Array.isArray(input)) return {};
23
+ const data: Record<string, any> = { ...input };
24
+
25
+ delete data.memory;
26
+ delete data.planner;
27
+
28
+ if (
29
+ data.topic &&
30
+ typeof data.topic === "object" &&
31
+ !Array.isArray(data.topic)
32
+ ) {
33
+ const topic = { ...data.topic };
34
+ delete topic.timeThresholdMs;
35
+ delete topic.messageThreshold;
36
+ delete topic.maxTopicsPerSession;
37
+ data.topic = topic;
38
+ }
39
+
40
+ if (
86
41
  data.expression &&
87
42
  typeof data.expression === "object" &&
88
43
  !Array.isArray(data.expression)
89
- ? { ...data.expression }
90
- : {};
91
-
92
- const learnAfterMessages = Number(rawExpression.learnAfterMessages);
93
- const legacyMaxExpressions = Number(rawExpression.maxExpressions);
94
- const sampleSize = Number(rawExpression.sampleSize);
95
-
96
- rawExpression.enabled =
97
- typeof rawExpression.enabled === "boolean" ? rawExpression.enabled : true;
98
- rawExpression.learnAfterMessages =
99
- Number.isFinite(learnAfterMessages) && learnAfterMessages > 0
100
- ? Math.floor(learnAfterMessages)
101
- : Number.isFinite(legacyMaxExpressions) && legacyMaxExpressions > 0
102
- ? Math.floor(legacyMaxExpressions)
103
- : DEFAULT_EXPRESSION_LEARN_AFTER_MESSAGES;
104
- rawExpression.sampleSize =
105
- Number.isFinite(sampleSize) && sampleSize > 0
106
- ? Math.floor(sampleSize)
107
- : DEFAULT_EXPRESSION_SAMPLE_SIZE;
108
- delete rawExpression.maxExpressions;
109
-
110
- data.expression = rawExpression;
111
-
112
- const rawEmoji =
113
- data.emoji && typeof data.emoji === "object" && !Array.isArray(data.emoji)
114
- ? { ...data.emoji }
115
- : {};
116
- rawEmoji.enabled =
117
- typeof rawEmoji.enabled === "boolean" ? rawEmoji.enabled : true;
118
- rawEmoji.characters = Array.isArray(rawEmoji.characters)
119
- ? rawEmoji.characters
120
- .map(String)
121
- .map((value: string) => value.trim())
122
- .filter(Boolean)
123
- : [];
124
- rawEmoji.stickers = Array.isArray(rawEmoji.stickers)
125
- ? rawEmoji.stickers
126
- .map(String)
127
- .map((value: string) => value.trim())
128
- .filter(Boolean)
129
- : [];
130
- delete rawEmoji.useAISelection;
131
- data.emoji = rawEmoji;
44
+ ) {
45
+ const expression = { ...data.expression };
46
+ delete expression.maxExpressions;
47
+ data.expression = expression;
48
+ }
49
+
50
+ if (
51
+ data.emoji &&
52
+ typeof data.emoji === "object" &&
53
+ !Array.isArray(data.emoji)
54
+ ) {
55
+ const emoji = { ...data.emoji };
56
+ delete emoji.useAISelection;
57
+ for (const key of ["characters", "stickers"] as const) {
58
+ if (!Array.isArray(emoji[key])) continue;
59
+ emoji[key] = emoji[key]
60
+ .map((item: unknown) => String(item ?? "").trim())
61
+ .filter(Boolean);
62
+ }
63
+ data.emoji = emoji;
64
+ }
132
65
 
133
66
  return data;
134
67
  }
@@ -181,13 +114,13 @@ export function createAIRoutes() {
181
114
  });
182
115
 
183
116
  app.get("/personalization", (c) => {
184
- const data = normalizePersonalizationConfig(
117
+ const data = stripDeadPersonalizationKeys(
185
118
  getChatConfig("personalization.json"),
186
119
  );
187
120
  return c.json({ ok: true, data });
188
121
  });
189
122
  app.put("/personalization", async (c) => {
190
- const body = normalizePersonalizationConfig(await c.req.json());
123
+ const body = stripDeadPersonalizationKeys(await c.req.json());
191
124
  logger.info(`[webui-action] ai.personalization.update`);
192
125
  return c.json({
193
126
  ok: true,
@@ -195,6 +128,19 @@ export function createAIRoutes() {
195
128
  });
196
129
  });
197
130
 
131
+ // 记忆配置由 chat 插件自己归一化(缺字段/坏值在读取时兜底),这里只做读写
132
+ app.get("/memory", (c) =>
133
+ c.json({ ok: true, data: getChatConfig("memory.json") }),
134
+ );
135
+ app.put("/memory", async (c) => {
136
+ const body = await c.req.json();
137
+ logger.info(`[webui-action] ai.memory.update`, {
138
+ enabled: body?.enabled,
139
+ retentionMonths: body?.retentionMonths,
140
+ });
141
+ return c.json({ ok: true, data: updateChatConfig("memory.json", body) });
142
+ });
143
+
198
144
  app.get("/settings", (c) =>
199
145
  c.json({ ok: true, data: getChatConfig("settings.json") }),
200
146
  );
@@ -405,7 +351,9 @@ export function createAIRoutes() {
405
351
  .filter((id: string) => id.includes("/") && id.length > 1),
406
352
  ),
407
353
  );
408
- logger.info(`[webui-action] ai.fallback.update`, { count: fallback.length });
354
+ logger.info(`[webui-action] ai.fallback.update`, {
355
+ count: fallback.length,
356
+ });
409
357
  api.setMainFallbackChain(fallback);
410
358
  const live = api.getMainFallbackChain?.() ?? fallback;
411
359
  return c.json({ ok: true, data: { fallback: live, liveFallback: live } });
@@ -496,9 +444,7 @@ export function createAIRoutes() {
496
444
  });
497
445
  });
498
446
 
499
- app.get("/stickers", (c) =>
500
- c.json({ ok: true, data: listStickerCatalog() }),
501
- );
447
+ app.get("/stickers", (c) => c.json({ ok: true, data: listStickerCatalog() }));
502
448
 
503
449
  app.get("/usage", (c) => {
504
450
  const api = getAIApi();
@@ -512,8 +458,7 @@ export function createAIRoutes() {
512
458
  ? (rawRange as AIUsageRange)
513
459
  : "today";
514
460
  const rawBotId = c.req.query("botId");
515
- const botId =
516
- rawBotId && rawBotId !== "all" ? Number(rawBotId) : undefined;
461
+ const botId = rawBotId && rawBotId !== "all" ? Number(rawBotId) : undefined;
517
462
 
518
463
  if (botId !== undefined && !Number.isFinite(botId)) {
519
464
  return c.json({ ok: false, error: "INVALID_BOT_ID" }, 400);
package/routes/index.ts CHANGED
@@ -9,3 +9,4 @@ export { createStoreRoutes } from "./store";
9
9
  export { createDataManagementRoutes } from "./data-management";
10
10
  export { createStrangerRoutes } from "./stranger";
11
11
  export { createAccessControlRoutes } from "./access-control";
12
+ export { createMcpRoutes } from "./mcp";
package/routes/mcp.ts ADDED
@@ -0,0 +1,136 @@
1
+ import { Hono } from "hono";
2
+ import { logger, servicesRegistry } from "mioku";
3
+ import type { McpServerInput, McpServiceApi } from "mioku-service-mcp";
4
+
5
+ /** 服务可能没装或没起来,统一走注册表按名查,缺失时返回 503 */
6
+ function mcp(): McpServiceApi | undefined {
7
+ return servicesRegistry.mcp as McpServiceApi | undefined;
8
+ }
9
+
10
+ function unavailable(c: { json: (body: unknown, status: 503) => Response }): Response {
11
+ return c.json({ ok: false, error: "MCP_SERVICE_UNAVAILABLE" }, 503);
12
+ }
13
+
14
+ function badRequest(
15
+ c: { json: (body: unknown, status: 400) => Response },
16
+ error: string,
17
+ ): Response {
18
+ return c.json({ ok: false, error }, 400);
19
+ }
20
+
21
+ function normalizeLimit(value: string | undefined, fallback: number): number {
22
+ const num = Math.floor(Number(value));
23
+ if (!Number.isFinite(num) || num <= 0) return fallback;
24
+ return Math.min(num, 500);
25
+ }
26
+
27
+ export function createMcpRoutes() {
28
+ const app = new Hono();
29
+
30
+ app.get("/overview", (c) => {
31
+ const api = mcp();
32
+ if (!api) return unavailable(c);
33
+ const config = api.getConfig();
34
+ return c.json({
35
+ ok: true,
36
+ data: {
37
+ autoConnect: config.autoConnect,
38
+ reconnectDelayMs: config.reconnectDelayMs,
39
+ maxConcurrency: config.maxConcurrency,
40
+ maxTools: config.maxTools,
41
+ maxResultChars: config.maxResultChars,
42
+ maxResultImages: config.maxResultImages,
43
+ revision: api.revision(),
44
+ toolCount: api.listTools().length,
45
+ catalogOnlyCount: api.listAllTools().filter((tool) => tool.catalogOnly).length,
46
+ servers: api.listServers(),
47
+ configs: api.listServerConfigs(),
48
+ },
49
+ });
50
+ });
51
+
52
+ app.post("/servers", async (c) => {
53
+ const api = mcp();
54
+ if (!api) return unavailable(c);
55
+ const body = (await c.req.json().catch(() => ({}))) as McpServerInput;
56
+ const name = String(body?.name ?? "").trim().toLowerCase();
57
+ if (!name) return badRequest(c, "请填写服务器名称");
58
+ logger.info("[webui-action] mcp.server.upsert", { name });
59
+ const result = await api.upsertServer({ ...body, name });
60
+ if ("error" in result) return badRequest(c, result.error);
61
+ return c.json({ ok: true, data: result });
62
+ });
63
+
64
+ app.delete("/servers/:name", async (c) => {
65
+ const api = mcp();
66
+ if (!api) return unavailable(c);
67
+ const name = c.req.param("name");
68
+ const removed = await api.removeServer(name);
69
+ if (!removed) return badRequest(c, `服务器 "${name}" 不存在`);
70
+ logger.info("[webui-action] mcp.server.remove", { name });
71
+ return c.json({ ok: true, data: { name } });
72
+ });
73
+
74
+ app.put("/servers/:name/enabled", async (c) => {
75
+ const api = mcp();
76
+ if (!api) return unavailable(c);
77
+ const name = c.req.param("name");
78
+ const body = (await c.req.json().catch(() => ({}))) as { enabled?: unknown };
79
+ const enabled = body?.enabled !== false;
80
+ const result = await api.setServerEnabled(name, enabled);
81
+ if ("error" in result) return badRequest(c, result.error);
82
+ logger.info(`[webui-action] mcp.server.${enabled ? "enable" : "disable"}`, { name });
83
+ return c.json({ ok: true, data: result });
84
+ });
85
+
86
+ for (const action of ["connect", "disconnect", "reconnect"] as const) {
87
+ app.post(`/servers/:name/${action}`, async (c) => {
88
+ const api = mcp();
89
+ if (!api) return unavailable(c);
90
+ const name = c.req.param("name");
91
+ logger.info(`[webui-action] mcp.server.${action}`, { name });
92
+ const result = await api[action](name);
93
+ if ("error" in result) return badRequest(c, result.error);
94
+ return c.json({ ok: true, data: result });
95
+ });
96
+ }
97
+
98
+ app.get("/servers/:name/tools", (c) => {
99
+ const api = mcp();
100
+ if (!api) return unavailable(c);
101
+ const name = c.req.param("name");
102
+ if (!api.getServer(name)) return badRequest(c, `服务器 "${name}" 不存在`);
103
+ const tools = api
104
+ .listAllTools()
105
+ .filter((tool) => tool.server === name)
106
+ .map((tool) => ({
107
+ publicName: tool.publicName,
108
+ tool: tool.tool,
109
+ description: tool.description,
110
+ catalogOnly: tool.catalogOnly,
111
+ parameters: tool.parameters,
112
+ }));
113
+ return c.json({ ok: true, data: tools });
114
+ });
115
+
116
+ app.post("/probe", async (c) => {
117
+ const api = mcp();
118
+ if (!api) return unavailable(c);
119
+ const body = (await c.req.json().catch(() => ({}))) as McpServerInput;
120
+ const name = String(body?.name ?? "").trim().toLowerCase();
121
+ if (!name) return badRequest(c, "请填写服务器名称");
122
+ logger.info("[webui-action] mcp.probe", { name });
123
+ const result = await api.probe({ ...body, name });
124
+ return c.json({ ok: true, data: result });
125
+ });
126
+
127
+ app.get("/logs", (c) => {
128
+ const api = mcp();
129
+ if (!api) return unavailable(c);
130
+ const server = c.req.query("server") || undefined;
131
+ const limit = normalizeLimit(c.req.query("limit"), 100);
132
+ return c.json({ ok: true, data: api.recentLogs(server, limit) });
133
+ });
134
+
135
+ return app;
136
+ }
package/system.ts CHANGED
@@ -4,7 +4,13 @@ import * as path from "node:path";
4
4
  import process from "node:process";
5
5
  import AdmZip from "adm-zip";
6
6
  import systemInfo from "systeminformation";
7
- import { buildAdapterReport, connectedBots, logger } from "mioku";
7
+ import {
8
+ buildAdapterReport,
9
+ connectedBots,
10
+ getConfigDefaults,
11
+ logger,
12
+ withConfigDefaults,
13
+ } from "mioku";
8
14
  import type { AdapterInstanceStatus, Bot } from "mioku";
9
15
  import type {
10
16
  InstallRequest,
@@ -2389,9 +2395,21 @@ export async function getSaying(): Promise<{ text: string }> {
2389
2395
  return fetchYiyan();
2390
2396
  }
2391
2397
 
2398
+ /**
2399
+ * 读插件配置:磁盘值覆盖默认值。
2400
+ *
2401
+ * 默认值只由插件在 `registerConfig` 时登记一次(见 mioku 的 config-defaults),
2402
+ * WebUI 这边不再手抄字段——以前每加一个配置项,要在这里和插件各改一遍。
2403
+ */
2404
+ function readPluginConfig(dir: string, owner: string, fileName: string): any {
2405
+ const stored = readJsonFile(path.join(dir, fileName), {});
2406
+ const name = fileName.replace(/\.json$/, "");
2407
+ const defaults = getConfigDefaults("plugin", owner, name);
2408
+ return withConfigDefaults(stored, defaults);
2409
+ }
2410
+
2392
2411
  export function getChatConfig(fileName: string): any {
2393
- const filePath = path.join(CHAT_CONFIG_DIR, fileName);
2394
- return readJsonFile(filePath, {});
2412
+ return readPluginConfig(CHAT_CONFIG_DIR, "chat", fileName);
2395
2413
  }
2396
2414
 
2397
2415
  export function updateChatConfig(fileName: string, data: any): any {
@@ -2401,8 +2419,7 @@ export function updateChatConfig(fileName: string, data: any): any {
2401
2419
  }
2402
2420
 
2403
2421
  export function getAgentConfig(fileName: string): any {
2404
- const filePath = path.join(AGENT_CONFIG_DIR, fileName);
2405
- return readJsonFile(filePath, {});
2422
+ return readPluginConfig(AGENT_CONFIG_DIR, "agent", fileName);
2406
2423
  }
2407
2424
 
2408
2425
  export function updateAgentConfig(fileName: string, data: any): any {