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 +7 -1
- package/index.ts +2 -0
- package/package.json +10 -3
- package/routes/agent.ts +39 -55
- package/routes/ai.ts +61 -116
- package/routes/index.ts +1 -0
- package/routes/mcp.ts +136 -0
- package/system.ts +22 -5
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
|
-
|
|
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.
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
typeof
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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 =
|
|
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 =
|
|
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`, {
|
|
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
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|