zen-gitsync 2.14.2 → 2.14.3
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/package.json +1 -1
- package/src/ui/public/assets/{AppVersionBadge-M9ydXpd9.js → AppVersionBadge-z2kqhmtD.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-scIk6OeM.js → BranchSelector-CSjsCeeJ.js} +1 -1
- package/src/ui/public/assets/{CommandConsole-BikQuarw.js → CommandConsole-96MUoG1F.js} +2 -2
- package/src/ui/public/assets/{CommitForm-DuvNxUdP.js → CommitForm-BBKQ0GqO.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-G17HYMln.js → CommonDialog-CgQovpCJ.js} +1 -1
- package/src/ui/public/assets/{EditorView-rGLnccH3.js → EditorView-aoA30emq.js} +0 -0
- package/src/ui/public/assets/{FlowExecutionViewer-B4BADP8e.js → FlowExecutionViewer-BvXlaQai.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-C3l3Jco7.js → FlowOrchestrationWorkspace-CgWUF1Jc.js} +1 -1
- package/src/ui/public/assets/{LogList-7agk5Obq.js → LogList-uF4tir23.js} +1 -1
- package/src/ui/public/assets/{MindmapView-mOFG9TmR.js → MindmapView-B8wr0EFL.js} +1 -1
- package/src/ui/public/assets/{MonitorView-Dlpgy8Bb.js → MonitorView-DaJ0h8Im.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton-CgS14qyg.js → ProjectStartupButton-CcH1NL2W.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-CFKl6CB9.js → RemoteRepoCard-BDbJ76uf.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-Dsc_a3t7.js → SourceMapView-2HjAlHnp.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-Cw2mL_zx.js → SvgIcon-CK6VGHb5.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-yBT2005h.js → UserInputNode-Cx8r5geA.js} +1 -1
- package/src/ui/public/assets/{WorkbenchView-CiG-qMl7.js → WorkbenchView-CG54iND-.js} +4 -4
- package/src/ui/public/assets/WorkbenchView-CvHPPs8Q.css +1 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-BMXWW0fG.js → _plugin-vue_export-helper-CJMndG_W.js} +2 -2
- package/src/ui/public/assets/{configStore-Cq6Qwn35.js → configStore-DDnE5eie.js} +1 -1
- package/src/ui/public/assets/index-B7FGNx_l.js +23 -0
- package/src/ui/public/assets/{index-gbRjS0sL.css → index-f6UhuE5j.css} +1 -1
- package/src/ui/public/index.html +6 -6
- package/src/ui/server/index.js +632 -625
- package/src/ui/server/middleware/errorHandler.js +84 -0
- package/src/ui/server/middleware/errorHandler.test.js +114 -0
- package/src/ui/server/routes/exec.js +0 -21
- package/src/ui/server/routes/gitOps.js +62 -0
- package/src/ui/server/routes/monitor.js +42 -26
- package/src/ui/server/routes/terminal.js +318 -320
- package/src/ui/server/routes/workbench/attachmentUtils.js +76 -0
- package/src/ui/server/routes/workbench/index.js +1680 -0
- package/src/ui/server/routes/workbench/instructionStore.js +175 -0
- package/src/ui/server/routes/workbench/jobStore.js +225 -0
- package/src/ui/server/routes/workbench/jsonParse.js +219 -0
- package/src/ui/server/routes/workbench/llmClient.js +214 -0
- package/src/ui/server/routes/workbench/projectScan.js +191 -0
- package/src/ui/server/routes/workbench/sessionStore.js +126 -0
- package/src/ui/server/routes/workbench/shared.js +115 -0
- package/src/ui/server/routes/workbench/taskRunner.js +493 -0
- package/src/ui/server/routes/workbench.js +16 -3523
- package/src/ui/public/assets/WorkbenchView-OmJ1eM5g.css +0 -1
- package/src/ui/public/assets/index-CPqKvZBK.js +0 -23
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// Express 全局错误处理中间件。
|
|
16
|
+
//
|
|
17
|
+
// 触发条件(按发生频率排序):
|
|
18
|
+
// 1) 路由用 asyncRoute 包装后 throw 出来的、未被捕获的异常
|
|
19
|
+
// —— asyncRoute 自己已经 res.json 返回了,不会到这里
|
|
20
|
+
// 2) 路由没用 asyncRoute(仍写 try/catch),但忘了 res 的场景
|
|
21
|
+
// 3) handler 内部 next(err) 主动透传
|
|
22
|
+
// 4) express.json() / 静态资源 fallback / socket.io 之外的中间件抛错
|
|
23
|
+
// 5) headersSent 之后 next(err) 走 express 默认兜底(关闭连接)
|
|
24
|
+
//
|
|
25
|
+
// 设计要点:
|
|
26
|
+
// - 必须是 4-arity (err, req, res, next) 才会被 express 识别为错误中间件
|
|
27
|
+
// - 必须注册在所有 register*Routes 之后、express.static 之后(兜底最后)
|
|
28
|
+
// - 兼容 HttpError(来自 asyncRoute.js)的 statusCode 字段
|
|
29
|
+
// - 兼容 express.json() 抛的 SyntaxError(400 Bad Request)
|
|
30
|
+
// - headersSent 时只能 next(error) 让 express 关连接,不能再 res.write
|
|
31
|
+
//
|
|
32
|
+
// 用法:
|
|
33
|
+
// app.use(createErrorHandler())
|
|
34
|
+
|
|
35
|
+
import logger from '../utils/logger.js'
|
|
36
|
+
import { HttpError } from '../utils/asyncRoute.js'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 全局错误处理中间件工厂。
|
|
40
|
+
* @returns {import('express').ErrorRequestHandler}
|
|
41
|
+
*/
|
|
42
|
+
export function createErrorHandler() {
|
|
43
|
+
return (err, req, res, next) => {
|
|
44
|
+
// 已经回包了就只能让 express 关连接,再 res.status 会抛 ERR_HTTP_HEADERS_SENT
|
|
45
|
+
if (res.headersSent) {
|
|
46
|
+
return next(err)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 状态码优先级:HttpError.statusCode > err.status(express 惯例)> err.statusCode > 500
|
|
50
|
+
let statusCode = 500
|
|
51
|
+
if (err instanceof HttpError && Number.isInteger(err.statusCode)) {
|
|
52
|
+
statusCode = err.statusCode
|
|
53
|
+
} else if (err?.status && Number.isInteger(err.status)) {
|
|
54
|
+
statusCode = err.status
|
|
55
|
+
} else if (err?.statusCode && Number.isInteger(err.statusCode)) {
|
|
56
|
+
statusCode = err.statusCode
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// express.json() 解析失败抛 SyntaxError → 400,不要让用户看到 500
|
|
60
|
+
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
|
|
61
|
+
statusCode = 400
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 4xx 一般是用户输入问题,info 级即可;5xx 是真异常,error 级
|
|
65
|
+
const logLevel = statusCode >= 500 ? 'error' : 'info'
|
|
66
|
+
// 与 asyncRoute 一致:err 无 message(throw null/数字/字符串无 .message)时走兜底文案,
|
|
67
|
+
// 避免 String(null) = 'null' 这种无意义错误信息泄露到响应里
|
|
68
|
+
const rawMsg = typeof err?.message === 'string' && err.message.length > 0 ? err.message : ''
|
|
69
|
+
const redactedMsg = rawMsg || 'Internal Server Error'
|
|
70
|
+
const logLine = `[${req.method} ${req.path}] ${statusCode} ${redactedMsg}`
|
|
71
|
+
if (logLevel === 'error') {
|
|
72
|
+
logger.error(logLine)
|
|
73
|
+
if (err?.stack) logger.error(err.stack)
|
|
74
|
+
} else {
|
|
75
|
+
logger.info(logLine)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 错误响应体格式与 asyncRoute 保持一致
|
|
79
|
+
res.status(statusCode).json({
|
|
80
|
+
success: false,
|
|
81
|
+
error: redactedMsg || 'Internal Server Error'
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// src/ui/server/middleware/errorHandler.js 单元测试。
|
|
16
|
+
//
|
|
17
|
+
// 核心契约:
|
|
18
|
+
// - HttpError → 用其 statusCode
|
|
19
|
+
// - 普通 Error → 500
|
|
20
|
+
// - SyntaxError (express.json 解析失败) → 400
|
|
21
|
+
// - res.headersSent → 不再 res.status,转 next(err)
|
|
22
|
+
// - err.status / err.statusCode 也被识别
|
|
23
|
+
import { test } from 'node:test'
|
|
24
|
+
import assert from 'node:assert/strict'
|
|
25
|
+
import { createErrorHandler } from './errorHandler.js'
|
|
26
|
+
import { HttpError } from '../utils/asyncRoute.js'
|
|
27
|
+
|
|
28
|
+
function makeMockRes(headersSent = false) {
|
|
29
|
+
return {
|
|
30
|
+
statusCode: 200,
|
|
31
|
+
headersSent,
|
|
32
|
+
_json: null,
|
|
33
|
+
status(code) { this.statusCode = code; return this },
|
|
34
|
+
json(p) { this._json = p; this.headersSent = true; return this }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeMockReq(method = 'GET', path = '/api/foo') {
|
|
39
|
+
return { method, path }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('errorHandler: HttpError(404) → 404 + message', () => {
|
|
43
|
+
const handler = createErrorHandler()
|
|
44
|
+
const err = new HttpError(404, '资源不存在')
|
|
45
|
+
const req = makeMockReq()
|
|
46
|
+
const res = makeMockRes()
|
|
47
|
+
handler(err, req, res, () => {})
|
|
48
|
+
assert.equal(res.statusCode, 404)
|
|
49
|
+
assert.deepEqual(res._json, { success: false, error: '资源不存在' })
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('errorHandler: 普通 Error → 500', () => {
|
|
53
|
+
const handler = createErrorHandler()
|
|
54
|
+
const err = new Error('内部炸了')
|
|
55
|
+
const req = makeMockReq()
|
|
56
|
+
const res = makeMockRes()
|
|
57
|
+
handler(err, req, res, () => {})
|
|
58
|
+
assert.equal(res.statusCode, 500)
|
|
59
|
+
assert.equal(res._json.success, false)
|
|
60
|
+
assert.equal(res._json.error, '内部炸了')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('errorHandler: err.status 优先级仅次于 HttpError', () => {
|
|
64
|
+
// express 惯例:某些中间件(如 express-jwt)会给 err 设 status 而非 statusCode
|
|
65
|
+
const handler = createErrorHandler()
|
|
66
|
+
const err = Object.assign(new Error('未授权'), { status: 401 })
|
|
67
|
+
const req = makeMockReq()
|
|
68
|
+
const res = makeMockRes()
|
|
69
|
+
handler(err, req, res, () => {})
|
|
70
|
+
assert.equal(res.statusCode, 401)
|
|
71
|
+
assert.equal(res._json.error, '未授权')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('errorHandler: SyntaxError 带 status=400 → 400 (express.json 解析失败)', () => {
|
|
75
|
+
const handler = createErrorHandler()
|
|
76
|
+
const err = Object.assign(new SyntaxError('Unexpected token'), { status: 400, body: '{bad}' })
|
|
77
|
+
const req = makeMockReq()
|
|
78
|
+
const res = makeMockRes()
|
|
79
|
+
handler(err, req, res, () => {})
|
|
80
|
+
assert.equal(res.statusCode, 400, 'JSON 解析错误不应是 500')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('errorHandler: res.headersSent=true → 不覆盖,转 next(err)', () => {
|
|
84
|
+
const handler = createErrorHandler()
|
|
85
|
+
const err = new Error('已发响应后抛错')
|
|
86
|
+
const req = makeMockReq()
|
|
87
|
+
const res = makeMockRes(true) // 模拟已发响应
|
|
88
|
+
let nextCalled = null
|
|
89
|
+
handler(err, req, res, (e) => { nextCalled = e })
|
|
90
|
+
assert.equal(res._json, null, '不应再调 res.json')
|
|
91
|
+
assert.ok(nextCalled, '应调用 next(err) 让 express 关连接')
|
|
92
|
+
assert.equal(nextCalled, err)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('errorHandler: err 无 message (throw null) → 500 + 兜底文案', () => {
|
|
96
|
+
const handler = createErrorHandler()
|
|
97
|
+
const req = makeMockReq()
|
|
98
|
+
const res = makeMockRes()
|
|
99
|
+
handler(null, req, res, () => {})
|
|
100
|
+
assert.equal(res.statusCode, 500)
|
|
101
|
+
assert.equal(res._json.error, 'Internal Server Error')
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('errorHandler: 兼容 asyncRoute 的错误响应格式', () => {
|
|
105
|
+
// errorHandler 与 asyncRoute 共享 {success:false, error} 格式,
|
|
106
|
+
// 前端不需要区分错误来自哪个路径
|
|
107
|
+
const handler = createErrorHandler()
|
|
108
|
+
const err = new HttpError(422, '校验失败')
|
|
109
|
+
const req = makeMockReq()
|
|
110
|
+
const res = makeMockRes()
|
|
111
|
+
handler(err, req, res, () => {})
|
|
112
|
+
assert.deepEqual(Object.keys(res._json).sort(), ['error', 'success'])
|
|
113
|
+
assert.equal(res._json.success, false)
|
|
114
|
+
})
|
|
@@ -117,27 +117,6 @@ export function registerExecRoutes({
|
|
|
117
117
|
nextProcessId,
|
|
118
118
|
runningProcesses
|
|
119
119
|
}) {
|
|
120
|
-
// 通用命令执行接口(非流式)— 走 argv 模式,无 shell
|
|
121
|
-
app.post('/api/exec', asyncRoute(async (req, res) => {
|
|
122
|
-
try {
|
|
123
|
-
const { command } = req.body || {};
|
|
124
|
-
if (!command || typeof command !== 'string' || !command.trim()) {
|
|
125
|
-
return res.status(400).json({ success: false, error: 'command 不能为空' });
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
// execGitCommand 本身已经走 execFile argv 模式(见 utils/index.js)
|
|
130
|
-
// 这里保持调用形态兼容
|
|
131
|
-
const { stdout = '', stderr = '' } = await execGitCommand(splitCommandArgs(command), { log: false });
|
|
132
|
-
return res.json({ success: true, stdout, stderr });
|
|
133
|
-
} catch (err) {
|
|
134
|
-
return res.status(400).json({ success: false, error: err?.message || String(err) });
|
|
135
|
-
}
|
|
136
|
-
} catch (error) {
|
|
137
|
-
res.status(500).json({ success: false, error: error.message });
|
|
138
|
-
}
|
|
139
|
-
}));
|
|
140
|
-
|
|
141
120
|
// 流式执行命令接口(支持实时输出)— argv 模式 + cwd 校验
|
|
142
121
|
app.post('/api/exec-stream', asyncRoute(async (req, res) => {
|
|
143
122
|
try {
|
|
@@ -206,6 +206,68 @@ export function registerGitOpsRoutes({
|
|
|
206
206
|
}
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
+
// 推送并设置上游分支(git push -u origin <branch>)
|
|
210
|
+
app.post('/api/git/push-with-upstream', async (req, res) => {
|
|
211
|
+
try {
|
|
212
|
+
const { branch } = req.body || {};
|
|
213
|
+
if (!branch || typeof branch !== 'string') {
|
|
214
|
+
return res.status(400).json({ success: false, error: '缺少 branch 参数' });
|
|
215
|
+
}
|
|
216
|
+
const { stdout } = await execGitCommand(['push', '-u', 'origin', branch]);
|
|
217
|
+
setRecentPushStatus({
|
|
218
|
+
justPushed: true,
|
|
219
|
+
pushTime: Date.now(),
|
|
220
|
+
validDuration: 10000
|
|
221
|
+
});
|
|
222
|
+
logger.info(`推送并设置上游成功: origin/${branch}`);
|
|
223
|
+
res.json({ success: true, message: stdout });
|
|
224
|
+
} catch (error) {
|
|
225
|
+
res.status(500).json({ success: false, error: error.message });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// 读取单个 git 全局配置
|
|
230
|
+
// GET /api/git/global-config?key=<name>
|
|
231
|
+
// key 不存在时返回空串(与 git --get 行为一致;前端 getGlobalConfig 容错为空)
|
|
232
|
+
app.get('/api/git/global-config', async (req, res) => {
|
|
233
|
+
try {
|
|
234
|
+
const key = typeof req.query.key === 'string' ? req.query.key.trim() : '';
|
|
235
|
+
if (!key) {
|
|
236
|
+
return res.status(400).json({ success: false, error: '缺少 key 参数' });
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const { stdout } = await execGitCommand(['config', '--global', '--get', key], { log: false });
|
|
240
|
+
return res.json({ success: true, value: String(stdout || '').trim() });
|
|
241
|
+
} catch (err) {
|
|
242
|
+
// git --get 在 key 不存在时返回非零退出码 → 视为"未设置",返回空串
|
|
243
|
+
return res.json({ success: true, value: '' });
|
|
244
|
+
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
res.status(500).json({ success: false, error: error.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// 写入单个 git 全局配置
|
|
251
|
+
// POST /api/git/global-config body: { key, value }
|
|
252
|
+
// value 为空字符串时执行 --unset,与 git 语义一致(允许前端"清空某项")
|
|
253
|
+
app.post('/api/git/global-config', async (req, res) => {
|
|
254
|
+
try {
|
|
255
|
+
const { key, value } = req.body || {};
|
|
256
|
+
if (!key || typeof key !== 'string') {
|
|
257
|
+
return res.status(400).json({ success: false, error: '缺少 key 参数' });
|
|
258
|
+
}
|
|
259
|
+
const trimmed = typeof value === 'string' ? value.trim() : '';
|
|
260
|
+
if (!trimmed) {
|
|
261
|
+
await execGitCommand(['config', '--global', '--unset', key]);
|
|
262
|
+
} else {
|
|
263
|
+
await execGitCommand(['config', '--global', key, trimmed]);
|
|
264
|
+
}
|
|
265
|
+
res.json({ success: true });
|
|
266
|
+
} catch (error) {
|
|
267
|
+
res.status(500).json({ success: false, error: error.message });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
209
271
|
// 带进度的推送更改 (SSE)
|
|
210
272
|
app.post('/api/push-with-progress', async (req, res) => {
|
|
211
273
|
// 设置SSE响应头
|
|
@@ -71,21 +71,26 @@ async function listPorts({ all = false } = {}) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
async function listPortsWindows({ all = false } = {}) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
74
|
+
// 三个子进程并行执行(原实现串行 await,netstat TCP + netstat UDP + tasklist
|
|
75
|
+
// 串行耗时 = 三者之和;并行后 = max(三者)),Windows 上 netstat 对大量
|
|
76
|
+
// ESTABLISHED 连接很慢,并行能把 8-19s 压到 3-8s。
|
|
77
|
+
const [tcpRes, udpRes, tasklistRes] = await Promise.all([
|
|
78
|
+
execFileP('netstat', ['-ano', '-p', 'TCP']).catch((e) => {
|
|
79
|
+
logger.warn(`[monitor] netstat TCP 失败: ${e?.message || e}`)
|
|
80
|
+
return { stdout: '' }
|
|
81
|
+
}),
|
|
82
|
+
execFileP('netstat', ['-ano', '-p', 'UDP']).catch((e) => {
|
|
83
|
+
logger.warn(`[monitor] netstat UDP 失败: ${e?.message || e}`)
|
|
84
|
+
return { stdout: '' }
|
|
85
|
+
}),
|
|
86
|
+
execFileP('tasklist', ['/FO', 'CSV', '/NH']).catch((e) => {
|
|
87
|
+
logger.warn(`[monitor] tasklist 失败: ${e?.message || e}`)
|
|
88
|
+
return { stdout: '' }
|
|
89
|
+
})
|
|
90
|
+
])
|
|
91
|
+
|
|
92
|
+
const tcpLines = tcpRes.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
93
|
+
const udpLines = udpRes.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
89
94
|
|
|
90
95
|
const pids = new Set()
|
|
91
96
|
const ports = []
|
|
@@ -133,19 +138,14 @@ async function listPortsWindows({ all = false } = {}) {
|
|
|
133
138
|
for (const l of tcpLines) parseLine(l, 'TCP')
|
|
134
139
|
for (const l of udpLines) parseLine(l, 'UDP')
|
|
135
140
|
|
|
136
|
-
//
|
|
141
|
+
// tasklist 已在上面并行拿到,这里直接解析 PID -> 进程名映射
|
|
137
142
|
const pidToName = new Map()
|
|
138
143
|
if (pids.size > 0) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const m = line.match(/^"([^"]+)","(\d+)"/)
|
|
145
|
-
if (m) pidToName.set(m[2], m[1])
|
|
146
|
-
}
|
|
147
|
-
} catch (e) {
|
|
148
|
-
logger.warn(`[monitor] tasklist 失败: ${e?.message || e}`)
|
|
144
|
+
// 输出形如: "node.exe","1234","Console","1","123,456 K"
|
|
145
|
+
for (const line of tasklistRes.stdout.split(/\r?\n/)) {
|
|
146
|
+
if (!line.trim()) continue
|
|
147
|
+
const m = line.match(/^"([^"]+)","(\d+)"/)
|
|
148
|
+
if (m) pidToName.set(m[2], m[1])
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -278,6 +278,13 @@ async function killProcess(pid, { force = false } = {}) {
|
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
// ── 路由注册 ────────────────────────────────────────────────────────────
|
|
281
|
+
// ports 列表缓存:netstat + tasklist 在 Windows 上对大量连接很慢(3-8s),
|
|
282
|
+
// 前端每 10-15s 轮询一次,5s 缓存能把第二次以后的响应压到 <10ms。
|
|
283
|
+
// 仅缓存默认场景(all=false,只看监听端口);all=true 实时查不缓存。
|
|
284
|
+
let _portsCache = null
|
|
285
|
+
let _portsCacheTime = 0
|
|
286
|
+
const PORTS_CACHE_TTL_MS = 5000
|
|
287
|
+
|
|
281
288
|
export function registerMonitorRoutes({ app }) {
|
|
282
289
|
// 系统概览:CPU + 内存 + 系统信息
|
|
283
290
|
app.get(
|
|
@@ -315,7 +322,16 @@ export function registerMonitorRoutes({ app }) {
|
|
|
315
322
|
'/api/monitor/ports',
|
|
316
323
|
asyncRoute(async (req, res) => {
|
|
317
324
|
const all = req.query.all === '1' || req.query.all === 'true'
|
|
325
|
+
// 默认场景命中缓存直接返回(all=true 实时查,不缓存)
|
|
326
|
+
const now = Date.now()
|
|
327
|
+
if (!all && _portsCache && (now - _portsCacheTime) < PORTS_CACHE_TTL_MS) {
|
|
328
|
+
return res.json({ success: true, data: _portsCache, cached: true })
|
|
329
|
+
}
|
|
318
330
|
const ports = await listPorts({ all })
|
|
331
|
+
if (!all) {
|
|
332
|
+
_portsCache = ports
|
|
333
|
+
_portsCacheTime = now
|
|
334
|
+
}
|
|
319
335
|
res.json({ success: true, data: ports })
|
|
320
336
|
})
|
|
321
337
|
)
|