@muyichengshayu/promptx 0.2.2 → 0.2.4

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.
@@ -3,17 +3,19 @@ import process from 'node:process'
3
3
  import { spawn } from 'node:child_process'
4
4
  import { fileURLToPath } from 'node:url'
5
5
 
6
- const DEFAULT_TIMEOUT_MS = 20_000
6
+ const DEFAULT_TIMEOUT_MS = Math.max(1_000, Number(process.env.PROMPTX_GIT_DIFF_TIMEOUT_MS) || 60_000)
7
+ const DEFAULT_MAX_WORKERS = Math.max(1, Number(process.env.PROMPTX_GIT_DIFF_WORKER_POOL_SIZE) || 2)
7
8
  const MAX_STDERR_LENGTH = 4000
8
9
 
9
10
  const currentDir = path.dirname(fileURLToPath(import.meta.url))
10
11
  const workerEntryPath = path.join(currentDir, 'gitDiffWorker.js')
11
12
 
12
- let workerProcess = null
13
- let workerStdoutBuffer = ''
14
- let workerStderrBuffer = ''
15
13
  let nextRequestId = 1
16
- const pendingRequests = new Map()
14
+ let nextWorkerId = 1
15
+ const workerPool = []
16
+ const requestQueue = []
17
+ const requestMap = new Map()
18
+ const activeRequests = new Map()
17
19
  const workerMetrics = {
18
20
  startedAt: new Date().toISOString(),
19
21
  spawnCount: 0,
@@ -44,11 +46,6 @@ function createWorkerError(message = '') {
44
46
  return error
45
47
  }
46
48
 
47
- function resetWorkerBuffers() {
48
- workerStdoutBuffer = ''
49
- workerStderrBuffer = ''
50
- }
51
-
52
49
  function createLastRequestSnapshot(request = {}, patch = {}) {
53
50
  return {
54
51
  requestId: String(request.requestId || patch.requestId || '').trim(),
@@ -68,91 +65,266 @@ function markLastRequest(request = {}, patch = {}) {
68
65
  workerMetrics.lastRequest = createLastRequestSnapshot(request, patch)
69
66
  }
70
67
 
71
- function appendWorkerStderr(chunk) {
72
- workerStderrBuffer = `${workerStderrBuffer}${Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || '')}`
73
- if (workerStderrBuffer.length > MAX_STDERR_LENGTH) {
74
- workerStderrBuffer = workerStderrBuffer.slice(-MAX_STDERR_LENGTH)
68
+ function getNowIso() {
69
+ return new Date().toISOString()
70
+ }
71
+
72
+ function getRequestDurationMs(request = {}) {
73
+ const startedAt = Date.parse(String(request.requestMeta?.startedAt || ''))
74
+ return Number.isFinite(startedAt) ? Math.max(0, Date.now() - startedAt) : 0
75
+ }
76
+
77
+ function getWorkerIndex(workerId = '') {
78
+ return workerPool.findIndex((worker) => worker.id === workerId)
79
+ }
80
+
81
+ function getWorkerById(workerId = '') {
82
+ const index = getWorkerIndex(workerId)
83
+ return index >= 0 ? workerPool[index] : null
84
+ }
85
+
86
+ function removeWorker(worker) {
87
+ const index = getWorkerIndex(worker?.id)
88
+ if (index >= 0) {
89
+ workerPool.splice(index, 1)
90
+ }
91
+ }
92
+
93
+ function clearRequestTimer(request) {
94
+ if (request?.timer) {
95
+ clearTimeout(request.timer)
96
+ request.timer = null
75
97
  }
76
98
  }
77
99
 
78
- function settlePendingRequest(requestId, handler) {
79
- const pending = pendingRequests.get(requestId)
80
- if (!pending) {
100
+ function removeQueuedRequest(requestId = '') {
101
+ const index = requestQueue.findIndex((request) => request.requestId === requestId)
102
+ if (index >= 0) {
103
+ requestQueue.splice(index, 1)
104
+ }
105
+ }
106
+
107
+ function updateWorkerExitMetrics({ code = null, signal = '', reason = '' } = {}) {
108
+ workerMetrics.lastWorkerExitAt = getNowIso()
109
+ workerMetrics.lastWorkerExitCode = typeof code === 'number' ? code : null
110
+ workerMetrics.lastWorkerExitSignal = String(signal || '').trim()
111
+ workerMetrics.lastWorkerExitReason = String(reason || '').trim()
112
+ }
113
+
114
+ function finalizeRequest(request, handler) {
115
+ if (!request || request.settled) {
81
116
  return
82
117
  }
83
118
 
84
- pendingRequests.delete(requestId)
85
- clearTimeout(pending.timer)
86
- handler(pending)
119
+ request.settled = true
120
+ clearRequestTimer(request)
121
+ removeQueuedRequest(request.requestId)
122
+ requestMap.delete(request.requestId)
123
+ activeRequests.delete(request.requestId)
124
+ handler(request)
87
125
  }
88
126
 
89
- function rejectAllPendingRequests(error) {
90
- for (const [requestId, pending] of pendingRequests.entries()) {
91
- pendingRequests.delete(requestId)
92
- clearTimeout(pending.timer)
127
+ function rejectRequestWithError(request, error) {
128
+ finalizeRequest(request, (pending) => {
129
+ workerMetrics.failedRequests += 1
130
+ markLastRequest(pending.requestMeta, {
131
+ finishedAt: getNowIso(),
132
+ durationMs: getRequestDurationMs(pending),
133
+ status: error?.code === 'GIT_DIFF_TIMEOUT' ? 'timeout' : 'failed',
134
+ timeout: error?.code === 'GIT_DIFF_TIMEOUT',
135
+ errorMessage: String(error?.message || error || 'git diff request failed'),
136
+ })
93
137
  pending.reject(error)
138
+ })
139
+ }
140
+
141
+ function resolveRequest(request, value) {
142
+ finalizeRequest(request, (pending) => {
143
+ workerMetrics.completedRequests += 1
144
+ markLastRequest(pending.requestMeta, {
145
+ finishedAt: getNowIso(),
146
+ durationMs: getRequestDurationMs(pending),
147
+ status: 'completed',
148
+ })
149
+ pending.resolve(value)
150
+ })
151
+ }
152
+
153
+ function createWorkerState(child) {
154
+ return {
155
+ id: `git-diff-worker-${nextWorkerId++}`,
156
+ child,
157
+ stdoutBuffer: '',
158
+ stderrBuffer: '',
159
+ activeRequestId: '',
160
+ startedAt: getNowIso(),
161
+ stopping: false,
162
+ }
163
+ }
164
+
165
+ function appendWorkerStderr(worker, chunk) {
166
+ worker.stderrBuffer = `${worker.stderrBuffer}${Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || '')}`
167
+ if (worker.stderrBuffer.length > MAX_STDERR_LENGTH) {
168
+ worker.stderrBuffer = worker.stderrBuffer.slice(-MAX_STDERR_LENGTH)
169
+ }
170
+ }
171
+
172
+ function stopWorker(worker, reason = '') {
173
+ if (!worker || worker.stopping) {
174
+ return
175
+ }
176
+
177
+ worker.stopping = true
178
+ removeWorker(worker)
179
+ updateWorkerExitMetrics({
180
+ signal: 'manual_stop',
181
+ reason,
182
+ })
183
+
184
+ if (worker.child?.exitCode === null && !worker.child?.killed) {
185
+ worker.child.kill()
186
+ }
187
+ }
188
+
189
+ function dispatchQueuedRequests() {
190
+ while (requestQueue.length) {
191
+ let worker = workerPool.find((item) => !item.activeRequestId && !item.stopping) || null
192
+ if (!worker && workerPool.length < DEFAULT_MAX_WORKERS) {
193
+ worker = ensureGitDiffWorker()
194
+ }
195
+ if (!worker) {
196
+ return
197
+ }
198
+
199
+ const request = requestQueue.shift()
200
+ if (!request || request.settled) {
201
+ continue
202
+ }
203
+
204
+ request.workerId = worker.id
205
+ worker.activeRequestId = request.requestId
206
+ activeRequests.set(request.requestId, request)
207
+
208
+ try {
209
+ worker.child.stdin?.write(`${JSON.stringify({
210
+ requestId: request.requestId,
211
+ action: 'getTaskGitDiffReview',
212
+ taskSlug: request.taskSlug,
213
+ options: request.options,
214
+ })}\n`)
215
+ } catch (error) {
216
+ worker.activeRequestId = ''
217
+ activeRequests.delete(request.requestId)
218
+ request.workerId = ''
219
+ rejectRequestWithError(request, createWorkerError(String(error?.message || error)))
220
+ stopWorker(worker, String(error?.message || error || 'worker write failed'))
221
+ }
94
222
  }
95
223
  }
96
224
 
97
- function detachWorkerListeners(child) {
98
- child.stdout?.removeAllListeners()
99
- child.stderr?.removeAllListeners()
100
- child.removeAllListeners()
225
+ function settleWorkerRequest(worker, requestId, handler) {
226
+ const request = activeRequests.get(requestId)
227
+ if (!request) {
228
+ return
229
+ }
230
+
231
+ if (worker.activeRequestId === requestId) {
232
+ worker.activeRequestId = ''
233
+ }
234
+ request.workerId = ''
235
+ handler(request)
236
+ dispatchQueuedRequests()
101
237
  }
102
238
 
103
- function handleWorkerLine(line) {
239
+ function handleWorkerLine(worker, line) {
104
240
  let payload
105
241
  try {
106
242
  payload = JSON.parse(line)
107
243
  } catch {
108
- rejectAllPendingRequests(
109
- createWorkerError(`git diff worker returned invalid JSON: ${line.slice(0, 200)}`)
110
- )
111
- stopGitDiffWorker()
244
+ const error = createWorkerError(`git diff worker returned invalid JSON: ${line.slice(0, 200)}`)
245
+ if (worker.activeRequestId) {
246
+ settleWorkerRequest(worker, worker.activeRequestId, (request) => {
247
+ rejectRequestWithError(request, error)
248
+ })
249
+ }
250
+ stopWorker(worker, error.message)
112
251
  return
113
252
  }
114
253
 
115
254
  const requestId = String(payload?.requestId || '').trim()
116
255
  if (!requestId) {
117
- rejectAllPendingRequests(
118
- createWorkerError(String(payload?.error?.message || '').trim() || 'git diff worker returned a response without requestId.')
256
+ const error = createWorkerError(
257
+ String(payload?.error?.message || '').trim() || 'git diff worker returned a response without requestId.'
119
258
  )
120
- stopGitDiffWorker()
259
+ if (worker.activeRequestId) {
260
+ settleWorkerRequest(worker, worker.activeRequestId, (request) => {
261
+ rejectRequestWithError(request, error)
262
+ })
263
+ }
264
+ stopWorker(worker, error.message)
121
265
  return
122
266
  }
123
267
 
124
- settlePendingRequest(requestId, ({ resolve, reject }) => {
268
+ settleWorkerRequest(worker, requestId, (request) => {
125
269
  if (!payload?.ok) {
126
- reject(createWorkerError(String(payload?.error?.message || '').trim() || workerStderrBuffer.trim()))
270
+ const error = createWorkerError(String(payload?.error?.message || '').trim() || worker.stderrBuffer.trim())
271
+ rejectRequestWithError(request, error)
127
272
  return
128
273
  }
129
- resolve(payload.result)
274
+ resolveRequest(request, payload.result)
130
275
  })
131
276
  }
132
277
 
133
- function handleWorkerStdout(chunk) {
134
- workerStdoutBuffer = `${workerStdoutBuffer}${Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || '')}`
278
+ function handleWorkerStdout(worker, chunk) {
279
+ worker.stdoutBuffer = `${worker.stdoutBuffer}${Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || '')}`
135
280
 
136
- let newlineIndex = workerStdoutBuffer.indexOf('\n')
281
+ let newlineIndex = worker.stdoutBuffer.indexOf('\n')
137
282
  while (newlineIndex !== -1) {
138
- const line = workerStdoutBuffer.slice(0, newlineIndex).trim()
139
- workerStdoutBuffer = workerStdoutBuffer.slice(newlineIndex + 1)
283
+ const line = worker.stdoutBuffer.slice(0, newlineIndex).trim()
284
+ worker.stdoutBuffer = worker.stdoutBuffer.slice(newlineIndex + 1)
140
285
  if (line) {
141
- handleWorkerLine(line)
286
+ handleWorkerLine(worker, line)
142
287
  }
143
- newlineIndex = workerStdoutBuffer.indexOf('\n')
288
+ newlineIndex = worker.stdoutBuffer.indexOf('\n')
144
289
  }
145
290
  }
146
291
 
147
- function ensureGitDiffWorker() {
148
- if (workerProcess && !workerProcess.killed && workerProcess.exitCode === null) {
149
- return workerProcess
292
+ function handleWorkerFailure(worker, error) {
293
+ removeWorker(worker)
294
+ updateWorkerExitMetrics({
295
+ reason: String(error?.message || error || 'worker error'),
296
+ })
297
+
298
+ if (worker.activeRequestId) {
299
+ settleWorkerRequest(worker, worker.activeRequestId, (request) => {
300
+ rejectRequestWithError(request, createWorkerError(String(error?.message || error)))
301
+ })
150
302
  }
151
303
 
152
- resetWorkerBuffers()
153
- if (workerMetrics.spawnCount > 0) {
154
- workerMetrics.restartCount += 1
304
+ dispatchQueuedRequests()
305
+ }
306
+
307
+ function handleWorkerClose(worker, code, signal) {
308
+ removeWorker(worker)
309
+ updateWorkerExitMetrics({
310
+ code,
311
+ signal,
312
+ reason: worker.stderrBuffer.trim() || (signal ? `signal:${signal}` : `code:${code}`),
313
+ })
314
+
315
+ if (worker.activeRequestId) {
316
+ const error = worker.stderrBuffer.trim()
317
+ ? createWorkerError(`git diff worker exited unexpectedly: ${worker.stderrBuffer.trim()}`)
318
+ : createWorkerError('git diff worker exited unexpectedly.')
319
+ settleWorkerRequest(worker, worker.activeRequestId, (request) => {
320
+ rejectRequestWithError(request, error)
321
+ })
155
322
  }
323
+
324
+ dispatchQueuedRequests()
325
+ }
326
+
327
+ function ensureGitDiffWorker() {
156
328
  const child = spawn(process.execPath, [workerEntryPath], {
157
329
  cwd: currentDir,
158
330
  env: {
@@ -161,58 +333,46 @@ function ensureGitDiffWorker() {
161
333
  stdio: ['pipe', 'pipe', 'pipe'],
162
334
  windowsHide: true,
163
335
  })
336
+
337
+ if (workerMetrics.spawnCount > 0) {
338
+ workerMetrics.restartCount += 1
339
+ }
340
+
341
+ const worker = createWorkerState(child)
342
+ workerPool.push(worker)
164
343
  workerMetrics.spawnCount += 1
165
- workerMetrics.lastWorkerSpawnedAt = new Date().toISOString()
344
+ workerMetrics.lastWorkerSpawnedAt = worker.startedAt
166
345
 
167
- child.stdout?.on('data', handleWorkerStdout)
168
- child.stderr?.on('data', appendWorkerStderr)
346
+ child.stdout?.on('data', (chunk) => {
347
+ handleWorkerStdout(worker, chunk)
348
+ })
349
+ child.stderr?.on('data', (chunk) => {
350
+ appendWorkerStderr(worker, chunk)
351
+ })
169
352
  child.on('error', (error) => {
170
- workerMetrics.lastWorkerExitAt = new Date().toISOString()
171
- workerMetrics.lastWorkerExitCode = null
172
- workerMetrics.lastWorkerExitSignal = ''
173
- workerMetrics.lastWorkerExitReason = String(error?.message || error || 'worker error')
174
- rejectAllPendingRequests(createWorkerError(String(error?.message || error)))
175
- if (workerProcess === child) {
176
- workerProcess = null
177
- }
178
- detachWorkerListeners(child)
353
+ handleWorkerFailure(worker, error)
179
354
  })
180
355
  child.on('close', (code, signal) => {
181
- const tail = workerStderrBuffer.trim()
182
- workerMetrics.lastWorkerExitAt = new Date().toISOString()
183
- workerMetrics.lastWorkerExitCode = typeof code === 'number' ? code : null
184
- workerMetrics.lastWorkerExitSignal = String(signal || '').trim()
185
- workerMetrics.lastWorkerExitReason = tail || (signal ? `signal:${signal}` : `code:${code}`)
186
- rejectAllPendingRequests(
187
- tail
188
- ? createWorkerError(`git diff worker exited unexpectedly: ${tail}`)
189
- : createWorkerError('git diff worker exited unexpectedly.')
190
- )
191
- if (workerProcess === child) {
192
- workerProcess = null
193
- }
194
- detachWorkerListeners(child)
195
- resetWorkerBuffers()
356
+ handleWorkerClose(worker, code, signal)
196
357
  })
197
358
 
198
- workerProcess = child
199
- return child
359
+ return worker
200
360
  }
201
361
 
202
362
  export function stopGitDiffWorker() {
203
- if (!workerProcess) {
204
- return
205
- }
363
+ const stopError = createWorkerError('git diff worker stopped.')
206
364
 
207
- const child = workerProcess
208
- workerProcess = null
209
- rejectAllPendingRequests(createWorkerError('git diff worker stopped.'))
210
- detachWorkerListeners(child)
211
- resetWorkerBuffers()
365
+ requestQueue.splice(0, requestQueue.length).forEach((request) => {
366
+ rejectRequestWithError(request, stopError)
367
+ })
212
368
 
213
- if (child.exitCode === null && !child.killed) {
214
- child.kill()
369
+ for (const request of [...activeRequests.values()]) {
370
+ rejectRequestWithError(request, stopError)
215
371
  }
372
+
373
+ ;[...workerPool].forEach((worker) => {
374
+ stopWorker(worker, 'stopped')
375
+ })
216
376
  }
217
377
 
218
378
  export function getTaskGitDiffReviewInSubprocess(taskSlug = '', options = {}) {
@@ -223,81 +383,83 @@ export function getTaskGitDiffReviewInSubprocess(taskSlug = '', options = {}) {
223
383
  taskSlug: String(taskSlug || '').trim(),
224
384
  scope: String(options.scope || '').trim(),
225
385
  filePath: String(options.filePath || '').trim(),
226
- startedAt: new Date().toISOString(),
386
+ startedAt: getNowIso(),
227
387
  }
228
- const child = ensureGitDiffWorker()
388
+
229
389
  workerMetrics.totalRequests += 1
230
390
 
231
391
  return new Promise((resolve, reject) => {
232
- const timer = setTimeout(() => {
233
- pendingRequests.delete(requestId)
234
- workerMetrics.timeoutRequests += 1
235
- workerMetrics.failedRequests += 1
236
- markLastRequest(requestMeta, {
237
- finishedAt: new Date().toISOString(),
238
- durationMs: Date.now() - Date.parse(requestMeta.startedAt),
239
- status: 'timeout',
240
- timeout: true,
241
- errorMessage: `git diff request timed out after ${timeoutMs}ms`,
242
- })
243
- stopGitDiffWorker()
244
- reject(createTimeoutError(timeoutMs))
245
- }, timeoutMs)
246
-
247
- pendingRequests.set(requestId, {
248
- resolve: (value) => {
249
- workerMetrics.completedRequests += 1
250
- markLastRequest(requestMeta, {
251
- finishedAt: new Date().toISOString(),
252
- durationMs: Date.now() - Date.parse(requestMeta.startedAt),
253
- status: 'completed',
254
- })
255
- resolve(value)
256
- },
257
- reject: (error) => {
258
- workerMetrics.failedRequests += 1
259
- markLastRequest(requestMeta, {
260
- finishedAt: new Date().toISOString(),
261
- durationMs: Date.now() - Date.parse(requestMeta.startedAt),
262
- status: error?.code === 'GIT_DIFF_TIMEOUT' ? 'timeout' : 'failed',
263
- timeout: error?.code === 'GIT_DIFF_TIMEOUT',
264
- errorMessage: String(error?.message || error || 'git diff request failed'),
265
- })
266
- reject(error)
267
- },
268
- timer,
269
- })
270
-
271
- try {
272
- child.stdin?.write(`${JSON.stringify({
273
- requestId,
274
- action: 'getTaskGitDiffReview',
275
- taskSlug: String(taskSlug || '').trim(),
276
- options,
277
- })}\n`)
278
- } catch (error) {
279
- settlePendingRequest(requestId, ({ reject: rejectPending }) => {
280
- rejectPending(createWorkerError(String(error?.message || error)))
281
- })
282
- stopGitDiffWorker()
392
+ const request = {
393
+ requestId,
394
+ taskSlug: String(taskSlug || '').trim(),
395
+ options,
396
+ resolve,
397
+ reject,
398
+ requestMeta,
399
+ settled: false,
400
+ workerId: '',
401
+ timer: setTimeout(() => {
402
+ const currentRequest = requestMap.get(requestId)
403
+ if (!currentRequest || currentRequest.settled) {
404
+ return
405
+ }
406
+
407
+ workerMetrics.timeoutRequests += 1
408
+ const timeoutError = createTimeoutError(timeoutMs)
409
+ if (currentRequest.workerId) {
410
+ const worker = getWorkerById(currentRequest.workerId)
411
+ if (worker && worker.activeRequestId === requestId) {
412
+ worker.activeRequestId = ''
413
+ }
414
+ rejectRequestWithError(currentRequest, timeoutError)
415
+ if (worker) {
416
+ stopWorker(worker, `request_timeout:${requestId}`)
417
+ }
418
+ dispatchQueuedRequests()
419
+ return
420
+ }
421
+
422
+ rejectRequestWithError(currentRequest, timeoutError)
423
+ }, timeoutMs),
283
424
  }
425
+
426
+ request.timer.unref?.()
427
+ requestMap.set(requestId, request)
428
+ requestQueue.push(request)
429
+ dispatchQueuedRequests()
284
430
  })
285
431
  }
286
432
 
287
433
  export function __getGitDiffWorkerPidForTest() {
288
- return Number(workerProcess?.pid || 0)
434
+ return Number(workerPool[0]?.child?.pid || 0)
435
+ }
436
+
437
+ export function __getGitDiffWorkerPidsForTest() {
438
+ return workerPool.map((worker) => Number(worker.child?.pid || 0)).filter((pid) => pid > 0)
289
439
  }
290
440
 
291
441
  export function getGitDiffWorkerDiagnostics() {
292
442
  return {
293
443
  worker: {
294
- pid: Number(workerProcess?.pid || 0),
295
- running: Boolean(workerProcess && !workerProcess.killed && workerProcess.exitCode === null),
296
- pendingRequests: pendingRequests.size,
297
- stderrTail: workerStderrBuffer.trim(),
444
+ pid: Number(workerPool[0]?.child?.pid || 0),
445
+ running: workerPool.some((worker) => !worker.stopping && worker.child?.exitCode === null && !worker.child?.killed),
446
+ pendingRequests: requestQueue.length + activeRequests.size,
447
+ stderrTail: String(workerPool[0]?.stderrBuffer || '').trim(),
298
448
  },
449
+ workers: workerPool.map((worker) => ({
450
+ id: worker.id,
451
+ pid: Number(worker.child?.pid || 0),
452
+ running: !worker.stopping && worker.child?.exitCode === null && !worker.child?.killed,
453
+ activeRequestId: String(worker.activeRequestId || '').trim(),
454
+ stderrTail: String(worker.stderrBuffer || '').trim(),
455
+ startedAt: worker.startedAt,
456
+ })),
299
457
  metrics: {
300
458
  ...workerMetrics,
459
+ maxWorkers: DEFAULT_MAX_WORKERS,
460
+ queueDepth: requestQueue.length,
461
+ activeRequestCount: activeRequests.size,
462
+ workerCount: workerPool.length,
301
463
  lastWorkerExitSignal: String(workerMetrics.lastWorkerExitSignal || ''),
302
464
  lastWorkerExitReason: String(workerMetrics.lastWorkerExitReason || ''),
303
465
  lastRequest: workerMetrics.lastRequest ? { ...workerMetrics.lastRequest } : null,
@@ -136,7 +136,7 @@ export function createRunDispatchService(options = {}) {
136
136
  throw createApiError('errors.shellEmptyCommand', '请输入要执行的命令,例如 !git status', 400)
137
137
  }
138
138
  if (commandMode === 'shell' && !allowShellCommand) {
139
- throw createApiError('errors.shellLocalOnly', '命令模式默认仅允许在本机本地界面中使用;如需对远程访问开放,请先到设置里显式开启。', 403)
139
+ throw createApiError('errors.shellLocalOnly', '当前入口未被允许执行命令。请在设置 -> 通用 -> 远程命令安全中启用对应模式,或改为本机本地访问。', 403)
140
140
  }
141
141
 
142
142
  const task = getTaskBySlug(normalizedTaskSlug)
@@ -7,6 +7,7 @@ const SYSTEM_CONFIG_FILE = 'system-config.json'
7
7
  const DEFAULT_RUNNER_MAX_CONCURRENT_RUNS = 3
8
8
  const MIN_RUNNER_MAX_CONCURRENT_RUNS = 1
9
9
  const MAX_RUNNER_MAX_CONCURRENT_RUNS = 16
10
+ const REMOTE_COMMAND_SECURITY_MODES = new Set(['disabled', 'relay', 'trusted-proxy'])
10
11
 
11
12
  function getSystemConfigPath() {
12
13
  const { dataDir } = ensurePromptxStorageReady()
@@ -40,9 +41,30 @@ function normalizeRunnerConfig(input = {}, fallback = {}) {
40
41
  }
41
42
  }
42
43
 
44
+ function normalizeRemoteCommandSecurityConfig(input = {}, fallback = {}) {
45
+ const inputMode = String(input?.mode || '').trim()
46
+ const fallbackMode = String(fallback?.mode || '').trim()
47
+ const mode = REMOTE_COMMAND_SECURITY_MODES.has(inputMode)
48
+ ? inputMode
49
+ : REMOTE_COMMAND_SECURITY_MODES.has(fallbackMode)
50
+ ? fallbackMode
51
+ : 'disabled'
52
+
53
+ return {
54
+ mode,
55
+ trustedProxyToken: mode === 'trusted-proxy'
56
+ ? String(input?.trustedProxyToken || fallback?.trustedProxyToken || '').trim()
57
+ : '',
58
+ }
59
+ }
60
+
43
61
  function normalizeSystemConfig(input = {}, fallback = {}) {
44
62
  return {
45
63
  runner: normalizeRunnerConfig(input?.runner || {}, fallback?.runner || {}),
64
+ remoteCommandSecurity: normalizeRemoteCommandSecurityConfig(
65
+ input?.remoteCommandSecurity || {},
66
+ fallback?.remoteCommandSecurity || {}
67
+ ),
46
68
  }
47
69
  }
48
70
 
@@ -73,7 +95,7 @@ function getSystemConfigManagedByEnv() {
73
95
  }
74
96
  }
75
97
 
76
- function getSystemConfigForClient() {
98
+ function getSystemConfigForRuntime() {
77
99
  const stored = readStoredSystemConfig()
78
100
  const managedByEnv = getSystemConfigManagedByEnv()
79
101
 
@@ -86,8 +108,21 @@ function getSystemConfigForClient() {
86
108
  }, stored)
87
109
  }
88
110
 
111
+ function getSystemConfigForClient() {
112
+ const effective = getSystemConfigForRuntime()
113
+
114
+ return {
115
+ runner: effective.runner,
116
+ remoteCommandSecurity: {
117
+ mode: String(effective.remoteCommandSecurity?.mode || 'disabled').trim() || 'disabled',
118
+ trustedProxyTokenConfigured: Boolean(String(effective.remoteCommandSecurity?.trustedProxyToken || '').trim()),
119
+ },
120
+ }
121
+ }
122
+
89
123
  export {
90
124
  getSystemConfigForClient,
125
+ getSystemConfigForRuntime,
91
126
  getSystemConfigManagedByEnv,
92
127
  getSystemConfigPath,
93
128
  normalizeSystemConfig,