zen-gitsync 2.14.1 → 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.
Files changed (49) hide show
  1. package/package.json +1 -1
  2. package/src/ui/public/assets/{AppVersionBadge-zv0-I77-.js → AppVersionBadge-z2kqhmtD.js} +2 -2
  3. package/src/ui/public/assets/{BranchSelector-D7-iEofR.js → BranchSelector-CSjsCeeJ.js} +1 -1
  4. package/src/ui/public/assets/{CommandConsole-Bu6N8qeh.js → CommandConsole-96MUoG1F.js} +2 -2
  5. package/src/ui/public/assets/{CommitForm-C11gIhY6.js → CommitForm-BBKQ0GqO.js} +1 -1
  6. package/src/ui/public/assets/{CommonDialog-G17HYMln.js → CommonDialog-CgQovpCJ.js} +1 -1
  7. package/src/ui/public/assets/{EditorView-Ce6CTr9d.js → EditorView-aoA30emq.js} +0 -0
  8. package/src/ui/public/assets/{FlowExecutionViewer-DA_gtjWd.js → FlowExecutionViewer-BvXlaQai.js} +1 -1
  9. package/src/ui/public/assets/{FlowOrchestrationWorkspace-BJ6tqbOP.js → FlowOrchestrationWorkspace-CgWUF1Jc.js} +1 -1
  10. package/src/ui/public/assets/{LogList-DYGfJdcb.js → LogList-uF4tir23.js} +1 -1
  11. package/src/ui/public/assets/MindmapView-B8rUSi-e.css +1 -0
  12. package/src/ui/public/assets/MindmapView-B8wr0EFL.js +1 -0
  13. package/src/ui/public/assets/{MonitorView-Dlpgy8Bb.js → MonitorView-DaJ0h8Im.js} +1 -1
  14. package/src/ui/public/assets/{ProjectStartupButton-C3LM1U1R.js → ProjectStartupButton-CcH1NL2W.js} +1 -1
  15. package/src/ui/public/assets/{RemoteRepoCard-ebapcxCN.js → RemoteRepoCard-BDbJ76uf.js} +1 -1
  16. package/src/ui/public/assets/{SourceMapView-CFTKhZQi.js → SourceMapView-2HjAlHnp.js} +1 -1
  17. package/src/ui/public/assets/{SvgIcon-Cw2mL_zx.js → SvgIcon-CK6VGHb5.js} +1 -1
  18. package/src/ui/public/assets/{UserInputNode-CfmSt5Mh.js → UserInputNode-Cx8r5geA.js} +1 -1
  19. package/src/ui/public/assets/{WorkbenchView-kAXgcs6z.js → WorkbenchView-CG54iND-.js} +4 -4
  20. package/src/ui/public/assets/WorkbenchView-CvHPPs8Q.css +1 -0
  21. package/src/ui/public/assets/{_plugin-vue_export-helper-BMXWW0fG.js → _plugin-vue_export-helper-CJMndG_W.js} +2 -2
  22. package/src/ui/public/assets/configStore-DDnE5eie.js +1 -0
  23. package/src/ui/public/assets/index-B7FGNx_l.js +23 -0
  24. package/src/ui/public/assets/{index-gbRjS0sL.css → index-f6UhuE5j.css} +1 -1
  25. package/src/ui/public/index.html +6 -6
  26. package/src/ui/server/index.js +632 -497
  27. package/src/ui/server/middleware/errorHandler.js +84 -0
  28. package/src/ui/server/middleware/errorHandler.test.js +114 -0
  29. package/src/ui/server/routes/exec.js +0 -21
  30. package/src/ui/server/routes/gitOps.js +70 -4
  31. package/src/ui/server/routes/monitor.js +42 -26
  32. package/src/ui/server/routes/terminal.js +318 -320
  33. package/src/ui/server/routes/workbench/attachmentUtils.js +76 -0
  34. package/src/ui/server/routes/workbench/index.js +1680 -0
  35. package/src/ui/server/routes/workbench/instructionStore.js +175 -0
  36. package/src/ui/server/routes/workbench/jobStore.js +225 -0
  37. package/src/ui/server/routes/workbench/jsonParse.js +219 -0
  38. package/src/ui/server/routes/workbench/llmClient.js +214 -0
  39. package/src/ui/server/routes/workbench/projectScan.js +191 -0
  40. package/src/ui/server/routes/workbench/sessionStore.js +126 -0
  41. package/src/ui/server/routes/workbench/shared.js +115 -0
  42. package/src/ui/server/routes/workbench/taskRunner.js +493 -0
  43. package/src/ui/server/routes/workbench.js +16 -3523
  44. package/src/utils/index.js +165 -12
  45. package/src/ui/public/assets/MindmapView-Az3mQBEW.js +0 -1
  46. package/src/ui/public/assets/MindmapView-RpSZW35N.css +0 -1
  47. package/src/ui/public/assets/WorkbenchView-OmJ1eM5g.css +0 -1
  48. package/src/ui/public/assets/configStore-DHCrsR6z.js +0 -1
  49. package/src/ui/public/assets/index-C1q8_YYO.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响应头
