agentdog 1.1.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/LICENSE +22 -0
- package/README.md +244 -0
- package/dist/adapters/adapter-factory.d.ts +21 -0
- package/dist/adapters/adapter-factory.d.ts.map +1 -0
- package/dist/adapters/adapter-factory.js +157 -0
- package/dist/adapters/adapter-factory.js.map +1 -0
- package/dist/adapters/http-sse-adapter.d.ts +49 -0
- package/dist/adapters/http-sse-adapter.d.ts.map +1 -0
- package/dist/adapters/http-sse-adapter.js +485 -0
- package/dist/adapters/http-sse-adapter.js.map +1 -0
- package/dist/adapters/stdio-adapter.d.ts +69 -0
- package/dist/adapters/stdio-adapter.d.ts.map +1 -0
- package/dist/adapters/stdio-adapter.js +664 -0
- package/dist/adapters/stdio-adapter.js.map +1 -0
- package/dist/adapters/streamable-http-adapter.d.ts +38 -0
- package/dist/adapters/streamable-http-adapter.d.ts.map +1 -0
- package/dist/adapters/streamable-http-adapter.js +321 -0
- package/dist/adapters/streamable-http-adapter.js.map +1 -0
- package/dist/ai-gateway/anthropic-errors.d.ts +4 -0
- package/dist/ai-gateway/anthropic-errors.d.ts.map +1 -0
- package/dist/ai-gateway/anthropic-errors.js +17 -0
- package/dist/ai-gateway/anthropic-errors.js.map +1 -0
- package/dist/ai-gateway/auth.d.ts +3 -0
- package/dist/ai-gateway/auth.d.ts.map +1 -0
- package/dist/ai-gateway/auth.js +14 -0
- package/dist/ai-gateway/auth.js.map +1 -0
- package/dist/ai-gateway/auth.test.d.ts +2 -0
- package/dist/ai-gateway/auth.test.d.ts.map +1 -0
- package/dist/ai-gateway/auth.test.js +57 -0
- package/dist/ai-gateway/auth.test.js.map +1 -0
- package/dist/ai-gateway/count-tokens.d.ts +5 -0
- package/dist/ai-gateway/count-tokens.d.ts.map +1 -0
- package/dist/ai-gateway/count-tokens.js +37 -0
- package/dist/ai-gateway/count-tokens.js.map +1 -0
- package/dist/ai-gateway/count-tokens.test.d.ts +2 -0
- package/dist/ai-gateway/count-tokens.test.d.ts.map +1 -0
- package/dist/ai-gateway/count-tokens.test.js +19 -0
- package/dist/ai-gateway/count-tokens.test.js.map +1 -0
- package/dist/ai-gateway/dialects.d.ts +4 -0
- package/dist/ai-gateway/dialects.d.ts.map +1 -0
- package/dist/ai-gateway/dialects.js +15 -0
- package/dist/ai-gateway/dialects.js.map +1 -0
- package/dist/ai-gateway/gateway-server.d.ts +15 -0
- package/dist/ai-gateway/gateway-server.d.ts.map +1 -0
- package/dist/ai-gateway/gateway-server.js +121 -0
- package/dist/ai-gateway/gateway-server.js.map +1 -0
- package/dist/ai-gateway/integration.test.d.ts +2 -0
- package/dist/ai-gateway/integration.test.d.ts.map +1 -0
- package/dist/ai-gateway/integration.test.js +205 -0
- package/dist/ai-gateway/integration.test.js.map +1 -0
- package/dist/ai-gateway/ir/anthropic-in.d.ts +3 -0
- package/dist/ai-gateway/ir/anthropic-in.d.ts.map +1 -0
- package/dist/ai-gateway/ir/anthropic-in.js +121 -0
- package/dist/ai-gateway/ir/anthropic-in.js.map +1 -0
- package/dist/ai-gateway/ir/anthropic-in.test.d.ts +2 -0
- package/dist/ai-gateway/ir/anthropic-in.test.d.ts.map +1 -0
- package/dist/ai-gateway/ir/anthropic-in.test.js +93 -0
- package/dist/ai-gateway/ir/anthropic-in.test.js.map +1 -0
- package/dist/ai-gateway/ir/anthropic-sse.d.ts +22 -0
- package/dist/ai-gateway/ir/anthropic-sse.d.ts.map +1 -0
- package/dist/ai-gateway/ir/anthropic-sse.js +200 -0
- package/dist/ai-gateway/ir/anthropic-sse.js.map +1 -0
- package/dist/ai-gateway/ir/anthropic-sse.test.d.ts +2 -0
- package/dist/ai-gateway/ir/anthropic-sse.test.d.ts.map +1 -0
- package/dist/ai-gateway/ir/anthropic-sse.test.js +101 -0
- package/dist/ai-gateway/ir/anthropic-sse.test.js.map +1 -0
- package/dist/ai-gateway/ir/gemini-out.d.ts +20 -0
- package/dist/ai-gateway/ir/gemini-out.d.ts.map +1 -0
- package/dist/ai-gateway/ir/gemini-out.js +218 -0
- package/dist/ai-gateway/ir/gemini-out.js.map +1 -0
- package/dist/ai-gateway/ir/gemini-out.test.d.ts +2 -0
- package/dist/ai-gateway/ir/gemini-out.test.d.ts.map +1 -0
- package/dist/ai-gateway/ir/gemini-out.test.js +125 -0
- package/dist/ai-gateway/ir/gemini-out.test.js.map +1 -0
- package/dist/ai-gateway/ir/openai-out.d.ts +17 -0
- package/dist/ai-gateway/ir/openai-out.d.ts.map +1 -0
- package/dist/ai-gateway/ir/openai-out.js +226 -0
- package/dist/ai-gateway/ir/openai-out.js.map +1 -0
- package/dist/ai-gateway/ir/openai-out.test.d.ts +2 -0
- package/dist/ai-gateway/ir/openai-out.test.d.ts.map +1 -0
- package/dist/ai-gateway/ir/openai-out.test.js +144 -0
- package/dist/ai-gateway/ir/openai-out.test.js.map +1 -0
- package/dist/ai-gateway/ir/types.d.ts +77 -0
- package/dist/ai-gateway/ir/types.d.ts.map +1 -0
- package/dist/ai-gateway/ir/types.js +4 -0
- package/dist/ai-gateway/ir/types.js.map +1 -0
- package/dist/ai-gateway/ir-channel.d.ts +15 -0
- package/dist/ai-gateway/ir-channel.d.ts.map +1 -0
- package/dist/ai-gateway/ir-channel.js +120 -0
- package/dist/ai-gateway/ir-channel.js.map +1 -0
- package/dist/ai-gateway/model-router.d.ts +11 -0
- package/dist/ai-gateway/model-router.d.ts.map +1 -0
- package/dist/ai-gateway/model-router.js +27 -0
- package/dist/ai-gateway/model-router.js.map +1 -0
- package/dist/ai-gateway/model-router.test.d.ts +2 -0
- package/dist/ai-gateway/model-router.test.d.ts.map +1 -0
- package/dist/ai-gateway/model-router.test.js +43 -0
- package/dist/ai-gateway/model-router.test.js.map +1 -0
- package/dist/ai-gateway/passthrough.d.ts +9 -0
- package/dist/ai-gateway/passthrough.d.ts.map +1 -0
- package/dist/ai-gateway/passthrough.js +81 -0
- package/dist/ai-gateway/passthrough.js.map +1 -0
- package/dist/ai-gateway/passthrough.test.d.ts +2 -0
- package/dist/ai-gateway/passthrough.test.d.ts.map +1 -0
- package/dist/ai-gateway/passthrough.test.js +108 -0
- package/dist/ai-gateway/passthrough.test.js.map +1 -0
- package/dist/ai-gateway/upstream/model-lister.d.ts +13 -0
- package/dist/ai-gateway/upstream/model-lister.d.ts.map +1 -0
- package/dist/ai-gateway/upstream/model-lister.js +61 -0
- package/dist/ai-gateway/upstream/model-lister.js.map +1 -0
- package/dist/ai-gateway/upstream/provider-client.d.ts +20 -0
- package/dist/ai-gateway/upstream/provider-client.d.ts.map +1 -0
- package/dist/ai-gateway/upstream/provider-client.js +76 -0
- package/dist/ai-gateway/upstream/provider-client.js.map +1 -0
- package/dist/ai-gateway/upstream/provider-client.test.d.ts +2 -0
- package/dist/ai-gateway/upstream/provider-client.test.d.ts.map +1 -0
- package/dist/ai-gateway/upstream/provider-client.test.js +69 -0
- package/dist/ai-gateway/upstream/provider-client.test.js.map +1 -0
- package/dist/cli/cli-main.d.ts +7 -0
- package/dist/cli/cli-main.d.ts.map +1 -0
- package/dist/cli/cli-main.js +116 -0
- package/dist/cli/cli-main.js.map +1 -0
- package/dist/cli/cli-router.d.ts +20 -0
- package/dist/cli/cli-router.d.ts.map +1 -0
- package/dist/cli/cli-router.js +271 -0
- package/dist/cli/cli-router.js.map +1 -0
- package/dist/cli/cli-utils.d.ts +30 -0
- package/dist/cli/cli-utils.d.ts.map +1 -0
- package/dist/cli/cli-utils.js +219 -0
- package/dist/cli/cli-utils.js.map +1 -0
- package/dist/cli/commands/audit-commands.d.ts +30 -0
- package/dist/cli/commands/audit-commands.d.ts.map +1 -0
- package/dist/cli/commands/audit-commands.js +417 -0
- package/dist/cli/commands/audit-commands.js.map +1 -0
- package/dist/cli/commands/autostart-command.d.ts +21 -0
- package/dist/cli/commands/autostart-command.d.ts.map +1 -0
- package/dist/cli/commands/autostart-command.js +243 -0
- package/dist/cli/commands/autostart-command.js.map +1 -0
- package/dist/cli/commands/autostart-command.test.d.ts +2 -0
- package/dist/cli/commands/autostart-command.test.d.ts.map +1 -0
- package/dist/cli/commands/autostart-command.test.js +35 -0
- package/dist/cli/commands/autostart-command.test.js.map +1 -0
- package/dist/cli/commands/config-commands.d.ts +30 -0
- package/dist/cli/commands/config-commands.d.ts.map +1 -0
- package/dist/cli/commands/config-commands.js +1013 -0
- package/dist/cli/commands/config-commands.js.map +1 -0
- package/dist/cli/commands/daemon-commands.d.ts +76 -0
- package/dist/cli/commands/daemon-commands.d.ts.map +1 -0
- package/dist/cli/commands/daemon-commands.js +451 -0
- package/dist/cli/commands/daemon-commands.js.map +1 -0
- package/dist/cli/commands/detect-commands.d.ts +17 -0
- package/dist/cli/commands/detect-commands.d.ts.map +1 -0
- package/dist/cli/commands/detect-commands.js +293 -0
- package/dist/cli/commands/detect-commands.js.map +1 -0
- package/dist/cli/commands/diagnose-commands.d.ts +19 -0
- package/dist/cli/commands/diagnose-commands.d.ts.map +1 -0
- package/dist/cli/commands/diagnose-commands.js +326 -0
- package/dist/cli/commands/diagnose-commands.js.map +1 -0
- package/dist/cli/commands/optimize-commands.d.ts +14 -0
- package/dist/cli/commands/optimize-commands.d.ts.map +1 -0
- package/dist/cli/commands/optimize-commands.js +189 -0
- package/dist/cli/commands/optimize-commands.js.map +1 -0
- package/dist/cli/commands/proxy-command.d.ts +27 -0
- package/dist/cli/commands/proxy-command.d.ts.map +1 -0
- package/dist/cli/commands/proxy-command.js +212 -0
- package/dist/cli/commands/proxy-command.js.map +1 -0
- package/dist/cli/commands/start-command.d.ts +22 -0
- package/dist/cli/commands/start-command.d.ts.map +1 -0
- package/dist/cli/commands/start-command.js +275 -0
- package/dist/cli/commands/start-command.js.map +1 -0
- package/dist/config/config-manager.d.ts +125 -0
- package/dist/config/config-manager.d.ts.map +1 -0
- package/dist/config/config-manager.js +569 -0
- package/dist/config/config-manager.js.map +1 -0
- package/dist/config/config-manager.test.d.ts +2 -0
- package/dist/config/config-manager.test.d.ts.map +1 -0
- package/dist/config/config-manager.test.js +209 -0
- package/dist/config/config-manager.test.js.map +1 -0
- package/dist/core/auto-config-generator.d.ts +55 -0
- package/dist/core/auto-config-generator.d.ts.map +1 -0
- package/dist/core/auto-config-generator.js +278 -0
- package/dist/core/auto-config-generator.js.map +1 -0
- package/dist/core/mcpdog-server.d.ts +49 -0
- package/dist/core/mcpdog-server.d.ts.map +1 -0
- package/dist/core/mcpdog-server.js +552 -0
- package/dist/core/mcpdog-server.js.map +1 -0
- package/dist/core/protocol-detector.d.ts +56 -0
- package/dist/core/protocol-detector.d.ts.map +1 -0
- package/dist/core/protocol-detector.js +365 -0
- package/dist/core/protocol-detector.js.map +1 -0
- package/dist/daemon/daemon-client.d.ts +34 -0
- package/dist/daemon/daemon-client.d.ts.map +1 -0
- package/dist/daemon/daemon-client.js +130 -0
- package/dist/daemon/daemon-client.js.map +1 -0
- package/dist/daemon/daemon-client.test.d.ts +2 -0
- package/dist/daemon/daemon-client.test.d.ts.map +1 -0
- package/dist/daemon/daemon-client.test.js +77 -0
- package/dist/daemon/daemon-client.test.js.map +1 -0
- package/dist/daemon/daemon-info.d.ts +7 -0
- package/dist/daemon/daemon-info.d.ts.map +1 -0
- package/dist/daemon/daemon-info.js +34 -0
- package/dist/daemon/daemon-info.js.map +1 -0
- package/dist/daemon/daemon-info.test.d.ts +2 -0
- package/dist/daemon/daemon-info.test.d.ts.map +1 -0
- package/dist/daemon/daemon-info.test.js +57 -0
- package/dist/daemon/daemon-info.test.js.map +1 -0
- package/dist/daemon/daemon-web-server.ai.test.d.ts +2 -0
- package/dist/daemon/daemon-web-server.ai.test.d.ts.map +1 -0
- package/dist/daemon/daemon-web-server.ai.test.js +119 -0
- package/dist/daemon/daemon-web-server.ai.test.js.map +1 -0
- package/dist/daemon/daemon-web-server.d.ts +59 -0
- package/dist/daemon/daemon-web-server.d.ts.map +1 -0
- package/dist/daemon/daemon-web-server.js +1236 -0
- package/dist/daemon/daemon-web-server.js.map +1 -0
- package/dist/daemon/daemon-web-server.test.d.ts +2 -0
- package/dist/daemon/daemon-web-server.test.d.ts.map +1 -0
- package/dist/daemon/daemon-web-server.test.js +93 -0
- package/dist/daemon/daemon-web-server.test.js.map +1 -0
- package/dist/daemon/mcpdog-daemon.d.ts +42 -0
- package/dist/daemon/mcpdog-daemon.d.ts.map +1 -0
- package/dist/daemon/mcpdog-daemon.js +302 -0
- package/dist/daemon/mcpdog-daemon.js.map +1 -0
- package/dist/daemon/stdio-proxy.d.ts +17 -0
- package/dist/daemon/stdio-proxy.d.ts.map +1 -0
- package/dist/daemon/stdio-proxy.js +143 -0
- package/dist/daemon/stdio-proxy.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +198 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-analyzer.d.ts +27 -0
- package/dist/intent-analyzer.d.ts.map +1 -0
- package/dist/intent-analyzer.js +140 -0
- package/dist/intent-analyzer.js.map +1 -0
- package/dist/logging/server-log-manager.d.ts +75 -0
- package/dist/logging/server-log-manager.d.ts.map +1 -0
- package/dist/logging/server-log-manager.js +185 -0
- package/dist/logging/server-log-manager.js.map +1 -0
- package/dist/mcp-registry.d.ts +35 -0
- package/dist/mcp-registry.d.ts.map +1 -0
- package/dist/mcp-registry.js +166 -0
- package/dist/mcp-registry.js.map +1 -0
- package/dist/middleware/auth.d.ts +5 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +81 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/mock-tool-database.d.ts +41 -0
- package/dist/mock-tool-database.d.ts.map +1 -0
- package/dist/mock-tool-database.js +461 -0
- package/dist/mock-tool-database.js.map +1 -0
- package/dist/proxy-server-manager.d.ts +11 -0
- package/dist/proxy-server-manager.d.ts.map +1 -0
- package/dist/proxy-server-manager.js +144 -0
- package/dist/proxy-server-manager.js.map +1 -0
- package/dist/router/tool-router.d.ts +61 -0
- package/dist/router/tool-router.d.ts.map +1 -0
- package/dist/router/tool-router.js +539 -0
- package/dist/router/tool-router.js.map +1 -0
- package/dist/router/tool-router.test.d.ts +2 -0
- package/dist/router/tool-router.test.d.ts.map +1 -0
- package/dist/router/tool-router.test.js +113 -0
- package/dist/router/tool-router.test.js.map +1 -0
- package/dist/streamable-http-server.d.ts +27 -0
- package/dist/streamable-http-server.d.ts.map +1 -0
- package/dist/streamable-http-server.js +302 -0
- package/dist/streamable-http-server.js.map +1 -0
- package/dist/tool-finder.d.ts +21 -0
- package/dist/tool-finder.d.ts.map +1 -0
- package/dist/tool-finder.js +86 -0
- package/dist/tool-finder.js.map +1 -0
- package/dist/tool-recommender.d.ts +23 -0
- package/dist/tool-recommender.d.ts.map +1 -0
- package/dist/tool-recommender.js +200 -0
- package/dist/tool-recommender.js.map +1 -0
- package/dist/types/index.d.ts +145 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/server-name-validator.d.ts +47 -0
- package/dist/utils/server-name-validator.d.ts.map +1 -0
- package/dist/utils/server-name-validator.js +156 -0
- package/dist/utils/server-name-validator.js.map +1 -0
- package/dist/web/web-server.d.ts +44 -0
- package/dist/web/web-server.d.ts.map +1 -0
- package/dist/web/web-server.js +500 -0
- package/dist/web/web-server.js.map +1 -0
- package/package.json +68 -0
- package/web/dist/assets/index-DMVhVtNU.css +1 -0
- package/web/dist/assets/index-VMXIgcOH.js +92 -0
- package/web/dist/assets/index-VMXIgcOH.js.map +1 -0
- package/web/dist/index.html +14 -0
|
@@ -0,0 +1,1236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon Web Server
|
|
3
|
+
* Connects directly to the daemon instance instead of creating a separate MCPServer
|
|
4
|
+
*/
|
|
5
|
+
import express from 'express';
|
|
6
|
+
import { createServer } from 'http';
|
|
7
|
+
import { Server as SocketIOServer } from 'socket.io';
|
|
8
|
+
import cors from 'cors';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { globalLogManager } from '../logging/server-log-manager.js';
|
|
13
|
+
import { ServerNameValidator } from '../utils/server-name-validator.js';
|
|
14
|
+
import { createExpressAuthMiddleware } from '../middleware/auth.js';
|
|
15
|
+
import { randomUUID } from 'crypto';
|
|
16
|
+
import { isValidProviderSlug } from '../config/config-manager.js';
|
|
17
|
+
import { listUpstreamModels } from '../ai-gateway/upstream/model-lister.js';
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = path.dirname(__filename);
|
|
20
|
+
// 校验监听地址:默认仅回环;对外开放必须配置 token,否则拒绝启动(安全基线,见设计文档 §5)
|
|
21
|
+
export function resolveListenHost(host, hasAuthToken) {
|
|
22
|
+
const resolved = host ?? 'localhost';
|
|
23
|
+
const isLoopback = resolved === 'localhost' || resolved === '127.0.0.1' || resolved === '::1';
|
|
24
|
+
if (!isLoopback && !hasAuthToken) {
|
|
25
|
+
throw new Error(`Refusing to listen on "${resolved}" without MCPDOG_AUTH_TOKEN: ` +
|
|
26
|
+
`remote exposure requires a token. Set MCPDOG_AUTH_TOKEN or change web.host back to "localhost".`);
|
|
27
|
+
}
|
|
28
|
+
return resolved;
|
|
29
|
+
}
|
|
30
|
+
export class DaemonWebServer {
|
|
31
|
+
app;
|
|
32
|
+
server;
|
|
33
|
+
io;
|
|
34
|
+
daemon;
|
|
35
|
+
port;
|
|
36
|
+
host;
|
|
37
|
+
configManager; // Add configManager property
|
|
38
|
+
constructor(daemon, port, host) {
|
|
39
|
+
this.daemon = daemon;
|
|
40
|
+
this.port = port;
|
|
41
|
+
this.host = host;
|
|
42
|
+
this.configManager = daemon.getConfigManager(); // Use daemon's configManager
|
|
43
|
+
// Create Express application
|
|
44
|
+
this.app = express();
|
|
45
|
+
this.server = createServer(this.app);
|
|
46
|
+
this.io = new SocketIOServer(this.server, {
|
|
47
|
+
cors: {
|
|
48
|
+
origin: "*",
|
|
49
|
+
methods: ["GET", "POST"]
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
this.setupMiddleware();
|
|
53
|
+
this.setupRoutes();
|
|
54
|
+
this.setupWebSocket();
|
|
55
|
+
this.setupDaemonEvents();
|
|
56
|
+
}
|
|
57
|
+
setupMiddleware() {
|
|
58
|
+
// CORS support
|
|
59
|
+
this.app.use(cors());
|
|
60
|
+
// JSON parsing
|
|
61
|
+
this.app.use(express.json());
|
|
62
|
+
// Authentication middleware (if token is configured)
|
|
63
|
+
const authToken = process.env.MCPDOG_AUTH_TOKEN;
|
|
64
|
+
if (authToken) {
|
|
65
|
+
console.log('[DAEMON-WEB] Authentication enabled');
|
|
66
|
+
// Add authentication check endpoint (before auth middleware)
|
|
67
|
+
this.app.get('/api/auth/status', (req, res) => {
|
|
68
|
+
const authHeader = req.headers.authorization;
|
|
69
|
+
if (!authHeader) {
|
|
70
|
+
return res.json({ authenticated: false, required: true });
|
|
71
|
+
}
|
|
72
|
+
const parts = authHeader.split(' ');
|
|
73
|
+
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
|
74
|
+
return res.json({ authenticated: false, required: true });
|
|
75
|
+
}
|
|
76
|
+
const token = parts[1];
|
|
77
|
+
const isAuthorized = Buffer.compare(Buffer.from(token), Buffer.from(authToken)) === 0;
|
|
78
|
+
if (!isAuthorized) {
|
|
79
|
+
return res.json({ authenticated: false, required: true });
|
|
80
|
+
}
|
|
81
|
+
res.json({ authenticated: true, required: true });
|
|
82
|
+
});
|
|
83
|
+
// Add login endpoint (before auth middleware)
|
|
84
|
+
this.app.post('/api/auth/login', (req, res) => {
|
|
85
|
+
const { token } = req.body;
|
|
86
|
+
if (!token) {
|
|
87
|
+
return res.status(400).json({ error: '需要提供访问令牌' });
|
|
88
|
+
}
|
|
89
|
+
const isAuthorized = Buffer.compare(Buffer.from(token), Buffer.from(authToken)) === 0;
|
|
90
|
+
if (!isAuthorized) {
|
|
91
|
+
return res.status(401).json({ error: '访问令牌无效' });
|
|
92
|
+
}
|
|
93
|
+
res.json({ success: true, message: '登录成功' });
|
|
94
|
+
});
|
|
95
|
+
this.app.use(createExpressAuthMiddleware(authToken));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
// When auth is disabled, return auth not required
|
|
99
|
+
this.app.get('/api/auth/status', (req, res) => {
|
|
100
|
+
res.json({ authenticated: true, required: false });
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
// Static file serving
|
|
104
|
+
const staticPath = path.join(__dirname, '../../web/dist');
|
|
105
|
+
this.app.use(express.static(staticPath));
|
|
106
|
+
// API route prefix
|
|
107
|
+
this.app.use('/api', this.createAPIRouter());
|
|
108
|
+
}
|
|
109
|
+
createAPIRouter() {
|
|
110
|
+
const router = express.Router();
|
|
111
|
+
// System status API
|
|
112
|
+
router.get('/status', this.handleGetStatus.bind(this));
|
|
113
|
+
// MCP JSON-RPC forwarding (replaces the former IPC mcp-request channel)
|
|
114
|
+
router.post('/mcp', this.handleMCPRequest.bind(this));
|
|
115
|
+
// System info API (version & config path for header display)
|
|
116
|
+
router.get('/system/info', this.handleGetSystemInfo.bind(this));
|
|
117
|
+
// Server management API
|
|
118
|
+
router.get('/servers', this.handleGetServers.bind(this));
|
|
119
|
+
router.post('/servers', this.handleAddServer.bind(this));
|
|
120
|
+
router.put('/servers/:name', this.handleUpdateServer.bind(this));
|
|
121
|
+
router.delete('/servers/:name', this.handleRemoveServer.bind(this));
|
|
122
|
+
router.post('/servers/:name/toggle', this.handleToggleServer.bind(this));
|
|
123
|
+
// Tool-level control API
|
|
124
|
+
router.get('/servers/:name/tools', this.handleGetServerTools.bind(this));
|
|
125
|
+
router.post('/servers/:name/tools/:tool/toggle', this.handleToggleServerTool.bind(this));
|
|
126
|
+
router.put('/servers/:name/tools', this.handleUpdateServerTools.bind(this));
|
|
127
|
+
// Tool management API
|
|
128
|
+
router.get('/tools', this.handleGetTools.bind(this));
|
|
129
|
+
router.post('/tools/:name/call', this.handleCallTool.bind(this));
|
|
130
|
+
// Config management API
|
|
131
|
+
router.get('/config', this.handleGetConfig.bind(this));
|
|
132
|
+
router.put('/config', this.handleUpdateConfig.bind(this));
|
|
133
|
+
// Daemon-specific API
|
|
134
|
+
router.get('/daemon/clients', this.handleGetClients.bind(this));
|
|
135
|
+
router.post('/daemon/reload', this.handleReloadConfig.bind(this));
|
|
136
|
+
// AI gateway & providers API
|
|
137
|
+
router.get('/ai-gateway/status', this.handleGetAiGatewayStatus.bind(this));
|
|
138
|
+
router.put('/ai-gateway/settings', this.handleUpdateAiGatewaySettings.bind(this));
|
|
139
|
+
router.get('/ai-providers', this.handleGetAiProviders.bind(this));
|
|
140
|
+
router.post('/ai-providers', this.handleAddAiProvider.bind(this));
|
|
141
|
+
router.put('/ai-providers/:id', this.handleUpdateAiProvider.bind(this));
|
|
142
|
+
router.delete('/ai-providers/:id', this.handleDeleteAiProvider.bind(this));
|
|
143
|
+
router.post('/ai-providers/:id/test', this.handleTestAiProvider.bind(this));
|
|
144
|
+
router.post('/ai-providers/:id/models', this.handleFetchProviderModels.bind(this));
|
|
145
|
+
// Log management API
|
|
146
|
+
router.get('/logs', this.handleGetAllLogs.bind(this));
|
|
147
|
+
router.get('/logs/:serverName', this.handleGetServerLogs.bind(this));
|
|
148
|
+
router.delete('/logs/:serverName', this.handleClearServerLogs.bind(this));
|
|
149
|
+
router.get('/logs/:serverName/stats', this.handleGetServerLogStats.bind(this));
|
|
150
|
+
return router;
|
|
151
|
+
}
|
|
152
|
+
setupRoutes() {
|
|
153
|
+
// SPA route support - all non-API routes return index.html
|
|
154
|
+
this.app.get('*', (req, res) => {
|
|
155
|
+
if (!req.path.startsWith('/api')) {
|
|
156
|
+
const indexPath = path.join(__dirname, '../../web/dist/index.html');
|
|
157
|
+
res.sendFile(indexPath);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
setupWebSocket() {
|
|
162
|
+
this.io.on('connection', (socket) => {
|
|
163
|
+
console.log('[DAEMON-WEB] Web client connected:', socket.id);
|
|
164
|
+
// Send initial status
|
|
165
|
+
this.sendStatusUpdate(socket);
|
|
166
|
+
// Client requests status update
|
|
167
|
+
socket.on('request-status', () => {
|
|
168
|
+
this.sendStatusUpdate(socket);
|
|
169
|
+
});
|
|
170
|
+
socket.on('disconnect', () => {
|
|
171
|
+
console.log('[DAEMON-WEB] Web client disconnected:', socket.id);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
setupDaemonEvents() {
|
|
176
|
+
// Listen for daemon events, push to web clients in real-time
|
|
177
|
+
this.daemon.on('server-started', (data) => {
|
|
178
|
+
console.log(`[DAEMON-WEB] Server started event received: ${data.serverName}`);
|
|
179
|
+
// Delay sending composite event to ensure adapter status is fully updated
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
const systemStatus = this.getSystemStatus();
|
|
182
|
+
if (systemStatus) {
|
|
183
|
+
console.log(`[DAEMON-WEB] Sending server-status-changed event for: ${data.serverName}`);
|
|
184
|
+
this.io.emit('server-status-changed', {
|
|
185
|
+
event: 'server-started',
|
|
186
|
+
serverName: data.serverName,
|
|
187
|
+
systemStatus: systemStatus,
|
|
188
|
+
originalData: data,
|
|
189
|
+
timestamp: data.timestamp || new Date().toISOString()
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// If status retrieval fails, fall back to original method
|
|
194
|
+
this.io.emit('server-started', data);
|
|
195
|
+
this.broadcastStatusUpdate();
|
|
196
|
+
}
|
|
197
|
+
}, 1000); // Delay to ensure status synchronization
|
|
198
|
+
});
|
|
199
|
+
this.daemon.on('server-stopped', (data) => {
|
|
200
|
+
console.log(`[DAEMON-WEB] Server stopped event received: ${data.serverName}`);
|
|
201
|
+
// Immediately get and send status
|
|
202
|
+
const systemStatus = this.getSystemStatus();
|
|
203
|
+
if (systemStatus) {
|
|
204
|
+
console.log(`[DAEMON-WEB] Sending server-status-changed event for: ${data.serverName}`);
|
|
205
|
+
this.io.emit('server-status-changed', {
|
|
206
|
+
event: 'server-stopped',
|
|
207
|
+
serverName: data.serverName,
|
|
208
|
+
systemStatus: systemStatus,
|
|
209
|
+
originalData: data,
|
|
210
|
+
timestamp: data.timestamp || new Date().toISOString()
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// If status retrieval fails, fall back to original method
|
|
215
|
+
this.io.emit('server-stopped', data);
|
|
216
|
+
this.broadcastStatusUpdate();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
this.daemon.on('routes-updated', (data) => {
|
|
220
|
+
this.io.emit('server-updated', {
|
|
221
|
+
serverName: data.serverName,
|
|
222
|
+
toolCount: data.toolCount,
|
|
223
|
+
timestamp: new Date().toISOString()
|
|
224
|
+
});
|
|
225
|
+
// Delay broadcasting status update to ensure tool routes are fully updated
|
|
226
|
+
setTimeout(() => {
|
|
227
|
+
console.log(`[DAEMON-WEB] Broadcasting delayed status update after routes-updated for: ${data.serverName}`);
|
|
228
|
+
this.broadcastStatusUpdate();
|
|
229
|
+
}, 500); // 500ms delay
|
|
230
|
+
});
|
|
231
|
+
this.daemon.on('server-connected', (data) => {
|
|
232
|
+
console.log(`[DAEMON-WEB] Server connected event received: ${data.serverName}`);
|
|
233
|
+
// Delay sending composite event to ensure adapter status is fully updated
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
const systemStatus = this.getSystemStatus();
|
|
236
|
+
if (systemStatus) {
|
|
237
|
+
console.log(`[DAEMON-WEB] Sending server-status-changed event for: ${data.serverName}`);
|
|
238
|
+
this.io.emit('server-status-changed', {
|
|
239
|
+
event: 'server-connected',
|
|
240
|
+
serverName: data.serverName,
|
|
241
|
+
systemStatus: systemStatus,
|
|
242
|
+
originalData: data,
|
|
243
|
+
timestamp: data.timestamp || new Date().toISOString()
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
// If status retrieval fails, fall back to original method
|
|
248
|
+
this.io.emit('server-connected', data);
|
|
249
|
+
this.broadcastStatusUpdate();
|
|
250
|
+
}
|
|
251
|
+
}, 1000); // Delay to ensure status synchronization
|
|
252
|
+
});
|
|
253
|
+
this.daemon.on('server-disconnected', (data) => {
|
|
254
|
+
console.log(`[DAEMON-WEB] Server disconnected event received: ${data.serverName}`);
|
|
255
|
+
// Immediately get and send status, as disconnection status changes are immediate
|
|
256
|
+
const systemStatus = this.getSystemStatus();
|
|
257
|
+
if (systemStatus) {
|
|
258
|
+
console.log(`[DAEMON-WEB] Sending server-status-changed event for: ${data.serverName}`);
|
|
259
|
+
this.io.emit('server-status-changed', {
|
|
260
|
+
event: 'server-disconnected',
|
|
261
|
+
serverName: data.serverName,
|
|
262
|
+
systemStatus: systemStatus,
|
|
263
|
+
originalData: data,
|
|
264
|
+
timestamp: data.timestamp || new Date().toISOString()
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// If status retrieval fails, fall back to original method
|
|
269
|
+
this.io.emit('server-disconnected', data);
|
|
270
|
+
this.broadcastStatusUpdate();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
this.daemon.on('server-error', (data) => {
|
|
274
|
+
this.io.emit('server-error', data);
|
|
275
|
+
});
|
|
276
|
+
this.daemon.on('server-log', (data) => {
|
|
277
|
+
this.io.emit('server-log', data);
|
|
278
|
+
});
|
|
279
|
+
this.daemon.on('tool-called', (data) => {
|
|
280
|
+
this.io.emit('tool-called', {
|
|
281
|
+
serverName: data.serverName,
|
|
282
|
+
toolName: data.toolName,
|
|
283
|
+
duration: data.duration,
|
|
284
|
+
timestamp: new Date().toISOString()
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
this.daemon.on('error', (data) => {
|
|
288
|
+
this.io.emit('error', {
|
|
289
|
+
error: data.error,
|
|
290
|
+
context: data.context,
|
|
291
|
+
timestamp: new Date().toISOString()
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
// Listen for config change events - this is the critical part for fixing!
|
|
295
|
+
this.daemon.on('config-changed', (config) => {
|
|
296
|
+
console.log('[DAEMON-WEB] Config changed event received, broadcasting to WebSocket clients');
|
|
297
|
+
this.io.emit('config-changed', {
|
|
298
|
+
config,
|
|
299
|
+
timestamp: new Date().toISOString()
|
|
300
|
+
});
|
|
301
|
+
// Broadcast latest status after config change
|
|
302
|
+
this.broadcastStatusUpdate();
|
|
303
|
+
});
|
|
304
|
+
// Listen for log manager events, push logs to web clients in real-time
|
|
305
|
+
globalLogManager.on('log-added', (data) => {
|
|
306
|
+
this.io.emit('enhanced-log-added', {
|
|
307
|
+
serverName: data.serverName,
|
|
308
|
+
logEntry: data.logEntry,
|
|
309
|
+
timestamp: data.logEntry.timestamp
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
globalLogManager.on('server-error', (data) => {
|
|
313
|
+
this.io.emit('server-log-error', data);
|
|
314
|
+
});
|
|
315
|
+
globalLogManager.on('connection-status-changed', (data) => {
|
|
316
|
+
this.io.emit('server-connection-status', data);
|
|
317
|
+
});
|
|
318
|
+
this.daemon.on('server-toggled', (data) => {
|
|
319
|
+
console.log(`[DAEMON-WEB] Server toggled event received: ${data.name} enabled: ${data.enabled}`);
|
|
320
|
+
const systemStatus = this.getSystemStatus();
|
|
321
|
+
if (systemStatus) {
|
|
322
|
+
this.io.emit('server-status-changed', {
|
|
323
|
+
event: data.enabled ? 'server-enabled' : 'server-disabled',
|
|
324
|
+
serverName: data.name,
|
|
325
|
+
systemStatus: systemStatus,
|
|
326
|
+
originalData: data,
|
|
327
|
+
timestamp: new Date().toISOString()
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
this.broadcastStatusUpdate();
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
// API handlers
|
|
336
|
+
async handleGetStatus(req, res) {
|
|
337
|
+
try {
|
|
338
|
+
const status = this.daemon['getFullStatus'](); // Access private method of daemon
|
|
339
|
+
res.json(status);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
res.status(500).json({
|
|
343
|
+
error: '获取状态失败',
|
|
344
|
+
message: error.message
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// 任意 MCP JSON-RPC 转发(替代原 IPC mcp-request,见设计文档 §3)
|
|
349
|
+
async handleMCPRequest(req, res) {
|
|
350
|
+
try {
|
|
351
|
+
const request = req.body;
|
|
352
|
+
if (!request || typeof request !== 'object' ||
|
|
353
|
+
request.jsonrpc !== '2.0' || typeof request.method !== 'string') {
|
|
354
|
+
return res.status(400).json({ error: 'Invalid MCP request: expect a JSON-RPC 2.0 object with method' });
|
|
355
|
+
}
|
|
356
|
+
const clientId = req.headers['x-mcpdog-client'] || 'http-anonymous';
|
|
357
|
+
const daemon = this.daemon;
|
|
358
|
+
daemon.recordClientActivity(clientId, 'stdio');
|
|
359
|
+
const response = await daemon.mcpServer.handleRequest(request, clientId);
|
|
360
|
+
res.json(response);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
res.status(500).json({ error: 'MCP request failed', message: error.message });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// Header 展示用的系统信息:版本号与配置文件路径
|
|
367
|
+
async handleGetSystemInfo(req, res) {
|
|
368
|
+
try {
|
|
369
|
+
const packagePath = path.join(__dirname, '../../package.json');
|
|
370
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
371
|
+
res.json({
|
|
372
|
+
version: packageJson.version,
|
|
373
|
+
configPath: this.configManager.getConfigPath()
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
res.status(500).json({
|
|
378
|
+
error: '获取系统信息失败',
|
|
379
|
+
message: error.message
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async handleGetServers(req, res) {
|
|
384
|
+
try {
|
|
385
|
+
// Get all servers from config file to merge configuration information
|
|
386
|
+
const configManager = this.daemon['configManager'];
|
|
387
|
+
const config = configManager.getConfig();
|
|
388
|
+
const configServers = config.servers || {};
|
|
389
|
+
// Get runtime status
|
|
390
|
+
const status = this.daemon['getFullStatus']();
|
|
391
|
+
const runtimeServers = status.servers || [];
|
|
392
|
+
const toolRouter = this.daemon['mcpServer'].getToolRouter();
|
|
393
|
+
// Create runtime status map
|
|
394
|
+
const runtimeMap = new Map();
|
|
395
|
+
(status.servers || []).forEach((server) => {
|
|
396
|
+
runtimeMap.set(server.name, server);
|
|
397
|
+
});
|
|
398
|
+
// Merge configuration and runtime information
|
|
399
|
+
const serversWithTools = Object.entries(configServers).map(([serverName, serverConfig]) => {
|
|
400
|
+
const runtimeInfo = runtimeMap.get(serverName);
|
|
401
|
+
const isConnected = !!runtimeInfo?.connected;
|
|
402
|
+
const tools = isConnected ? toolRouter.getToolsByServer(serverName) : [];
|
|
403
|
+
const enabledTools = isConnected ? toolRouter.getEnabledToolsByServer(serverName) : [];
|
|
404
|
+
return {
|
|
405
|
+
// Include all ServerConfig properties and ensure name is set correctly
|
|
406
|
+
...serverConfig,
|
|
407
|
+
name: serverName, // Always use the key as the definitive name
|
|
408
|
+
// Add server runtime status
|
|
409
|
+
connected: isConnected,
|
|
410
|
+
toolCount: tools.length,
|
|
411
|
+
enabledToolCount: enabledTools.length,
|
|
412
|
+
tools: tools.map(tool => ({
|
|
413
|
+
name: tool.name,
|
|
414
|
+
description: tool.description,
|
|
415
|
+
enabled: this.isToolEnabled(serverConfig, tool.name),
|
|
416
|
+
inputSchema: tool.inputSchema,
|
|
417
|
+
settings: {}
|
|
418
|
+
}))
|
|
419
|
+
};
|
|
420
|
+
});
|
|
421
|
+
res.json(serversWithTools);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
res.status(500).json({
|
|
425
|
+
error: '获取服务器列表失败',
|
|
426
|
+
message: error.message
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async handleGetTools(req, res) {
|
|
431
|
+
try {
|
|
432
|
+
const mcpServer = this.daemon['mcpServer'];
|
|
433
|
+
const toolRouter = mcpServer.getToolRouter();
|
|
434
|
+
const tools = await toolRouter.getAllTools(true);
|
|
435
|
+
const toolsWithServer = tools.map(tool => {
|
|
436
|
+
const route = toolRouter.findToolRoute(tool.name);
|
|
437
|
+
return {
|
|
438
|
+
...tool,
|
|
439
|
+
serverName: route?.serverName || 'unknown'
|
|
440
|
+
};
|
|
441
|
+
});
|
|
442
|
+
res.json(toolsWithServer);
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
res.status(500).json({
|
|
446
|
+
error: '获取工具列表失败',
|
|
447
|
+
message: error.message
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async handleCallTool(req, res) {
|
|
452
|
+
try {
|
|
453
|
+
const { name } = req.params;
|
|
454
|
+
const { args } = req.body;
|
|
455
|
+
const mcpServer = this.daemon['mcpServer'];
|
|
456
|
+
const toolRouter = mcpServer.getToolRouter();
|
|
457
|
+
const result = await toolRouter.callTool(name, args || {});
|
|
458
|
+
res.json(result);
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
res.status(500).json({
|
|
462
|
+
error: '工具调用失败',
|
|
463
|
+
message: error.message
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async handleGetConfig(req, res) {
|
|
468
|
+
try {
|
|
469
|
+
const configManager = this.daemon['configManager'];
|
|
470
|
+
const config = configManager.getConfig();
|
|
471
|
+
res.json(config);
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
res.status(500).json({
|
|
475
|
+
error: '获取配置失败',
|
|
476
|
+
message: error.message
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
async handleGetClients(req, res) {
|
|
481
|
+
try {
|
|
482
|
+
const clientInfos = this.daemon['clientInfos'];
|
|
483
|
+
const clientList = Array.from(clientInfos.entries()).map(([id, info]) => ({
|
|
484
|
+
id,
|
|
485
|
+
type: info.type,
|
|
486
|
+
lastSeen: info.lastSeen
|
|
487
|
+
}));
|
|
488
|
+
res.json(clientList);
|
|
489
|
+
}
|
|
490
|
+
catch (error) {
|
|
491
|
+
res.status(500).json({
|
|
492
|
+
error: '获取客户端列表失败',
|
|
493
|
+
message: error.message
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async handleReloadConfig(req, res) {
|
|
498
|
+
try {
|
|
499
|
+
await this.daemon['reloadConfig']();
|
|
500
|
+
res.json({ success: true, message: '配置已重载' });
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
res.status(500).json({
|
|
504
|
+
error: '配置重载失败',
|
|
505
|
+
message: error.message
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// ===== AI gateway & providers handlers =====
|
|
510
|
+
async handleGetAiGatewayStatus(req, res) {
|
|
511
|
+
const cfg = this.configManager.getAIGatewayConfig();
|
|
512
|
+
const gateway = this.daemon.getAiGateway?.();
|
|
513
|
+
res.json({
|
|
514
|
+
running: gateway?.isRunning() ?? false,
|
|
515
|
+
enabled: cfg?.enabled ?? false,
|
|
516
|
+
port: cfg?.port ?? 62125,
|
|
517
|
+
host: cfg?.host ?? '127.0.0.1',
|
|
518
|
+
apiKey: cfg?.apiKey ?? '',
|
|
519
|
+
providers: this.configManager.getAIProviders().map(p => ({
|
|
520
|
+
id: p.id, slug: p.slug, dialect: p.dialect, enabled: p.enabled,
|
|
521
|
+
})),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
async handleUpdateAiGatewaySettings(req, res) {
|
|
525
|
+
try {
|
|
526
|
+
const { enabled, port, host, resetApiKey } = req.body || {};
|
|
527
|
+
const cfg = this.configManager.getAIGatewayConfig() || {
|
|
528
|
+
enabled: false, port: 62125, host: '127.0.0.1', apiKey: '',
|
|
529
|
+
};
|
|
530
|
+
if (typeof enabled === 'boolean') {
|
|
531
|
+
cfg.enabled = enabled;
|
|
532
|
+
}
|
|
533
|
+
if (typeof port === 'number' && port > 0 && port < 65536) {
|
|
534
|
+
cfg.port = port;
|
|
535
|
+
}
|
|
536
|
+
if (typeof host === 'string' && host) {
|
|
537
|
+
cfg.host = host;
|
|
538
|
+
}
|
|
539
|
+
if (resetApiKey === true) {
|
|
540
|
+
cfg.apiKey = `ad-sk-${randomUUID()}`;
|
|
541
|
+
}
|
|
542
|
+
// 启用时确保有 apiKey(首次自动生成)
|
|
543
|
+
if (cfg.enabled && !cfg.apiKey) {
|
|
544
|
+
cfg.apiKey = `ad-sk-${randomUUID()}`;
|
|
545
|
+
}
|
|
546
|
+
await this.configManager.setAIGateway(cfg);
|
|
547
|
+
await this.daemon.restartAiGateway();
|
|
548
|
+
res.json({ success: true, message: 'AI 网关设置已更新', apiKey: cfg.apiKey, enabled: cfg.enabled, port: cfg.port, host: cfg.host });
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
res.status(500).json({ error: '更新 AI 网关设置失败', message: error.message });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// apiKey 脱敏:仅保留末 4 位;短 key 全遮蔽
|
|
555
|
+
maskKey(key) {
|
|
556
|
+
if (!key || key.length <= 4) {
|
|
557
|
+
return '****';
|
|
558
|
+
}
|
|
559
|
+
return '*'.repeat(key.length - 4) + key.slice(-4);
|
|
560
|
+
}
|
|
561
|
+
maskProvider(p) {
|
|
562
|
+
return { ...p, apiKey: this.maskKey(p.apiKey), apiKeyMasked: this.maskKey(p.apiKey) };
|
|
563
|
+
}
|
|
564
|
+
async handleGetAiProviders(req, res) {
|
|
565
|
+
res.json(this.configManager.getAIProviders().map(p => this.maskProvider(p)));
|
|
566
|
+
}
|
|
567
|
+
async handleAddAiProvider(req, res) {
|
|
568
|
+
const { slug, name, dialect, baseUrl, apiKey, enabled, models, headers } = req.body || {};
|
|
569
|
+
if (!slug || !dialect || !baseUrl || !apiKey) {
|
|
570
|
+
res.status(400).json({ error: 'slug, dialect, baseUrl, apiKey 为必填项' });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (!['openai', 'anthropic', 'gemini'].includes(dialect)) {
|
|
574
|
+
res.status(400).json({ error: `invalid dialect: ${dialect}` });
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (!isValidProviderSlug(slug)) {
|
|
578
|
+
res.status(400).json({ error: 'slug 格式非法:须匹配 /^[a-z0-9][a-z0-9-]*$/' });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const provider = {
|
|
582
|
+
id: randomUUID(), slug, name, dialect, baseUrl, apiKey,
|
|
583
|
+
enabled: enabled !== false, models, headers,
|
|
584
|
+
};
|
|
585
|
+
if (!this.configManager.addAIProvider(provider)) {
|
|
586
|
+
res.status(400).json({ error: `slug "${slug}" 已存在或格式非法` });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
res.status(201).json({ message: '供应商已创建', provider: this.maskProvider(provider) });
|
|
590
|
+
}
|
|
591
|
+
async handleUpdateAiProvider(req, res) {
|
|
592
|
+
const { id } = req.params;
|
|
593
|
+
const updates = req.body || {};
|
|
594
|
+
delete updates.id;
|
|
595
|
+
if (!this.configManager.updateAIProvider(id, updates)) {
|
|
596
|
+
res.status(400).json({ error: '更新失败:id 不存在、slug 冲突或格式非法' });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const updated = this.configManager.getAIProviders().find(p => p.id === id);
|
|
600
|
+
res.json({ message: '供应商已更新', provider: updated ? this.maskProvider(updated) : null });
|
|
601
|
+
}
|
|
602
|
+
async handleDeleteAiProvider(req, res) {
|
|
603
|
+
const { id } = req.params;
|
|
604
|
+
if (!this.configManager.removeAIProvider(id)) {
|
|
605
|
+
res.status(404).json({ error: '供应商不存在' });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
res.json({ message: '供应商已删除', id });
|
|
609
|
+
}
|
|
610
|
+
async handleTestAiProvider(req, res) {
|
|
611
|
+
const provider = this.configManager.getAIProviders().find(p => p.id === req.params.id);
|
|
612
|
+
if (!provider) {
|
|
613
|
+
res.status(404).json({ error: '供应商不存在' });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
// 测试时允许用请求体带来的新 apiKey(编辑未保存时测试)
|
|
617
|
+
const withOverrides = { ...provider, ...(req.body?.apiKey ? { apiKey: req.body.apiKey } : {}) };
|
|
618
|
+
const result = await listUpstreamModels(withOverrides);
|
|
619
|
+
res.json({ ok: result.ok, status: result.status, message: result.message, modelCount: result.models.length });
|
|
620
|
+
}
|
|
621
|
+
async handleFetchProviderModels(req, res) {
|
|
622
|
+
const provider = this.configManager.getAIProviders().find(p => p.id === req.params.id);
|
|
623
|
+
if (!provider) {
|
|
624
|
+
res.status(404).json({ error: '供应商不存在' });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const withOverrides = { ...provider, ...(req.body?.apiKey ? { apiKey: req.body.apiKey } : {}) };
|
|
628
|
+
const result = await listUpstreamModels(withOverrides);
|
|
629
|
+
if (!result.ok) {
|
|
630
|
+
res.status(502).json({ error: '拉取上游模型列表失败', message: result.message, status: result.status });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
res.json({ models: result.models });
|
|
634
|
+
}
|
|
635
|
+
// WebSocket helper methods
|
|
636
|
+
async sendStatusUpdate(socket) {
|
|
637
|
+
try {
|
|
638
|
+
const status = this.daemon['getFullStatus']();
|
|
639
|
+
// Get all servers from config file to merge configuration information
|
|
640
|
+
const configManager = this.daemon['configManager'];
|
|
641
|
+
const config = configManager.getConfig();
|
|
642
|
+
const configServers = config.servers || {};
|
|
643
|
+
const toolRouter = this.daemon['mcpServer'].getToolRouter();
|
|
644
|
+
// Get latest adapter status directly from toolRouter to avoid caching issues
|
|
645
|
+
const allAdapters = toolRouter.getAllAdapters();
|
|
646
|
+
const runtimeMap = new Map();
|
|
647
|
+
allAdapters.forEach((adapter) => {
|
|
648
|
+
runtimeMap.set(adapter.name, {
|
|
649
|
+
connected: adapter.isConnected,
|
|
650
|
+
toolCount: toolRouter.getToolsByServer(adapter.name).length,
|
|
651
|
+
enabledToolCount: toolRouter.getEnabledToolsByServer(adapter.name).length
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
// Merge configuration and runtime information
|
|
655
|
+
const serversWithTools = Object.entries(configServers).map(([serverName, serverConfig]) => {
|
|
656
|
+
const runtimeInfo = runtimeMap.get(serverName);
|
|
657
|
+
const isConnected = runtimeInfo ? !!runtimeInfo.connected : false;
|
|
658
|
+
const toolCount = runtimeInfo ? runtimeInfo.toolCount : 0;
|
|
659
|
+
const enabledToolCount = runtimeInfo ? runtimeInfo.enabledToolCount : 0;
|
|
660
|
+
const tools = isConnected ? toolRouter.getToolsByServer(serverName) : [];
|
|
661
|
+
return {
|
|
662
|
+
// Include all ServerConfig properties and ensure name is set correctly
|
|
663
|
+
...serverConfig,
|
|
664
|
+
name: serverName, // Always use the key as the definitive name
|
|
665
|
+
// Add server runtime status
|
|
666
|
+
connected: isConnected,
|
|
667
|
+
toolCount: toolCount,
|
|
668
|
+
enabledToolCount: enabledToolCount,
|
|
669
|
+
tools: tools.map(tool => ({
|
|
670
|
+
name: tool.name,
|
|
671
|
+
description: tool.description,
|
|
672
|
+
enabled: this.isToolEnabled(serverConfig, tool.name),
|
|
673
|
+
inputSchema: tool.inputSchema,
|
|
674
|
+
settings: {}
|
|
675
|
+
}))
|
|
676
|
+
};
|
|
677
|
+
});
|
|
678
|
+
// Send status update with structure consistent with /api/servers
|
|
679
|
+
const enhancedStatus = {
|
|
680
|
+
...status,
|
|
681
|
+
servers: serversWithTools
|
|
682
|
+
};
|
|
683
|
+
console.log(`[DAEMON-WEB] Broadcasting status update: ${serversWithTools.map(s => `${s.name}(connected:${s.connected}, tools:${s.toolCount})`).join(', ')}`);
|
|
684
|
+
socket.emit('status-update', enhancedStatus);
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
console.error('[DAEMON-WEB] Error sending status update:', error);
|
|
688
|
+
socket.emit('error', {
|
|
689
|
+
error: '获取状态失败',
|
|
690
|
+
message: error.message
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
broadcastStatusUpdate() {
|
|
695
|
+
this.io.sockets.sockets.forEach(socket => {
|
|
696
|
+
this.sendStatusUpdate(socket);
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
// Get complete system status
|
|
700
|
+
getSystemStatus() {
|
|
701
|
+
try {
|
|
702
|
+
const toolRouter = this.daemon['mcpServer'].getToolRouter();
|
|
703
|
+
const configManager = this.daemon['configManager'];
|
|
704
|
+
const config = configManager.getConfig();
|
|
705
|
+
const serversConfig = config?.servers || {};
|
|
706
|
+
const enabledServers = Object.keys(serversConfig).filter(name => serversConfig[name].enabled !== false);
|
|
707
|
+
// Get latest adapter status directly from toolRouter to avoid caching issues
|
|
708
|
+
const allAdapters = toolRouter.getAllAdapters();
|
|
709
|
+
const runtimeMap = new Map();
|
|
710
|
+
allAdapters.forEach((adapter) => {
|
|
711
|
+
runtimeMap.set(adapter.name, {
|
|
712
|
+
connected: adapter.isConnected,
|
|
713
|
+
toolCount: toolRouter.getToolsByServer(adapter.name).length,
|
|
714
|
+
enabledToolCount: toolRouter.getEnabledToolsByServer(adapter.name).length
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
const connectedCount = allAdapters.filter((adapter) => adapter.isConnected).length;
|
|
718
|
+
const totalTools = allAdapters.reduce((sum, adapter) => {
|
|
719
|
+
return sum + toolRouter.getToolsByServer(adapter.name).length;
|
|
720
|
+
}, 0);
|
|
721
|
+
const totalEnabledTools = allAdapters.reduce((sum, adapter) => {
|
|
722
|
+
return sum + toolRouter.getEnabledToolsByServer(adapter.name).length;
|
|
723
|
+
}, 0);
|
|
724
|
+
const serversWithTools = enabledServers.map(serverName => {
|
|
725
|
+
const serverConfig = { ...serversConfig[serverName], name: serverName };
|
|
726
|
+
const runtimeInfo = runtimeMap.get(serverName);
|
|
727
|
+
const isConnected = runtimeInfo ? runtimeInfo.connected : false;
|
|
728
|
+
const toolCount = runtimeInfo ? runtimeInfo.toolCount : 0;
|
|
729
|
+
const enabledToolCount = runtimeInfo ? runtimeInfo.enabledToolCount : 0;
|
|
730
|
+
const tools = isConnected ? toolRouter.getToolsByServer(serverConfig.name) : [];
|
|
731
|
+
return {
|
|
732
|
+
...serverConfig,
|
|
733
|
+
connected: isConnected,
|
|
734
|
+
toolCount: toolCount,
|
|
735
|
+
enabledToolCount: enabledToolCount,
|
|
736
|
+
tools: tools.map(tool => ({
|
|
737
|
+
name: tool.name,
|
|
738
|
+
description: tool.description,
|
|
739
|
+
enabled: this.isToolEnabled(serverConfig, tool.name),
|
|
740
|
+
inputSchema: tool.inputSchema,
|
|
741
|
+
settings: {}
|
|
742
|
+
}))
|
|
743
|
+
};
|
|
744
|
+
});
|
|
745
|
+
return {
|
|
746
|
+
total: enabledServers.length,
|
|
747
|
+
connected: connectedCount,
|
|
748
|
+
totalTools: totalTools, // This should be total tools
|
|
749
|
+
enabledTools: totalEnabledTools, // This should be enabled tools
|
|
750
|
+
servers: serversWithTools
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
catch (error) {
|
|
754
|
+
console.error('[DAEMON-WEB] Error getting system status:', error);
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async handleAddServer(req, res) {
|
|
759
|
+
try {
|
|
760
|
+
const { name, config } = req.body;
|
|
761
|
+
const configManager = this.daemon['configManager'];
|
|
762
|
+
// Validate server name
|
|
763
|
+
const nameValidation = ServerNameValidator.validateServerName(name);
|
|
764
|
+
if (!nameValidation.valid) {
|
|
765
|
+
return res.status(400).json({
|
|
766
|
+
error: '服务器名称无效',
|
|
767
|
+
details: nameValidation.error,
|
|
768
|
+
suggestions: nameValidation.suggestions
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
// Check for name conflicts
|
|
772
|
+
if (configManager.checkServerNameConflict(name)) {
|
|
773
|
+
return res.status(409).json({
|
|
774
|
+
error: '服务器名称已存在',
|
|
775
|
+
existingName: name
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
try {
|
|
779
|
+
// Add the server
|
|
780
|
+
configManager.addServer(name, config);
|
|
781
|
+
await configManager.saveConfig();
|
|
782
|
+
await configManager.loadConfig();
|
|
783
|
+
// Only start the server if it's enabled, without reloading all config
|
|
784
|
+
if (config.enabled) {
|
|
785
|
+
console.log(`[DAEMON-WEB] Server ${name} is enabled, starting it directly`);
|
|
786
|
+
// Use configManager's toggleServer method to start the server
|
|
787
|
+
configManager.toggleServer(name, true);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
console.log(`[DAEMON-WEB] Server ${name} is disabled, skipping start`);
|
|
791
|
+
}
|
|
792
|
+
// Emit a server-added event for the specific server
|
|
793
|
+
const systemStatus = this.getSystemStatus();
|
|
794
|
+
if (systemStatus) {
|
|
795
|
+
this.io.emit('server-status-changed', {
|
|
796
|
+
event: 'server-added',
|
|
797
|
+
serverName: name,
|
|
798
|
+
systemStatus: systemStatus,
|
|
799
|
+
originalData: { name, ...config },
|
|
800
|
+
timestamp: new Date().toISOString()
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
res.json({
|
|
804
|
+
success: true,
|
|
805
|
+
message: `服务器 ${name} 添加成功`,
|
|
806
|
+
server: { name, ...config }
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
catch (error) {
|
|
810
|
+
res.status(500).json({
|
|
811
|
+
error: '添加服务器失败',
|
|
812
|
+
message: error.message
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
catch (error) {
|
|
817
|
+
res.status(500).json({
|
|
818
|
+
error: '添加服务器失败',
|
|
819
|
+
message: error.message
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async handleUpdateServer(req, res) {
|
|
824
|
+
try {
|
|
825
|
+
const { name } = req.params;
|
|
826
|
+
const serverConfig = req.body;
|
|
827
|
+
const configManager = this.daemon['configManager'];
|
|
828
|
+
// If name is being updated, validate the new name
|
|
829
|
+
if (serverConfig.name && serverConfig.name !== name) {
|
|
830
|
+
const nameValidation = ServerNameValidator.validateServerName(serverConfig.name);
|
|
831
|
+
if (!nameValidation.valid) {
|
|
832
|
+
return res.status(400).json({
|
|
833
|
+
error: '服务器名称无效',
|
|
834
|
+
details: nameValidation.error,
|
|
835
|
+
suggestions: nameValidation.suggestions
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
// Check for name conflicts with other servers
|
|
839
|
+
if (configManager.checkServerNameConflict(serverConfig.name)) {
|
|
840
|
+
return res.status(409).json({
|
|
841
|
+
error: '服务器名称已存在',
|
|
842
|
+
existingName: serverConfig.name
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
// Get the old server config to check if it was enabled
|
|
848
|
+
const oldConfig = configManager.getServerConfig(name);
|
|
849
|
+
const wasEnabled = oldConfig?.enabled || false;
|
|
850
|
+
const nameChanged = serverConfig.name && serverConfig.name !== name;
|
|
851
|
+
// Update the server configuration
|
|
852
|
+
configManager.updateServer(name, serverConfig);
|
|
853
|
+
await configManager.saveConfig();
|
|
854
|
+
await configManager.loadConfig();
|
|
855
|
+
// Get the new server config
|
|
856
|
+
const newConfig = configManager.getServerConfig(serverConfig.name || name);
|
|
857
|
+
const isEnabled = newConfig?.enabled || false;
|
|
858
|
+
// Always restart the server if it was enabled, regardless of what changed
|
|
859
|
+
// This ensures any config change (command, args, env, etc.) takes effect
|
|
860
|
+
if (wasEnabled) {
|
|
861
|
+
console.log(`[DAEMON-WEB] Server ${name} config updated, restarting server`);
|
|
862
|
+
if (nameChanged) {
|
|
863
|
+
// Name changed: disable old server and enable new server
|
|
864
|
+
console.log(`[DAEMON-WEB] Disabling old server: ${name}`);
|
|
865
|
+
configManager.toggleServer(name, false);
|
|
866
|
+
console.log(`[DAEMON-WEB] Enabling new server: ${serverConfig.name}`);
|
|
867
|
+
configManager.toggleServer(serverConfig.name, true);
|
|
868
|
+
}
|
|
869
|
+
else {
|
|
870
|
+
// Config changed but name is the same: restart the server
|
|
871
|
+
console.log(`[DAEMON-WEB] Restarting server ${name} due to config change`);
|
|
872
|
+
configManager.toggleServer(name, false);
|
|
873
|
+
configManager.toggleServer(name, true);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
else if (wasEnabled !== isEnabled) {
|
|
877
|
+
// Only enabled status changed
|
|
878
|
+
console.log(`[DAEMON-WEB] Toggling server ${serverConfig.name || name} to ${isEnabled}`);
|
|
879
|
+
configManager.toggleServer(serverConfig.name || name, isEnabled);
|
|
880
|
+
}
|
|
881
|
+
else {
|
|
882
|
+
console.log(`[DAEMON-WEB] Server ${name} updated but was not enabled, no restart needed`);
|
|
883
|
+
}
|
|
884
|
+
// Emit a server-updated event for the specific server
|
|
885
|
+
const systemStatus = this.getSystemStatus();
|
|
886
|
+
if (systemStatus) {
|
|
887
|
+
this.io.emit('server-status-changed', {
|
|
888
|
+
event: 'server-updated',
|
|
889
|
+
serverName: serverConfig.name || name,
|
|
890
|
+
systemStatus: systemStatus,
|
|
891
|
+
originalData: { name: serverConfig.name || name, ...serverConfig },
|
|
892
|
+
timestamp: new Date().toISOString()
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
// Rely on daemon events to broadcast status update
|
|
896
|
+
res.json({
|
|
897
|
+
success: true,
|
|
898
|
+
message: `服务器 ${name} 已更新`,
|
|
899
|
+
server: { name: serverConfig.name || name, ...serverConfig }
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
catch (error) {
|
|
903
|
+
res.status(500).json({
|
|
904
|
+
error: '更新服务器失败',
|
|
905
|
+
message: error.message
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
res.status(500).json({
|
|
911
|
+
error: '更新服务器失败',
|
|
912
|
+
message: error.message
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async handleRemoveServer(req, res) {
|
|
917
|
+
try {
|
|
918
|
+
const { name } = req.params;
|
|
919
|
+
const configManager = this.daemon['configManager'];
|
|
920
|
+
// Get the server config before removing it to check if it was enabled
|
|
921
|
+
const serverConfig = configManager.getServerConfig(name);
|
|
922
|
+
const wasEnabled = serverConfig?.enabled || false;
|
|
923
|
+
await configManager.removeServer(name);
|
|
924
|
+
await configManager.saveConfig();
|
|
925
|
+
await configManager.loadConfig();
|
|
926
|
+
// Only stop the server if it was enabled, without reloading all config
|
|
927
|
+
if (wasEnabled) {
|
|
928
|
+
console.log(`[DAEMON-WEB] Server ${name} was enabled, stopping it directly`);
|
|
929
|
+
// Use configManager's toggleServer method to stop the server
|
|
930
|
+
configManager.toggleServer(name, false);
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
console.log(`[DAEMON-WEB] Server ${name} was disabled, skipping stop`);
|
|
934
|
+
}
|
|
935
|
+
// Emit a server-removed event for the specific server
|
|
936
|
+
const systemStatus = this.getSystemStatus();
|
|
937
|
+
if (systemStatus) {
|
|
938
|
+
this.io.emit('server-status-changed', {
|
|
939
|
+
event: 'server-removed',
|
|
940
|
+
serverName: name,
|
|
941
|
+
systemStatus: systemStatus,
|
|
942
|
+
originalData: { name },
|
|
943
|
+
timestamp: new Date().toISOString()
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
// Rely on daemon events to broadcast status update
|
|
947
|
+
res.json({ success: true, message: `服务器 ${name} 已删除` });
|
|
948
|
+
}
|
|
949
|
+
catch (error) {
|
|
950
|
+
res.status(500).json({
|
|
951
|
+
error: '删除服务器失败',
|
|
952
|
+
message: error.message
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
async handleToggleServer(req, res) {
|
|
957
|
+
try {
|
|
958
|
+
const { name } = req.params;
|
|
959
|
+
const configManager = this.daemon['configManager'];
|
|
960
|
+
const config = configManager.getConfig();
|
|
961
|
+
const serverConfig = config.servers[name];
|
|
962
|
+
if (!serverConfig) {
|
|
963
|
+
return res.status(404).json({ error: '未找到服务器' });
|
|
964
|
+
}
|
|
965
|
+
// Use configManager's toggleServer method, which automatically emits events
|
|
966
|
+
const oldEnabled = serverConfig.enabled;
|
|
967
|
+
await configManager.toggleServer(name, !oldEnabled);
|
|
968
|
+
// Save configuration to persist the change
|
|
969
|
+
await configManager.saveConfig();
|
|
970
|
+
console.log(`[DAEMON-WEB] Server ${name} toggled: ${oldEnabled} -> ${!oldEnabled}`);
|
|
971
|
+
// ConfigManager's toggleServer method emits a server-toggled event
|
|
972
|
+
// MCPDogServer already listens to this event and automatically handles server start/stop
|
|
973
|
+
// No need to manually call reloadConfig() or manually connect/disconnect servers
|
|
974
|
+
// Send composite event, including toggle operation and latest status
|
|
975
|
+
setTimeout(() => {
|
|
976
|
+
const systemStatus = this.getSystemStatus();
|
|
977
|
+
if (systemStatus) {
|
|
978
|
+
console.log(`[DAEMON-WEB] Sending server-status-changed event for toggle: ${name}`);
|
|
979
|
+
this.io.emit('server-status-changed', {
|
|
980
|
+
event: serverConfig.enabled ? 'server-enabled' : 'server-disabled',
|
|
981
|
+
serverName: name,
|
|
982
|
+
systemStatus: systemStatus,
|
|
983
|
+
originalData: { enabled: serverConfig.enabled },
|
|
984
|
+
timestamp: new Date().toISOString()
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
// If status retrieval fails, fall back to original method
|
|
989
|
+
this.broadcastStatusUpdate();
|
|
990
|
+
}
|
|
991
|
+
}, 100); // Short delay to ensure config is saved
|
|
992
|
+
res.json({
|
|
993
|
+
success: true,
|
|
994
|
+
server: name,
|
|
995
|
+
enabled: serverConfig.enabled,
|
|
996
|
+
message: `服务器 ${name} 已${serverConfig.enabled ? '启用' : '禁用'}`
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
catch (error) {
|
|
1000
|
+
res.status(500).json({
|
|
1001
|
+
error: '切换服务器状态失败',
|
|
1002
|
+
message: error.message
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async handleUpdateConfig(req, res) {
|
|
1007
|
+
try {
|
|
1008
|
+
const newConfig = req.body;
|
|
1009
|
+
const configManager = this.daemon['configManager'];
|
|
1010
|
+
// Update config
|
|
1011
|
+
configManager['config'] = newConfig; // Directly set config
|
|
1012
|
+
await configManager.saveConfig();
|
|
1013
|
+
// Reload daemon configuration
|
|
1014
|
+
await this.daemon['reloadConfig']();
|
|
1015
|
+
res.json({ success: true, message: '配置更新成功' });
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
res.status(500).json({
|
|
1019
|
+
error: '更新配置失败',
|
|
1020
|
+
message: error.message
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
// New tool-level control API handler
|
|
1025
|
+
async handleGetServerTools(req, res) {
|
|
1026
|
+
try {
|
|
1027
|
+
const { name } = req.params;
|
|
1028
|
+
const mcpServer = this.daemon['mcpServer'];
|
|
1029
|
+
const configManager = this.daemon['configManager'];
|
|
1030
|
+
const serverConfig = configManager.getServerConfig(name);
|
|
1031
|
+
if (!serverConfig) {
|
|
1032
|
+
return res.status(404).json({ error: '未找到服务器' });
|
|
1033
|
+
}
|
|
1034
|
+
const toolRouter = mcpServer.getToolRouter();
|
|
1035
|
+
const allTools = toolRouter.getToolsByServer(name);
|
|
1036
|
+
// Apply tool filtering config
|
|
1037
|
+
const toolsWithConfig = allTools.map(tool => ({
|
|
1038
|
+
...tool,
|
|
1039
|
+
enabled: this.isToolEnabled(serverConfig, tool.name),
|
|
1040
|
+
settings: serverConfig.toolsConfig?.toolSettings?.[tool.name] || {}
|
|
1041
|
+
}));
|
|
1042
|
+
res.json({
|
|
1043
|
+
serverName: name,
|
|
1044
|
+
toolsConfig: serverConfig.toolsConfig || { mode: 'all' },
|
|
1045
|
+
tools: toolsWithConfig
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
catch (error) {
|
|
1049
|
+
res.status(500).json({
|
|
1050
|
+
error: '获取服务器工具失败',
|
|
1051
|
+
message: error.message
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
async handleToggleServerTool(req, res) {
|
|
1056
|
+
try {
|
|
1057
|
+
const { name, tool } = req.params;
|
|
1058
|
+
const configManager = this.daemon['configManager'];
|
|
1059
|
+
const config = configManager.getConfig();
|
|
1060
|
+
const serverConfig = config.servers[name];
|
|
1061
|
+
if (!serverConfig) {
|
|
1062
|
+
return res.status(404).json({ error: '未找到服务器' });
|
|
1063
|
+
}
|
|
1064
|
+
// Initialize tool config
|
|
1065
|
+
if (!serverConfig.toolsConfig) {
|
|
1066
|
+
serverConfig.toolsConfig = { mode: 'all' };
|
|
1067
|
+
}
|
|
1068
|
+
if (!serverConfig.toolsConfig.toolSettings) {
|
|
1069
|
+
serverConfig.toolsConfig.toolSettings = {};
|
|
1070
|
+
}
|
|
1071
|
+
// Toggle tool status
|
|
1072
|
+
const currentEnabled = this.isToolEnabled(serverConfig, tool);
|
|
1073
|
+
serverConfig.toolsConfig.toolSettings[tool] = {
|
|
1074
|
+
...serverConfig.toolsConfig.toolSettings[tool],
|
|
1075
|
+
enabled: !currentEnabled
|
|
1076
|
+
};
|
|
1077
|
+
// If mode is 'all', switch to 'blacklist' or 'whitelist' mode
|
|
1078
|
+
if (serverConfig.toolsConfig.mode === 'all') {
|
|
1079
|
+
serverConfig.toolsConfig.mode = currentEnabled ? 'blacklist' : 'whitelist';
|
|
1080
|
+
}
|
|
1081
|
+
// Save configuration with tool-toggle context to avoid server reconnection
|
|
1082
|
+
await configManager.saveConfig();
|
|
1083
|
+
// Just broadcast status update instead of full reload
|
|
1084
|
+
this.broadcastStatusUpdate();
|
|
1085
|
+
res.json({
|
|
1086
|
+
success: true,
|
|
1087
|
+
tool,
|
|
1088
|
+
enabled: !currentEnabled,
|
|
1089
|
+
message: `工具 ${tool} 已${!currentEnabled ? '启用' : '禁用'}`
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
res.status(500).json({
|
|
1094
|
+
error: '工具切换失败',
|
|
1095
|
+
message: error.message
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
async handleUpdateServerTools(req, res) {
|
|
1100
|
+
try {
|
|
1101
|
+
const { name } = req.params;
|
|
1102
|
+
const { toolsConfig } = req.body;
|
|
1103
|
+
const configManager = this.daemon['configManager'];
|
|
1104
|
+
const config = configManager.getConfig();
|
|
1105
|
+
const serverConfig = config.servers[name];
|
|
1106
|
+
if (!serverConfig) {
|
|
1107
|
+
return res.status(404).json({ error: '未找到服务器' });
|
|
1108
|
+
}
|
|
1109
|
+
// Update tool config
|
|
1110
|
+
serverConfig.toolsConfig = toolsConfig;
|
|
1111
|
+
// Save configuration with tool-config-update context to avoid server reconnection
|
|
1112
|
+
await configManager.saveConfig();
|
|
1113
|
+
// Just broadcast status update instead of full reload
|
|
1114
|
+
this.broadcastStatusUpdate();
|
|
1115
|
+
res.json({ success: true, message: '服务器工具配置已更新' });
|
|
1116
|
+
}
|
|
1117
|
+
catch (error) {
|
|
1118
|
+
res.status(500).json({
|
|
1119
|
+
error: '更新服务器工具失败',
|
|
1120
|
+
message: error.message
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
// Helper method: check if tool is enabled
|
|
1125
|
+
isToolEnabled(serverConfig, toolName) {
|
|
1126
|
+
const toolsConfig = serverConfig.toolsConfig;
|
|
1127
|
+
if (!toolsConfig) {
|
|
1128
|
+
return true; // Default to all enabled
|
|
1129
|
+
}
|
|
1130
|
+
// Check specific tool settings
|
|
1131
|
+
const toolSettings = toolsConfig.toolSettings?.[toolName];
|
|
1132
|
+
if (toolSettings !== undefined) {
|
|
1133
|
+
return toolSettings.enabled;
|
|
1134
|
+
}
|
|
1135
|
+
// Determine based on mode
|
|
1136
|
+
switch (toolsConfig.mode) {
|
|
1137
|
+
case 'all':
|
|
1138
|
+
return true;
|
|
1139
|
+
case 'whitelist':
|
|
1140
|
+
return toolsConfig.enabledTools?.includes(toolName) || false;
|
|
1141
|
+
case 'blacklist':
|
|
1142
|
+
return !toolsConfig.disabledTools?.includes(toolName);
|
|
1143
|
+
default:
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
// Log API handlers
|
|
1148
|
+
async handleGetAllLogs(req, res) {
|
|
1149
|
+
try {
|
|
1150
|
+
const limit = parseInt(req.query.limit) || 100;
|
|
1151
|
+
const logs = globalLogManager.getAllRecentLogs(limit);
|
|
1152
|
+
res.json(logs);
|
|
1153
|
+
}
|
|
1154
|
+
catch (error) {
|
|
1155
|
+
res.status(500).json({
|
|
1156
|
+
error: '获取日志失败',
|
|
1157
|
+
message: error.message
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
async handleGetServerLogs(req, res) {
|
|
1162
|
+
try {
|
|
1163
|
+
const { serverName } = req.params;
|
|
1164
|
+
const limit = parseInt(req.query.limit) || 100;
|
|
1165
|
+
const level = req.query.level;
|
|
1166
|
+
const source = req.query.source;
|
|
1167
|
+
const search = req.query.search;
|
|
1168
|
+
let logs;
|
|
1169
|
+
if (search) {
|
|
1170
|
+
logs = globalLogManager.searchLogs(serverName, search, { level: level, source: source, limit });
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
logs = globalLogManager.getLogs(serverName, limit);
|
|
1174
|
+
}
|
|
1175
|
+
res.json(logs);
|
|
1176
|
+
}
|
|
1177
|
+
catch (error) {
|
|
1178
|
+
res.status(500).json({
|
|
1179
|
+
error: '获取服务器日志失败',
|
|
1180
|
+
message: error.message
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
async handleClearServerLogs(req, res) {
|
|
1185
|
+
try {
|
|
1186
|
+
const { serverName } = req.params;
|
|
1187
|
+
globalLogManager.clearLogs(serverName);
|
|
1188
|
+
res.json({ success: true, message: `已清空 ${serverName} 的日志` });
|
|
1189
|
+
}
|
|
1190
|
+
catch (error) {
|
|
1191
|
+
res.status(500).json({
|
|
1192
|
+
error: '清空服务器日志失败',
|
|
1193
|
+
message: error.message
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
async handleGetServerLogStats(req, res) {
|
|
1198
|
+
try {
|
|
1199
|
+
const { serverName } = req.params;
|
|
1200
|
+
const stats = globalLogManager.getStats(serverName);
|
|
1201
|
+
if (!stats) {
|
|
1202
|
+
return res.status(404).json({ error: '未找到服务器' });
|
|
1203
|
+
}
|
|
1204
|
+
res.json(stats);
|
|
1205
|
+
}
|
|
1206
|
+
catch (error) {
|
|
1207
|
+
res.status(500).json({
|
|
1208
|
+
error: '获取服务器日志统计失败',
|
|
1209
|
+
message: error.message
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// Server control
|
|
1214
|
+
async start() {
|
|
1215
|
+
const host = resolveListenHost(this.host, Boolean(process.env.MCPDOG_AUTH_TOKEN));
|
|
1216
|
+
return new Promise((resolve, reject) => {
|
|
1217
|
+
this.server.listen(this.port, host, () => {
|
|
1218
|
+
console.log(`[DAEMON-WEB] Web interface started on http://${host}:${this.port}`);
|
|
1219
|
+
console.log(`[DAEMON-WEB] WebSocket: ws://${host}:${this.port}`);
|
|
1220
|
+
resolve();
|
|
1221
|
+
});
|
|
1222
|
+
this.server.on('error', (error) => {
|
|
1223
|
+
reject(error);
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
async stop() {
|
|
1228
|
+
return new Promise((resolve) => {
|
|
1229
|
+
this.server.close(() => {
|
|
1230
|
+
console.log('[DAEMON-WEB] Web server stopped');
|
|
1231
|
+
resolve();
|
|
1232
|
+
});
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
//# sourceMappingURL=daemon-web-server.js.map
|