mcp-log-query-server 1.0.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/README.md +151 -0
- package/config.js +474 -0
- package/index.js +555 -0
- package/package.json +42 -0
- package/server-sse.js +203 -0
- package/ssh-client.js +326 -0
package/server-sse.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log Query MCP Server - SSE/HTTP 模式
|
|
3
|
+
*
|
|
4
|
+
* 支持通过 HTTP 和 Server-Sent Events 与 MCP 客户端通信
|
|
5
|
+
* 适用于 Docker 部署
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
10
|
+
import express from 'express';
|
|
11
|
+
import cors from 'cors';
|
|
12
|
+
import { findService, getAllServices, DEFAULTS } from './config.js';
|
|
13
|
+
import { queryLog, testConnection } from './ssh-client.js';
|
|
14
|
+
|
|
15
|
+
const app = express();
|
|
16
|
+
const PORT = process.env.PORT || 3100;
|
|
17
|
+
|
|
18
|
+
// 启用 CORS
|
|
19
|
+
app.use(cors());
|
|
20
|
+
app.use(express.json());
|
|
21
|
+
|
|
22
|
+
// 存储活跃的 SSE 连接
|
|
23
|
+
const transports = {};
|
|
24
|
+
|
|
25
|
+
// 健康检查端点
|
|
26
|
+
app.get('/health', (req, res) => {
|
|
27
|
+
res.json({ status: 'ok', service: 'mcp-log-query' });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// SSE 端点 - 客户端连接
|
|
31
|
+
app.get('/sse', async (req, res) => {
|
|
32
|
+
console.log('[SSE] 新客户端连接');
|
|
33
|
+
|
|
34
|
+
// 设置 SSE 响应头
|
|
35
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
36
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
37
|
+
res.setHeader('Connection', 'keep-alive');
|
|
38
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
39
|
+
res.flushHeaders();
|
|
40
|
+
|
|
41
|
+
// 生成 sessionId
|
|
42
|
+
const sessionId = Date.now().toString();
|
|
43
|
+
|
|
44
|
+
// 创建 SSE transport,传入 sessionId
|
|
45
|
+
const transport = new SSEServerTransport(`/message?sessionId=${sessionId}`, res);
|
|
46
|
+
transports[sessionId] = transport;
|
|
47
|
+
|
|
48
|
+
// 创建 MCP Server
|
|
49
|
+
const server = new McpServer({
|
|
50
|
+
name: 'mcp-log-query',
|
|
51
|
+
version: '1.0.0'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// 注册工具
|
|
55
|
+
registerTools(server);
|
|
56
|
+
|
|
57
|
+
// 连接 transport
|
|
58
|
+
try {
|
|
59
|
+
await server.connect(transport);
|
|
60
|
+
console.log(`[SSE] 客户端已连接, sessionId: ${sessionId}`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error('[SSE] 连接错误:', err);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 保持连接活跃 - 每 30 秒发送心跳
|
|
66
|
+
const heartbeat = setInterval(() => {
|
|
67
|
+
if (!res.writableEnded) {
|
|
68
|
+
res.write(': heartbeat\n\n');
|
|
69
|
+
}
|
|
70
|
+
}, 30000);
|
|
71
|
+
|
|
72
|
+
// 清理断开的连接
|
|
73
|
+
res.on('close', () => {
|
|
74
|
+
console.log(`[SSE] 客户端断开, sessionId: ${sessionId}`);
|
|
75
|
+
clearInterval(heartbeat);
|
|
76
|
+
delete transports[sessionId];
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// 消息端点 - 接收客户端请求
|
|
81
|
+
app.post('/message', async (req, res) => {
|
|
82
|
+
const sessionId = req.query.sessionId;
|
|
83
|
+
const transport = transports[sessionId];
|
|
84
|
+
|
|
85
|
+
if (!transport) {
|
|
86
|
+
res.status(404).json({ error: 'Session not found' });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await transport.handlePostMessage(req, res);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 注册 MCP 工具
|
|
95
|
+
*/
|
|
96
|
+
function registerTools(server) {
|
|
97
|
+
// query_log 工具
|
|
98
|
+
server.tool('query_log', {
|
|
99
|
+
description: '查询服务容器的日志文件。返回最近的日志内容。',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
service: {
|
|
104
|
+
type: 'string',
|
|
105
|
+
description: '服务名称,如 clife-senior-health、clife-senior-archive,或别名如 health、archive'
|
|
106
|
+
},
|
|
107
|
+
lines: {
|
|
108
|
+
type: 'number',
|
|
109
|
+
description: '返回的日志行数,默认 100',
|
|
110
|
+
default: 100
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
required: ['service']
|
|
114
|
+
}
|
|
115
|
+
}, async (args) => {
|
|
116
|
+
const service = findService(args.service);
|
|
117
|
+
if (!service) {
|
|
118
|
+
return { content: [{ type: 'text', text: `错误: 未找到服务 "${args.service}"` }] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const lines = args.lines || DEFAULTS.lines;
|
|
122
|
+
const command = `tail -${lines}`;
|
|
123
|
+
|
|
124
|
+
console.log(`[MCP] 查询日志: ${service.name}`);
|
|
125
|
+
const result = await queryLog(service, command);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
content: [{
|
|
129
|
+
type: 'text',
|
|
130
|
+
text: `## ${service.name} 日志 (最近 ${lines} 行)\n\n\`\`\`\n${result}\n\`\`\``
|
|
131
|
+
}]
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// search_log 工具
|
|
136
|
+
server.tool('search_log', {
|
|
137
|
+
description: '在服务日志中搜索关键词',
|
|
138
|
+
inputSchema: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
properties: {
|
|
141
|
+
service: { type: 'string', description: '服务名称或别名' },
|
|
142
|
+
keyword: { type: 'string', description: '搜索关键词' },
|
|
143
|
+
context_lines: { type: 'number', description: '上下文行数', default: 5 },
|
|
144
|
+
case_sensitive: { type: 'boolean', description: '区分大小写', default: false }
|
|
145
|
+
},
|
|
146
|
+
required: ['service', 'keyword']
|
|
147
|
+
}
|
|
148
|
+
}, async (args) => {
|
|
149
|
+
const service = findService(args.service);
|
|
150
|
+
if (!service) {
|
|
151
|
+
return { content: [{ type: 'text', text: `错误: 未找到服务 "${args.service}"` }] };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const grepFlags = args.case_sensitive ? '' : '-i';
|
|
155
|
+
const contextLines = args.context_lines || 5;
|
|
156
|
+
const command = `grep ${grepFlags} -C ${contextLines} "${args.keyword}"`;
|
|
157
|
+
|
|
158
|
+
console.log(`[MCP] 搜索日志: ${service.name}, 关键词: ${args.keyword}`);
|
|
159
|
+
const result = await queryLog(service, command);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
content: [{
|
|
163
|
+
type: 'text',
|
|
164
|
+
text: `## ${service.name} 日志搜索结果\n\n**关键词**: ${args.keyword}\n\n\`\`\`\n${result || '未找到匹配内容'}\n\`\`\``
|
|
165
|
+
}]
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// list_services 工具
|
|
170
|
+
server.tool('list_services', {
|
|
171
|
+
description: '列出所有可查询日志的服务',
|
|
172
|
+
inputSchema: { type: 'object', properties: {} }
|
|
173
|
+
}, async () => {
|
|
174
|
+
const services = getAllServices();
|
|
175
|
+
const text = services.map(s =>
|
|
176
|
+
`- **${s.name}**: ${s.description}\n 别名: ${s.aliases.join(', ')}`
|
|
177
|
+
).join('\n');
|
|
178
|
+
|
|
179
|
+
return { content: [{ type: 'text', text: `## 可用服务\n\n${text}` }] };
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// test_connection 工具
|
|
183
|
+
server.tool('test_connection', {
|
|
184
|
+
description: '测试 SSH 连接',
|
|
185
|
+
inputSchema: { type: 'object', properties: {} }
|
|
186
|
+
}, async () => {
|
|
187
|
+
const result = await testConnection();
|
|
188
|
+
return {
|
|
189
|
+
content: [{
|
|
190
|
+
type: 'text',
|
|
191
|
+
text: result.success ? '✅ SSH 连接正常' : `❌ 连接失败: ${result.error}`
|
|
192
|
+
}]
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 启动服务器
|
|
198
|
+
app.listen(PORT, () => {
|
|
199
|
+
console.log(`[MCP] Log Query Server (SSE) 已启动,端口: ${PORT}`);
|
|
200
|
+
console.log(`[MCP] SSE 端点: http://localhost:${PORT}/sse`);
|
|
201
|
+
console.log(`[MCP] 健康检查: http://localhost:${PORT}/health`);
|
|
202
|
+
});
|
|
203
|
+
|
package/ssh-client.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH Client - 通过 JumpServer 堡垒机连接 K8s 服务器并执行命令
|
|
3
|
+
*
|
|
4
|
+
* JumpServer 交互流程:
|
|
5
|
+
* 1. SSH 连接堡垒机,等待 Opt> 提示符
|
|
6
|
+
* 2. 输入目标服务器 IP,等待 [Host]> 提示符
|
|
7
|
+
* 3. 输入服务器 ID (如 1),等待进入服务器 shell
|
|
8
|
+
* 4. 执行 kubectl 命令查询日志
|
|
9
|
+
* 5. 退出
|
|
10
|
+
*
|
|
11
|
+
* 注意:
|
|
12
|
+
* - 堡垒机只有 30 秒等待时间,必须快速响应!
|
|
13
|
+
* - 使用 \r 而不是 \n 发送命令
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Client } from 'ssh2';
|
|
17
|
+
import { JUMP_HOST, K8S_SERVER, DEFAULTS } from './config.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 执行日志查询
|
|
21
|
+
* @param {Object} service - 服务配置
|
|
22
|
+
* @param {string} command - 日志查询命令(如 tail -100 *.log)
|
|
23
|
+
* @param {Object} options - 选项
|
|
24
|
+
* @returns {Promise<string>} 日志内容
|
|
25
|
+
*/
|
|
26
|
+
export async function queryLog(service, command, options = {}) {
|
|
27
|
+
const timeout = options.timeout || DEFAULTS.timeout;
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const conn = new Client();
|
|
31
|
+
let buffer = ''; // 累积所有输出
|
|
32
|
+
let timeoutId;
|
|
33
|
+
let stage = 'init'; // init -> opt -> host -> server -> kubectl -> done
|
|
34
|
+
let kubectlOutput = '';
|
|
35
|
+
let collectingOutput = false;
|
|
36
|
+
|
|
37
|
+
// 设置超时
|
|
38
|
+
timeoutId = setTimeout(() => {
|
|
39
|
+
conn.end();
|
|
40
|
+
reject(new Error(`命令执行超时 (${timeout}ms)`));
|
|
41
|
+
}, timeout);
|
|
42
|
+
|
|
43
|
+
conn.on('ready', () => {
|
|
44
|
+
console.error('[SSH] 已连接到堡垒机');
|
|
45
|
+
|
|
46
|
+
conn.shell({ term: 'xterm', rows: 24, cols: 500 }, (err, stream) => {
|
|
47
|
+
if (err) {
|
|
48
|
+
clearTimeout(timeoutId);
|
|
49
|
+
reject(err);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
stream.on('close', () => {
|
|
54
|
+
clearTimeout(timeoutId);
|
|
55
|
+
conn.end();
|
|
56
|
+
|
|
57
|
+
// 清理输出
|
|
58
|
+
const cleanOutput = cleanTerminalOutput(kubectlOutput || buffer);
|
|
59
|
+
resolve(cleanOutput);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
stream.on('data', (data) => {
|
|
63
|
+
const text = data.toString();
|
|
64
|
+
buffer += text;
|
|
65
|
+
|
|
66
|
+
if (collectingOutput) {
|
|
67
|
+
kubectlOutput += text;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// JumpServer 状态机 - 检查累积的 buffer
|
|
71
|
+
|
|
72
|
+
// 阶段1: 等待 Opt> 提示符
|
|
73
|
+
if (stage === 'init' && buffer.includes('Opt>')) {
|
|
74
|
+
stage = 'opt';
|
|
75
|
+
console.error('[SSH] 输入目标服务器 IP');
|
|
76
|
+
stream.write(K8S_SERVER.host + '\r');
|
|
77
|
+
}
|
|
78
|
+
// 阶段2: 等待 [Host]> 提示符(搜索结果列表后)
|
|
79
|
+
else if (stage === 'opt' && buffer.includes('[Host]>')) {
|
|
80
|
+
stage = 'host';
|
|
81
|
+
console.error('[SSH] 选择服务器 ID: ' + K8S_SERVER.selectOption);
|
|
82
|
+
stream.write(K8S_SERVER.selectOption + '\r');
|
|
83
|
+
}
|
|
84
|
+
// 阶段3: 等待进入服务器 shell(检测 ~]$ 或 ~]# 提示符)
|
|
85
|
+
else if (stage === 'host' && (buffer.includes('~]$') || buffer.includes('~]#'))) {
|
|
86
|
+
stage = 'server';
|
|
87
|
+
console.error('[SSH] 已进入服务器,执行 kubectl 命令');
|
|
88
|
+
|
|
89
|
+
// 构建并执行 kubectl 命令
|
|
90
|
+
const kubectlCmd = buildKubectlCommand(service, command);
|
|
91
|
+
console.error(`[SSH] 命令: ${kubectlCmd.substring(0, 80)}...`);
|
|
92
|
+
|
|
93
|
+
// 重置 buffer,开始收集 kubectl 输出
|
|
94
|
+
kubectlOutput = '';
|
|
95
|
+
collectingOutput = true;
|
|
96
|
+
|
|
97
|
+
stream.write(kubectlCmd + '\r');
|
|
98
|
+
stage = 'kubectl';
|
|
99
|
+
}
|
|
100
|
+
// 阶段4: kubectl 执行完成
|
|
101
|
+
else if (stage === 'kubectl' && collectingOutput && kubectlOutput.length > 50) {
|
|
102
|
+
// 检测命令执行完成(返回到 shell 提示符)
|
|
103
|
+
if (kubectlOutput.includes('~]$') || kubectlOutput.includes('~]#')) {
|
|
104
|
+
stage = 'done';
|
|
105
|
+
collectingOutput = false;
|
|
106
|
+
console.error('[SSH] 命令执行完成,退出');
|
|
107
|
+
|
|
108
|
+
// 立即退出
|
|
109
|
+
stream.write('exit\r');
|
|
110
|
+
stream.write('exit\r');
|
|
111
|
+
setTimeout(() => stream.end(), 500);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
stream.stderr.on('data', (data) => {
|
|
117
|
+
console.error('[SSH stderr]', data.toString());
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
conn.on('error', (err) => {
|
|
123
|
+
clearTimeout(timeoutId);
|
|
124
|
+
reject(new Error(`SSH 连接错误: ${err.message}`));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 连接堡垒机
|
|
128
|
+
conn.connect({
|
|
129
|
+
host: JUMP_HOST.host,
|
|
130
|
+
port: JUMP_HOST.port,
|
|
131
|
+
username: JUMP_HOST.username,
|
|
132
|
+
password: JUMP_HOST.password,
|
|
133
|
+
readyTimeout: DEFAULTS.connectTimeout
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 构建 kubectl exec 命令
|
|
140
|
+
* @param {Object} service - 服务配置
|
|
141
|
+
* @param {string} logCommand - 日志命令,如 "tail -100" 或 "grep error"
|
|
142
|
+
*/
|
|
143
|
+
function buildKubectlCommand(service, logCommand) {
|
|
144
|
+
const { namespace, podPattern, logPath, logFile } = service;
|
|
145
|
+
const file = logFile || 'normal.log';
|
|
146
|
+
const fullPath = `${logPath}/${file}`;
|
|
147
|
+
|
|
148
|
+
// 构建完整命令
|
|
149
|
+
// 例如: kubectl exec pod/xxx -n namespace -- tail -100 /path/to/normal.log
|
|
150
|
+
return `kubectl exec $(kubectl get pod -n ${namespace} -o name | grep ${podPattern} | head -1) -n ${namespace} -- ${logCommand} ${fullPath}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 清理终端输出中的控制字符
|
|
155
|
+
*/
|
|
156
|
+
function cleanTerminalOutput(output) {
|
|
157
|
+
return output
|
|
158
|
+
// 移除 ANSI 转义序列
|
|
159
|
+
.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '')
|
|
160
|
+
// 移除其他控制字符
|
|
161
|
+
.replace(/\x1B\][^\x07]*\x07/g, '')
|
|
162
|
+
// 移除回车符
|
|
163
|
+
.replace(/\r/g, '')
|
|
164
|
+
// 移除堡垒机提示信息(根据实际情况调整)
|
|
165
|
+
.split('\n')
|
|
166
|
+
.filter(line => {
|
|
167
|
+
// 过滤掉提示行和空行
|
|
168
|
+
const trimmed = line.trim();
|
|
169
|
+
if (!trimmed) return false;
|
|
170
|
+
if (trimmed.startsWith('Last login:')) return false;
|
|
171
|
+
if (trimmed.includes('Welcome')) return false;
|
|
172
|
+
if (trimmed.match(/^\[.*@.*\][$#]/)) return false;
|
|
173
|
+
return true;
|
|
174
|
+
})
|
|
175
|
+
.join('\n')
|
|
176
|
+
.trim();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 测试 SSH 连接
|
|
181
|
+
*/
|
|
182
|
+
export async function testConnection() {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const conn = new Client();
|
|
185
|
+
|
|
186
|
+
const timeoutId = setTimeout(() => {
|
|
187
|
+
conn.end();
|
|
188
|
+
reject(new Error('连接超时'));
|
|
189
|
+
}, DEFAULTS.connectTimeout);
|
|
190
|
+
|
|
191
|
+
conn.on('ready', () => {
|
|
192
|
+
clearTimeout(timeoutId);
|
|
193
|
+
conn.end();
|
|
194
|
+
resolve({ success: true, message: '堡垒机连接成功' });
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
conn.on('error', (err) => {
|
|
198
|
+
clearTimeout(timeoutId);
|
|
199
|
+
reject(new Error(`连接失败: ${err.message}`));
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
conn.connect({
|
|
203
|
+
host: JUMP_HOST.host,
|
|
204
|
+
port: JUMP_HOST.port,
|
|
205
|
+
username: JUMP_HOST.username,
|
|
206
|
+
password: JUMP_HOST.password,
|
|
207
|
+
readyTimeout: DEFAULTS.connectTimeout
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 执行通用 kubectl 命令
|
|
215
|
+
* @param {string} kubectlCommand - 完整的 kubectl 命令
|
|
216
|
+
* @param {Object} options - 选项
|
|
217
|
+
* @returns {Promise<string>} 命令输出
|
|
218
|
+
*/
|
|
219
|
+
export async function executeKubectl(kubectlCommand, options = {}) {
|
|
220
|
+
const timeout = options.timeout || DEFAULTS.timeout;
|
|
221
|
+
|
|
222
|
+
return new Promise((resolve, reject) => {
|
|
223
|
+
const conn = new Client();
|
|
224
|
+
let buffer = '';
|
|
225
|
+
let timeoutId;
|
|
226
|
+
let stage = 'init';
|
|
227
|
+
let kubectlOutput = '';
|
|
228
|
+
let collectingOutput = false;
|
|
229
|
+
|
|
230
|
+
// 设置超时
|
|
231
|
+
timeoutId = setTimeout(() => {
|
|
232
|
+
conn.end();
|
|
233
|
+
reject(new Error(`命令执行超时 (${timeout}ms)`));
|
|
234
|
+
}, timeout);
|
|
235
|
+
|
|
236
|
+
conn.on('ready', () => {
|
|
237
|
+
console.error('[SSH] 已连接到堡垒机');
|
|
238
|
+
|
|
239
|
+
conn.shell({ term: 'xterm', rows: 24, cols: 500 }, (err, stream) => {
|
|
240
|
+
if (err) {
|
|
241
|
+
clearTimeout(timeoutId);
|
|
242
|
+
reject(err);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
stream.on('close', () => {
|
|
247
|
+
clearTimeout(timeoutId);
|
|
248
|
+
conn.end();
|
|
249
|
+
|
|
250
|
+
// 清理输出
|
|
251
|
+
const cleanOutput = cleanTerminalOutput(kubectlOutput || buffer);
|
|
252
|
+
resolve(cleanOutput);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
stream.on('data', (data) => {
|
|
256
|
+
const text = data.toString();
|
|
257
|
+
buffer += text;
|
|
258
|
+
|
|
259
|
+
if (collectingOutput) {
|
|
260
|
+
kubectlOutput += text;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// JumpServer 状态机
|
|
264
|
+
|
|
265
|
+
// 阶段1: 等待 Opt> 提示符
|
|
266
|
+
if (stage === 'init' && buffer.includes('Opt>')) {
|
|
267
|
+
stage = 'opt';
|
|
268
|
+
console.error('[SSH] 输入目标服务器 IP');
|
|
269
|
+
stream.write(K8S_SERVER.host + '\r');
|
|
270
|
+
}
|
|
271
|
+
// 阶段2: 等待 [Host]> 提示符
|
|
272
|
+
else if (stage === 'opt' && buffer.includes('[Host]>')) {
|
|
273
|
+
stage = 'host';
|
|
274
|
+
console.error('[SSH] 选择服务器 ID: ' + K8S_SERVER.selectOption);
|
|
275
|
+
stream.write(K8S_SERVER.selectOption + '\r');
|
|
276
|
+
}
|
|
277
|
+
// 阶段3: 等待进入服务器 shell
|
|
278
|
+
else if (stage === 'host' && (buffer.includes('~]$') || buffer.includes('~]#'))) {
|
|
279
|
+
stage = 'server';
|
|
280
|
+
console.error('[SSH] 已进入服务器,执行 kubectl 命令');
|
|
281
|
+
console.error(`[SSH] 命令: ${kubectlCommand.substring(0, 100)}...`);
|
|
282
|
+
|
|
283
|
+
// 重置 buffer,开始收集 kubectl 输出
|
|
284
|
+
kubectlOutput = '';
|
|
285
|
+
collectingOutput = true;
|
|
286
|
+
|
|
287
|
+
stream.write(kubectlCommand + '\r');
|
|
288
|
+
stage = 'kubectl';
|
|
289
|
+
}
|
|
290
|
+
// 阶段4: kubectl 执行完成
|
|
291
|
+
else if (stage === 'kubectl' && collectingOutput && kubectlOutput.length > 50) {
|
|
292
|
+
// 检测命令执行完成(返回到 shell 提示符)
|
|
293
|
+
if (kubectlOutput.includes('~]$') || kubectlOutput.includes('~]#')) {
|
|
294
|
+
stage = 'done';
|
|
295
|
+
collectingOutput = false;
|
|
296
|
+
console.error('[SSH] 命令执行完成,退出');
|
|
297
|
+
|
|
298
|
+
// 立即退出
|
|
299
|
+
stream.write('exit\r');
|
|
300
|
+
stream.write('exit\r');
|
|
301
|
+
setTimeout(() => stream.end(), 500);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
stream.stderr.on('data', (data) => {
|
|
307
|
+
console.error('[SSH stderr]', data.toString());
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
conn.on('error', (err) => {
|
|
313
|
+
clearTimeout(timeoutId);
|
|
314
|
+
reject(new Error(`SSH 连接错误: ${err.message}`));
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// 连接堡垒机
|
|
318
|
+
conn.connect({
|
|
319
|
+
host: JUMP_HOST.host,
|
|
320
|
+
port: JUMP_HOST.port,
|
|
321
|
+
username: JUMP_HOST.username,
|
|
322
|
+
password: JUMP_HOST.password,
|
|
323
|
+
readyTimeout: DEFAULTS.connectTimeout
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|