@@ -664,8 +726,10 @@ export function registerGitOpsRoutes({
664
726
  });
665
727
  }
666
728
 
667
- // 尝试清理 Git 锁文件
668
- await checkAndClearGitLock();
729
+ // 尝试清理 Git 锁文件(破坏性入口:force=true 绕过 mtime 判断,
730
+ // 仍走 PID liveness 校验——用户主动触发的 reset --hard 不该被
731
+ // 一个刚崩出来的新孤儿锁挡住)
732
+ await checkAndClearGitLock({ force: true });
669
733
 
670
734
  // 执行 git reset --hard origin/branch 命令
671
735
  await execGitCommand(['reset', '--hard', `origin/${branch}`]);
@@ -682,8 +746,10 @@ export function registerGitOpsRoutes({
682
746
  // 清除本地所有更改,包括未跟踪文件 (git reset --hard && git clean -fd)
683
747
  app.post('/api/discard-all-changes', async (req, res) => {
684
748
  try {
685
- // 尝试清理 Git 锁文件
686
- await checkAndClearGitLock();
749
+ // 尝试清理 Git 锁文件(破坏性入口:force=true 绕过 mtime 判断,
750
+ // 仍走 PID liveness 校验——用户主动触发的 discard-all 不该被
751
+ // 一个刚崩出来的新孤儿锁挡住)
752
+ await checkAndClearGitLock({ force: true });
687
753
 
688
754
  // 1. 执行 git reset --hard 丢弃已跟踪文件的更改
689
755
  await execGitCommand(['reset', '--hard']);
@@ -71,21 +71,26 @@ async function listPorts({ all = false } = {}) {
71
71
  }
72
72
 
73
73
  async function listPortsWindows({ all = false } = {}) {
74
- const tcpLines = []
75
- const udpLines = []
76
- // netstat 一次只能指定一个 proto,分两次调用
77
- try {
78
- const { stdout } = await execFileP('netstat', ['-ano', '-p', 'TCP'])
79
- tcpLines.push(...stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean))
80
- } catch (e) {
81
- logger.warn(`[monitor] netstat TCP 失败: ${e?.message || e}`)
82
- }
83
- try {
84
- const { stdout } = await execFileP('netstat', ['-ano', '-p', 'UDP'])
85
- udpLines.push(...stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean))
86
- } catch (e) {
87
- logger.warn(`[monitor] netstat UDP 失败: ${e?.message || e}`)
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
- // 批量拿 PID -> 进程名映射,避免对每个 PID 单独 tasklist
141
+ // tasklist 已在上面并行拿到,这里直接解析 PID -> 进程名映射
137
142
  const pidToName = new Map()
138
143
  if (pids.size > 0) {
139
- try {
140
- const { stdout } = await execFileP('tasklist', ['/FO', 'CSV', '/NH'])
141
- // 输出形如: "node.exe","1234","Console","1","123,456 K"
142
- for (const line of stdout.split(/\r?\n/)) {
143
- if (!line.trim()) continue
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
  )