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.
Files changed (291) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +244 -0
  3. package/dist/adapters/adapter-factory.d.ts +21 -0
  4. package/dist/adapters/adapter-factory.d.ts.map +1 -0
  5. package/dist/adapters/adapter-factory.js +157 -0
  6. package/dist/adapters/adapter-factory.js.map +1 -0
  7. package/dist/adapters/http-sse-adapter.d.ts +49 -0
  8. package/dist/adapters/http-sse-adapter.d.ts.map +1 -0
  9. package/dist/adapters/http-sse-adapter.js +485 -0
  10. package/dist/adapters/http-sse-adapter.js.map +1 -0
  11. package/dist/adapters/stdio-adapter.d.ts +69 -0
  12. package/dist/adapters/stdio-adapter.d.ts.map +1 -0
  13. package/dist/adapters/stdio-adapter.js +664 -0
  14. package/dist/adapters/stdio-adapter.js.map +1 -0
  15. package/dist/adapters/streamable-http-adapter.d.ts +38 -0
  16. package/dist/adapters/streamable-http-adapter.d.ts.map +1 -0
  17. package/dist/adapters/streamable-http-adapter.js +321 -0
  18. package/dist/adapters/streamable-http-adapter.js.map +1 -0
  19. package/dist/ai-gateway/anthropic-errors.d.ts +4 -0
  20. package/dist/ai-gateway/anthropic-errors.d.ts.map +1 -0
  21. package/dist/ai-gateway/anthropic-errors.js +17 -0
  22. package/dist/ai-gateway/anthropic-errors.js.map +1 -0
  23. package/dist/ai-gateway/auth.d.ts +3 -0
  24. package/dist/ai-gateway/auth.d.ts.map +1 -0
  25. package/dist/ai-gateway/auth.js +14 -0
  26. package/dist/ai-gateway/auth.js.map +1 -0
  27. package/dist/ai-gateway/auth.test.d.ts +2 -0
  28. package/dist/ai-gateway/auth.test.d.ts.map +1 -0
  29. package/dist/ai-gateway/auth.test.js +57 -0
  30. package/dist/ai-gateway/auth.test.js.map +1 -0
  31. package/dist/ai-gateway/count-tokens.d.ts +5 -0
  32. package/dist/ai-gateway/count-tokens.d.ts.map +1 -0
  33. package/dist/ai-gateway/count-tokens.js +37 -0
  34. package/dist/ai-gateway/count-tokens.js.map +1 -0
  35. package/dist/ai-gateway/count-tokens.test.d.ts +2 -0
  36. package/dist/ai-gateway/count-tokens.test.d.ts.map +1 -0
  37. package/dist/ai-gateway/count-tokens.test.js +19 -0
  38. package/dist/ai-gateway/count-tokens.test.js.map +1 -0
  39. package/dist/ai-gateway/dialects.d.ts +4 -0
  40. package/dist/ai-gateway/dialects.d.ts.map +1 -0
  41. package/dist/ai-gateway/dialects.js +15 -0
  42. package/dist/ai-gateway/dialects.js.map +1 -0
  43. package/dist/ai-gateway/gateway-server.d.ts +15 -0
  44. package/dist/ai-gateway/gateway-server.d.ts.map +1 -0
  45. package/dist/ai-gateway/gateway-server.js +121 -0
  46. package/dist/ai-gateway/gateway-server.js.map +1 -0
  47. package/dist/ai-gateway/integration.test.d.ts +2 -0
  48. package/dist/ai-gateway/integration.test.d.ts.map +1 -0
  49. package/dist/ai-gateway/integration.test.js +205 -0
  50. package/dist/ai-gateway/integration.test.js.map +1 -0
  51. package/dist/ai-gateway/ir/anthropic-in.d.ts +3 -0
  52. package/dist/ai-gateway/ir/anthropic-in.d.ts.map +1 -0
  53. package/dist/ai-gateway/ir/anthropic-in.js +121 -0
  54. package/dist/ai-gateway/ir/anthropic-in.js.map +1 -0
  55. package/dist/ai-gateway/ir/anthropic-in.test.d.ts +2 -0
  56. package/dist/ai-gateway/ir/anthropic-in.test.d.ts.map +1 -0
  57. package/dist/ai-gateway/ir/anthropic-in.test.js +93 -0
  58. package/dist/ai-gateway/ir/anthropic-in.test.js.map +1 -0
  59. package/dist/ai-gateway/ir/anthropic-sse.d.ts +22 -0
  60. package/dist/ai-gateway/ir/anthropic-sse.d.ts.map +1 -0
  61. package/dist/ai-gateway/ir/anthropic-sse.js +200 -0
  62. package/dist/ai-gateway/ir/anthropic-sse.js.map +1 -0
  63. package/dist/ai-gateway/ir/anthropic-sse.test.d.ts +2 -0
  64. package/dist/ai-gateway/ir/anthropic-sse.test.d.ts.map +1 -0
  65. package/dist/ai-gateway/ir/anthropic-sse.test.js +101 -0
  66. package/dist/ai-gateway/ir/anthropic-sse.test.js.map +1 -0
  67. package/dist/ai-gateway/ir/gemini-out.d.ts +20 -0
  68. package/dist/ai-gateway/ir/gemini-out.d.ts.map +1 -0
  69. package/dist/ai-gateway/ir/gemini-out.js +218 -0
  70. package/dist/ai-gateway/ir/gemini-out.js.map +1 -0
  71. package/dist/ai-gateway/ir/gemini-out.test.d.ts +2 -0
  72. package/dist/ai-gateway/ir/gemini-out.test.d.ts.map +1 -0
  73. package/dist/ai-gateway/ir/gemini-out.test.js +125 -0
  74. package/dist/ai-gateway/ir/gemini-out.test.js.map +1 -0
  75. package/dist/ai-gateway/ir/openai-out.d.ts +17 -0
  76. package/dist/ai-gateway/ir/openai-out.d.ts.map +1 -0
  77. package/dist/ai-gateway/ir/openai-out.js +226 -0
  78. package/dist/ai-gateway/ir/openai-out.js.map +1 -0
  79. package/dist/ai-gateway/ir/openai-out.test.d.ts +2 -0
  80. package/dist/ai-gateway/ir/openai-out.test.d.ts.map +1 -0
  81. package/dist/ai-gateway/ir/openai-out.test.js +144 -0
  82. package/dist/ai-gateway/ir/openai-out.test.js.map +1 -0
  83. package/dist/ai-gateway/ir/types.d.ts +77 -0
  84. package/dist/ai-gateway/ir/types.d.ts.map +1 -0
  85. package/dist/ai-gateway/ir/types.js +4 -0
  86. package/dist/ai-gateway/ir/types.js.map +1 -0
  87. package/dist/ai-gateway/ir-channel.d.ts +15 -0
  88. package/dist/ai-gateway/ir-channel.d.ts.map +1 -0
  89. package/dist/ai-gateway/ir-channel.js +120 -0
  90. package/dist/ai-gateway/ir-channel.js.map +1 -0
  91. package/dist/ai-gateway/model-router.d.ts +11 -0
  92. package/dist/ai-gateway/model-router.d.ts.map +1 -0
  93. package/dist/ai-gateway/model-router.js +27 -0
  94. package/dist/ai-gateway/model-router.js.map +1 -0
  95. package/dist/ai-gateway/model-router.test.d.ts +2 -0
  96. package/dist/ai-gateway/model-router.test.d.ts.map +1 -0
  97. package/dist/ai-gateway/model-router.test.js +43 -0
  98. package/dist/ai-gateway/model-router.test.js.map +1 -0
  99. package/dist/ai-gateway/passthrough.d.ts +9 -0
  100. package/dist/ai-gateway/passthrough.d.ts.map +1 -0
  101. package/dist/ai-gateway/passthrough.js +81 -0
  102. package/dist/ai-gateway/passthrough.js.map +1 -0
  103. package/dist/ai-gateway/passthrough.test.d.ts +2 -0
  104. package/dist/ai-gateway/passthrough.test.d.ts.map +1 -0
  105. package/dist/ai-gateway/passthrough.test.js +108 -0
  106. package/dist/ai-gateway/passthrough.test.js.map +1 -0
  107. package/dist/ai-gateway/upstream/model-lister.d.ts +13 -0
  108. package/dist/ai-gateway/upstream/model-lister.d.ts.map +1 -0
  109. package/dist/ai-gateway/upstream/model-lister.js +61 -0
  110. package/dist/ai-gateway/upstream/model-lister.js.map +1 -0
  111. package/dist/ai-gateway/upstream/provider-client.d.ts +20 -0
  112. package/dist/ai-gateway/upstream/provider-client.d.ts.map +1 -0
  113. package/dist/ai-gateway/upstream/provider-client.js +76 -0
  114. package/dist/ai-gateway/upstream/provider-client.js.map +1 -0
  115. package/dist/ai-gateway/upstream/provider-client.test.d.ts +2 -0
  116. package/dist/ai-gateway/upstream/provider-client.test.d.ts.map +1 -0
  117. package/dist/ai-gateway/upstream/provider-client.test.js +69 -0
  118. package/dist/ai-gateway/upstream/provider-client.test.js.map +1 -0
  119. package/dist/cli/cli-main.d.ts +7 -0
  120. package/dist/cli/cli-main.d.ts.map +1 -0
  121. package/dist/cli/cli-main.js +116 -0
  122. package/dist/cli/cli-main.js.map +1 -0
  123. package/dist/cli/cli-router.d.ts +20 -0
  124. package/dist/cli/cli-router.d.ts.map +1 -0
  125. package/dist/cli/cli-router.js +271 -0
  126. package/dist/cli/cli-router.js.map +1 -0
  127. package/dist/cli/cli-utils.d.ts +30 -0
  128. package/dist/cli/cli-utils.d.ts.map +1 -0
  129. package/dist/cli/cli-utils.js +219 -0
  130. package/dist/cli/cli-utils.js.map +1 -0
  131. package/dist/cli/commands/audit-commands.d.ts +30 -0
  132. package/dist/cli/commands/audit-commands.d.ts.map +1 -0
  133. package/dist/cli/commands/audit-commands.js +417 -0
  134. package/dist/cli/commands/audit-commands.js.map +1 -0
  135. package/dist/cli/commands/autostart-command.d.ts +21 -0
  136. package/dist/cli/commands/autostart-command.d.ts.map +1 -0
  137. package/dist/cli/commands/autostart-command.js +243 -0
  138. package/dist/cli/commands/autostart-command.js.map +1 -0
  139. package/dist/cli/commands/autostart-command.test.d.ts +2 -0
  140. package/dist/cli/commands/autostart-command.test.d.ts.map +1 -0
  141. package/dist/cli/commands/autostart-command.test.js +35 -0
  142. package/dist/cli/commands/autostart-command.test.js.map +1 -0
  143. package/dist/cli/commands/config-commands.d.ts +30 -0
  144. package/dist/cli/commands/config-commands.d.ts.map +1 -0
  145. package/dist/cli/commands/config-commands.js +1013 -0
  146. package/dist/cli/commands/config-commands.js.map +1 -0
  147. package/dist/cli/commands/daemon-commands.d.ts +76 -0
  148. package/dist/cli/commands/daemon-commands.d.ts.map +1 -0
  149. package/dist/cli/commands/daemon-commands.js +451 -0
  150. package/dist/cli/commands/daemon-commands.js.map +1 -0
  151. package/dist/cli/commands/detect-commands.d.ts +17 -0
  152. package/dist/cli/commands/detect-commands.d.ts.map +1 -0
  153. package/dist/cli/commands/detect-commands.js +293 -0
  154. package/dist/cli/commands/detect-commands.js.map +1 -0
  155. package/dist/cli/commands/diagnose-commands.d.ts +19 -0
  156. package/dist/cli/commands/diagnose-commands.d.ts.map +1 -0
  157. package/dist/cli/commands/diagnose-commands.js +326 -0
  158. package/dist/cli/commands/diagnose-commands.js.map +1 -0
  159. package/dist/cli/commands/optimize-commands.d.ts +14 -0
  160. package/dist/cli/commands/optimize-commands.d.ts.map +1 -0
  161. package/dist/cli/commands/optimize-commands.js +189 -0
  162. package/dist/cli/commands/optimize-commands.js.map +1 -0
  163. package/dist/cli/commands/proxy-command.d.ts +27 -0
  164. package/dist/cli/commands/proxy-command.d.ts.map +1 -0
  165. package/dist/cli/commands/proxy-command.js +212 -0
  166. package/dist/cli/commands/proxy-command.js.map +1 -0
  167. package/dist/cli/commands/start-command.d.ts +22 -0
  168. package/dist/cli/commands/start-command.d.ts.map +1 -0
  169. package/dist/cli/commands/start-command.js +275 -0
  170. package/dist/cli/commands/start-command.js.map +1 -0
  171. package/dist/config/config-manager.d.ts +125 -0
  172. package/dist/config/config-manager.d.ts.map +1 -0
  173. package/dist/config/config-manager.js +569 -0
  174. package/dist/config/config-manager.js.map +1 -0
  175. package/dist/config/config-manager.test.d.ts +2 -0
  176. package/dist/config/config-manager.test.d.ts.map +1 -0
  177. package/dist/config/config-manager.test.js +209 -0
  178. package/dist/config/config-manager.test.js.map +1 -0
  179. package/dist/core/auto-config-generator.d.ts +55 -0
  180. package/dist/core/auto-config-generator.d.ts.map +1 -0
  181. package/dist/core/auto-config-generator.js +278 -0
  182. package/dist/core/auto-config-generator.js.map +1 -0
  183. package/dist/core/mcpdog-server.d.ts +49 -0
  184. package/dist/core/mcpdog-server.d.ts.map +1 -0
  185. package/dist/core/mcpdog-server.js +552 -0
  186. package/dist/core/mcpdog-server.js.map +1 -0
  187. package/dist/core/protocol-detector.d.ts +56 -0
  188. package/dist/core/protocol-detector.d.ts.map +1 -0
  189. package/dist/core/protocol-detector.js +365 -0
  190. package/dist/core/protocol-detector.js.map +1 -0
  191. package/dist/daemon/daemon-client.d.ts +34 -0
  192. package/dist/daemon/daemon-client.d.ts.map +1 -0
  193. package/dist/daemon/daemon-client.js +130 -0
  194. package/dist/daemon/daemon-client.js.map +1 -0
  195. package/dist/daemon/daemon-client.test.d.ts +2 -0
  196. package/dist/daemon/daemon-client.test.d.ts.map +1 -0
  197. package/dist/daemon/daemon-client.test.js +77 -0
  198. package/dist/daemon/daemon-client.test.js.map +1 -0
  199. package/dist/daemon/daemon-info.d.ts +7 -0
  200. package/dist/daemon/daemon-info.d.ts.map +1 -0
  201. package/dist/daemon/daemon-info.js +34 -0
  202. package/dist/daemon/daemon-info.js.map +1 -0
  203. package/dist/daemon/daemon-info.test.d.ts +2 -0
  204. package/dist/daemon/daemon-info.test.d.ts.map +1 -0
  205. package/dist/daemon/daemon-info.test.js +57 -0
  206. package/dist/daemon/daemon-info.test.js.map +1 -0
  207. package/dist/daemon/daemon-web-server.ai.test.d.ts +2 -0
  208. package/dist/daemon/daemon-web-server.ai.test.d.ts.map +1 -0
  209. package/dist/daemon/daemon-web-server.ai.test.js +119 -0
  210. package/dist/daemon/daemon-web-server.ai.test.js.map +1 -0
  211. package/dist/daemon/daemon-web-server.d.ts +59 -0
  212. package/dist/daemon/daemon-web-server.d.ts.map +1 -0
  213. package/dist/daemon/daemon-web-server.js +1236 -0
  214. package/dist/daemon/daemon-web-server.js.map +1 -0
  215. package/dist/daemon/daemon-web-server.test.d.ts +2 -0
  216. package/dist/daemon/daemon-web-server.test.d.ts.map +1 -0
  217. package/dist/daemon/daemon-web-server.test.js +93 -0
  218. package/dist/daemon/daemon-web-server.test.js.map +1 -0
  219. package/dist/daemon/mcpdog-daemon.d.ts +42 -0
  220. package/dist/daemon/mcpdog-daemon.d.ts.map +1 -0
  221. package/dist/daemon/mcpdog-daemon.js +302 -0
  222. package/dist/daemon/mcpdog-daemon.js.map +1 -0
  223. package/dist/daemon/stdio-proxy.d.ts +17 -0
  224. package/dist/daemon/stdio-proxy.d.ts.map +1 -0
  225. package/dist/daemon/stdio-proxy.js +143 -0
  226. package/dist/daemon/stdio-proxy.js.map +1 -0
  227. package/dist/index.d.ts +14 -0
  228. package/dist/index.d.ts.map +1 -0
  229. package/dist/index.js +198 -0
  230. package/dist/index.js.map +1 -0
  231. package/dist/intent-analyzer.d.ts +27 -0
  232. package/dist/intent-analyzer.d.ts.map +1 -0
  233. package/dist/intent-analyzer.js +140 -0
  234. package/dist/intent-analyzer.js.map +1 -0
  235. package/dist/logging/server-log-manager.d.ts +75 -0
  236. package/dist/logging/server-log-manager.d.ts.map +1 -0
  237. package/dist/logging/server-log-manager.js +185 -0
  238. package/dist/logging/server-log-manager.js.map +1 -0
  239. package/dist/mcp-registry.d.ts +35 -0
  240. package/dist/mcp-registry.d.ts.map +1 -0
  241. package/dist/mcp-registry.js +166 -0
  242. package/dist/mcp-registry.js.map +1 -0
  243. package/dist/middleware/auth.d.ts +5 -0
  244. package/dist/middleware/auth.d.ts.map +1 -0
  245. package/dist/middleware/auth.js +81 -0
  246. package/dist/middleware/auth.js.map +1 -0
  247. package/dist/mock-tool-database.d.ts +41 -0
  248. package/dist/mock-tool-database.d.ts.map +1 -0
  249. package/dist/mock-tool-database.js +461 -0
  250. package/dist/mock-tool-database.js.map +1 -0
  251. package/dist/proxy-server-manager.d.ts +11 -0
  252. package/dist/proxy-server-manager.d.ts.map +1 -0
  253. package/dist/proxy-server-manager.js +144 -0
  254. package/dist/proxy-server-manager.js.map +1 -0
  255. package/dist/router/tool-router.d.ts +61 -0
  256. package/dist/router/tool-router.d.ts.map +1 -0
  257. package/dist/router/tool-router.js +539 -0
  258. package/dist/router/tool-router.js.map +1 -0
  259. package/dist/router/tool-router.test.d.ts +2 -0
  260. package/dist/router/tool-router.test.d.ts.map +1 -0
  261. package/dist/router/tool-router.test.js +113 -0
  262. package/dist/router/tool-router.test.js.map +1 -0
  263. package/dist/streamable-http-server.d.ts +27 -0
  264. package/dist/streamable-http-server.d.ts.map +1 -0
  265. package/dist/streamable-http-server.js +302 -0
  266. package/dist/streamable-http-server.js.map +1 -0
  267. package/dist/tool-finder.d.ts +21 -0
  268. package/dist/tool-finder.d.ts.map +1 -0
  269. package/dist/tool-finder.js +86 -0
  270. package/dist/tool-finder.js.map +1 -0
  271. package/dist/tool-recommender.d.ts +23 -0
  272. package/dist/tool-recommender.d.ts.map +1 -0
  273. package/dist/tool-recommender.js +200 -0
  274. package/dist/tool-recommender.js.map +1 -0
  275. package/dist/types/index.d.ts +145 -0
  276. package/dist/types/index.d.ts.map +1 -0
  277. package/dist/types/index.js +2 -0
  278. package/dist/types/index.js.map +1 -0
  279. package/dist/utils/server-name-validator.d.ts +47 -0
  280. package/dist/utils/server-name-validator.d.ts.map +1 -0
  281. package/dist/utils/server-name-validator.js +156 -0
  282. package/dist/utils/server-name-validator.js.map +1 -0
  283. package/dist/web/web-server.d.ts +44 -0
  284. package/dist/web/web-server.d.ts.map +1 -0
  285. package/dist/web/web-server.js +500 -0
  286. package/dist/web/web-server.js.map +1 -0
  287. package/package.json +68 -0
  288. package/web/dist/assets/index-DMVhVtNU.css +1 -0
  289. package/web/dist/assets/index-VMXIgcOH.js +92 -0
  290. package/web/dist/assets/index-VMXIgcOH.js.map +1 -0
  291. 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