@jingyi0605/codingns 0.3.6 → 0.4.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 (185) hide show
  1. package/README.md +3 -0
  2. package/bin/codingns.mjs +489 -1
  3. package/dist/public/assets/{TerminalPage-D00S4KM6.js → TerminalPage-6jHZV9Mh.js} +17 -17
  4. package/dist/public/assets/index-CSVhg7I8.js +123 -0
  5. package/dist/public/assets/index-Ce1VX19m.css +1 -0
  6. package/dist/public/index.html +2 -2
  7. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +173 -0
  8. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +307 -0
  9. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  10. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +199 -2
  11. package/dist/server/modules/assistant-capability/assistant-capability-service.js +565 -3
  12. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  13. package/dist/server/modules/butler/assistant-automation-service.d.ts +110 -0
  14. package/dist/server/modules/butler/assistant-automation-service.js +786 -0
  15. package/dist/server/modules/butler/assistant-automation-service.js.map +1 -0
  16. package/dist/server/modules/butler/assistant-automation-trigger.d.ts +94 -0
  17. package/dist/server/modules/butler/assistant-automation-trigger.js +400 -0
  18. package/dist/server/modules/butler/assistant-automation-trigger.js.map +1 -0
  19. package/dist/server/modules/butler/assistant-sandbox-service.d.ts +55 -0
  20. package/dist/server/modules/butler/assistant-sandbox-service.js +266 -0
  21. package/dist/server/modules/butler/assistant-sandbox-service.js.map +1 -0
  22. package/dist/server/modules/butler/butler-action-context-service.d.ts +4 -1
  23. package/dist/server/modules/butler/butler-action-context-service.js +8 -2
  24. package/dist/server/modules/butler/butler-action-context-service.js.map +1 -1
  25. package/dist/server/modules/butler/butler-control-session-service.d.ts +8 -1
  26. package/dist/server/modules/butler/butler-control-session-service.js +154 -40
  27. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -1
  28. package/dist/server/modules/butler/butler-control-timer-scheduler.d.ts +32 -0
  29. package/dist/server/modules/butler/butler-control-timer-scheduler.js +93 -0
  30. package/dist/server/modules/butler/butler-control-timer-scheduler.js.map +1 -0
  31. package/dist/server/modules/butler/butler-control-timer-service.d.ts +42 -0
  32. package/dist/server/modules/butler/butler-control-timer-service.js +132 -0
  33. package/dist/server/modules/butler/butler-control-timer-service.js.map +1 -0
  34. package/dist/server/modules/butler/butler-controller.d.ts +42 -2
  35. package/dist/server/modules/butler/butler-controller.js +79 -12
  36. package/dist/server/modules/butler/butler-controller.js.map +1 -1
  37. package/dist/server/modules/butler/butler-follow-up-service.d.ts +9 -1
  38. package/dist/server/modules/butler/butler-follow-up-service.js +273 -181
  39. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -1
  40. package/dist/server/modules/butler/butler-inbox-analysis-service.d.ts +4 -1
  41. package/dist/server/modules/butler/butler-inbox-analysis-service.js +18 -4
  42. package/dist/server/modules/butler/butler-inbox-analysis-service.js.map +1 -1
  43. package/dist/server/modules/butler/butler-profile-service.js +2 -5
  44. package/dist/server/modules/butler/butler-profile-service.js.map +1 -1
  45. package/dist/server/modules/butler/butler-project-service.d.ts +3 -1
  46. package/dist/server/modules/butler/butler-project-service.js +7 -1
  47. package/dist/server/modules/butler/butler-project-service.js.map +1 -1
  48. package/dist/server/modules/butler/butler-session-service.d.ts +3 -1
  49. package/dist/server/modules/butler/butler-session-service.js +12 -1
  50. package/dist/server/modules/butler/butler-session-service.js.map +1 -1
  51. package/dist/server/modules/butler/butler-session-summary-service.js +2 -1
  52. package/dist/server/modules/butler/butler-session-summary-service.js.map +1 -1
  53. package/dist/server/modules/butler/butler-workspace-context.d.ts +3 -0
  54. package/dist/server/modules/butler/butler-workspace-context.js +164 -44
  55. package/dist/server/modules/butler/butler-workspace-context.js.map +1 -1
  56. package/dist/server/modules/butler/patrol-execution-service.js +2 -1
  57. package/dist/server/modules/butler/patrol-execution-service.js.map +1 -1
  58. package/dist/server/modules/butler/provider-adapter-registry.d.ts +3 -0
  59. package/dist/server/modules/butler/provider-adapter-registry.js +18 -1
  60. package/dist/server/modules/butler/provider-adapter-registry.js.map +1 -1
  61. package/dist/server/modules/butler/verification-run-service.d.ts +9 -2
  62. package/dist/server/modules/butler/verification-run-service.js +188 -34
  63. package/dist/server/modules/butler/verification-run-service.js.map +1 -1
  64. package/dist/server/modules/debug-target/debug-target-controller.js +1 -1
  65. package/dist/server/modules/debug-target/debug-target-controller.js.map +1 -1
  66. package/dist/server/modules/debug-target/debug-target-service.d.ts +7 -2
  67. package/dist/server/modules/debug-target/debug-target-service.js +563 -100
  68. package/dist/server/modules/debug-target/debug-target-service.js.map +1 -1
  69. package/dist/server/modules/git/git-command-helper-client.d.ts +1 -0
  70. package/dist/server/modules/git/git-command-helper-client.js +19 -26
  71. package/dist/server/modules/git/git-command-helper-client.js.map +1 -1
  72. package/dist/server/modules/git/git-command-runner.js +19 -1
  73. package/dist/server/modules/git/git-command-runner.js.map +1 -1
  74. package/dist/server/modules/preferences/profile-service.d.ts +3 -1
  75. package/dist/server/modules/preferences/profile-service.js +74 -3
  76. package/dist/server/modules/preferences/profile-service.js.map +1 -1
  77. package/dist/server/modules/provider/provider-discovery-helper-client.d.ts +5 -3
  78. package/dist/server/modules/provider/provider-discovery-helper-client.js +129 -43
  79. package/dist/server/modules/provider/provider-discovery-helper-client.js.map +1 -1
  80. package/dist/server/modules/provider/provider-discovery-helper-process.js +44 -0
  81. package/dist/server/modules/provider/provider-discovery-helper-process.js.map +1 -1
  82. package/dist/server/modules/provider/provider-discovery-runtime.js +83 -3
  83. package/dist/server/modules/provider/provider-discovery-runtime.js.map +1 -1
  84. package/dist/server/modules/sessions/claude-runtime-helper-client.js +23 -1
  85. package/dist/server/modules/sessions/claude-runtime-helper-client.js.map +1 -1
  86. package/dist/server/modules/sessions/session-history-service.d.ts +7 -1
  87. package/dist/server/modules/sessions/session-history-service.js +251 -41
  88. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  89. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +6 -0
  90. package/dist/server/modules/sessions/session-live-runtime-service.js +97 -11
  91. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  92. package/dist/server/modules/sessions/session-message-origin-utils.d.ts +12 -0
  93. package/dist/server/modules/sessions/session-message-origin-utils.js +45 -0
  94. package/dist/server/modules/sessions/session-message-origin-utils.js.map +1 -0
  95. package/dist/server/modules/sessions/session-permission-request-service.js +167 -0
  96. package/dist/server/modules/sessions/session-permission-request-service.js.map +1 -1
  97. package/dist/server/modules/skills/builtin-skill-service.js +1 -1
  98. package/dist/server/modules/skills/builtin-skill-service.js.map +1 -1
  99. package/dist/server/modules/skills/builtin-skills/codingns-assistant/SKILL.md +19 -12
  100. package/dist/server/modules/skills/builtin-skills/codingns-assistant/references/cli-workflow.md +9 -3
  101. package/dist/server/modules/tasks/task-helper-client.d.ts +5 -2
  102. package/dist/server/modules/tasks/task-helper-client.js +118 -38
  103. package/dist/server/modules/tasks/task-helper-client.js.map +1 -1
  104. package/dist/server/modules/tasks/task-helper-process.js +94 -3
  105. package/dist/server/modules/tasks/task-helper-process.js.map +1 -1
  106. package/dist/server/modules/tasks/task-types.d.ts +3 -0
  107. package/dist/server/modules/tasks/task-types.js +4 -1
  108. package/dist/server/modules/tasks/task-types.js.map +1 -1
  109. package/dist/server/modules/terminal/command-template-service.d.ts +9 -0
  110. package/dist/server/modules/terminal/command-template-service.js +87 -5
  111. package/dist/server/modules/terminal/command-template-service.js.map +1 -1
  112. package/dist/server/modules/terminal/terminal-controller.d.ts +3 -0
  113. package/dist/server/modules/terminal/terminal-controller.js +41 -0
  114. package/dist/server/modules/terminal/terminal-controller.js.map +1 -1
  115. package/dist/server/modules/workbench/workbench-service.d.ts +3 -0
  116. package/dist/server/modules/workbench/workbench-service.js +4 -3
  117. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  118. package/dist/server/modules/workbench/workspace-file-watcher.d.ts +14 -6
  119. package/dist/server/modules/workbench/workspace-file-watcher.js +267 -57
  120. package/dist/server/modules/workbench/workspace-file-watcher.js.map +1 -1
  121. package/dist/server/modules/workbench/workspace-panel-snapshot-service.d.ts +2 -0
  122. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +32 -3
  123. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
  124. package/dist/server/modules/worktree/worktree-manager.d.ts +9 -1
  125. package/dist/server/modules/worktree/worktree-manager.js +9 -1
  126. package/dist/server/modules/worktree/worktree-manager.js.map +1 -1
  127. package/dist/server/routes/assistant.js +19 -0
  128. package/dist/server/routes/assistant.js.map +1 -1
  129. package/dist/server/routes/butler.js +5 -0
  130. package/dist/server/routes/butler.js.map +1 -1
  131. package/dist/server/server/create-server.d.ts +8 -0
  132. package/dist/server/server/create-server.js +36 -13
  133. package/dist/server/server/create-server.js.map +1 -1
  134. package/dist/server/storage/repositories/assistant-automation-run-repository.d.ts +12 -0
  135. package/dist/server/storage/repositories/assistant-automation-run-repository.js +139 -0
  136. package/dist/server/storage/repositories/assistant-automation-run-repository.js.map +1 -0
  137. package/dist/server/storage/repositories/assistant-automation-task-repository.d.ts +15 -0
  138. package/dist/server/storage/repositories/assistant-automation-task-repository.js +173 -0
  139. package/dist/server/storage/repositories/assistant-automation-task-repository.js.map +1 -0
  140. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.d.ts +17 -0
  141. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js +164 -0
  142. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js.map +1 -0
  143. package/dist/server/storage/repositories/butler-control-session-repository.js +27 -3
  144. package/dist/server/storage/repositories/butler-control-session-repository.js.map +1 -1
  145. package/dist/server/storage/repositories/butler-control-timer-repository.d.ts +15 -0
  146. package/dist/server/storage/repositories/butler-control-timer-repository.js +157 -0
  147. package/dist/server/storage/repositories/butler-control-timer-repository.js.map +1 -0
  148. package/dist/server/storage/repositories/user-preference-profile-repository.js +6 -3
  149. package/dist/server/storage/repositories/user-preference-profile-repository.js.map +1 -1
  150. package/dist/server/storage/sqlite/client.js +239 -2
  151. package/dist/server/storage/sqlite/client.js.map +1 -1
  152. package/dist/server/storage/sqlite/schema.sql +107 -1
  153. package/dist/server/types/domain.d.ts +89 -2
  154. package/dist/server/ws/workbench-ws-hub.d.ts +14 -8
  155. package/dist/server/ws/workbench-ws-hub.js +299 -163
  156. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  157. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +4 -1
  158. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +111 -3
  159. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
  160. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +6 -1
  161. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +306 -31
  162. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  163. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +5 -1
  164. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +187 -26
  165. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -1
  166. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +4 -1
  167. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +98 -1
  168. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -1
  169. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +2 -0
  170. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +71 -8
  171. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  172. package/node_modules/@codingns/session-sync-core/dist/providers/utils.d.ts +1 -0
  173. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js +4 -1
  174. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js.map +1 -1
  175. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.js +44 -0
  176. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.js.map +1 -1
  177. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +9 -3
  178. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  179. package/node_modules/@codingns/session-sync-core/dist/runtime/types.d.ts +1 -0
  180. package/node_modules/@codingns/session-sync-core/dist/services.js +17 -8
  181. package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
  182. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +4 -0
  183. package/package.json +1 -1
  184. package/dist/public/assets/index-BlOinYqR.js +0 -122
  185. package/dist/public/assets/index-Dg_7g6lA.css +0 -1
@@ -4,7 +4,9 @@ import path from "node:path";
4
4
  import { AppError } from "../../shared/errors/app-error.js";
5
5
  import { createId } from "../../shared/utils/id.js";
6
6
  import { nowIso } from "../../shared/utils/time.js";
7
+ import { DEFAULT_DEBUG_PORT_POOLS } from "../preferences/profile-service.js";
7
8
  import { buildTemplateCommandLine, getShellEnterSequence } from "../terminal/terminal-shell.js";
9
+ import { discoverTemplateRuntimeStatuses } from "../terminal/template-port-runtime.js";
8
10
  import { createTaskManager } from "../tasks/task-manager.js";
9
11
  import { HOST_TASK_TYPES } from "../tasks/task-types.js";
10
12
  import { FRAMEWORK_COMPATIBILITY_MATRIX, FRAMEWORK_COMPATIBILITY_MATRIX_VERSION, getFrameworkCompatibilityItem } from "./framework-compatibility-matrix.js";
@@ -21,10 +23,11 @@ export class DebugTargetService {
21
23
  runtimeBindingRepository;
22
24
  aiFallbackEditRepository;
23
25
  terminalCommandTemplateRepository;
26
+ preferenceProfileService;
24
27
  terminalService;
25
28
  terminalInstanceRepository;
26
29
  taskManager;
27
- constructor(db, workspaceService, workspaceWorktreeRepository, debugTargetRepository, debugServiceRepository, frameworkAnalysisResultRepository, debugRuntimeSessionRepository, portLeaseRepository, runtimeBindingRepository, aiFallbackEditRepository, terminalCommandTemplateRepository, terminalService, terminalInstanceRepository, taskManager = createTaskManager()) {
30
+ constructor(db, workspaceService, workspaceWorktreeRepository, debugTargetRepository, debugServiceRepository, frameworkAnalysisResultRepository, debugRuntimeSessionRepository, portLeaseRepository, runtimeBindingRepository, aiFallbackEditRepository, terminalCommandTemplateRepository, preferenceProfileService, terminalService, terminalInstanceRepository, taskManager = createTaskManager()) {
28
31
  this.db = db;
29
32
  this.workspaceService = workspaceService;
30
33
  this.workspaceWorktreeRepository = workspaceWorktreeRepository;
@@ -36,6 +39,7 @@ export class DebugTargetService {
36
39
  this.runtimeBindingRepository = runtimeBindingRepository;
37
40
  this.aiFallbackEditRepository = aiFallbackEditRepository;
38
41
  this.terminalCommandTemplateRepository = terminalCommandTemplateRepository;
42
+ this.preferenceProfileService = preferenceProfileService;
39
43
  this.terminalService = terminalService;
40
44
  this.terminalInstanceRepository = terminalInstanceRepository;
41
45
  this.taskManager = taskManager;
@@ -134,7 +138,94 @@ export class DebugTargetService {
134
138
  items: FRAMEWORK_COMPATIBILITY_MATRIX
135
139
  };
136
140
  }
137
- async createLaunchPlan(targetId, portRequests = []) {
141
+ async createLaunchPlan(targetId, portRequests = [], userId = null) {
142
+ return await this.createRegisteredTemplatePreviewPlan(targetId, portRequests, userId);
143
+ }
144
+ async run(input) {
145
+ const launchPlan = await this.createManagedRuntimeLaunchPlan(input.targetId, input.portRequests ?? [], input.userId);
146
+ if (!launchPlan.autoStartAllowed) {
147
+ const aiFallbackItem = launchPlan.services.find((item) => item.aiFallback?.eligible);
148
+ if (aiFallbackItem) {
149
+ await this.failRuntimePlan(launchPlan.runtimeSession.id, aiFallbackItem.failureStage ?? "ai_fallback_required", "当前服务需要进入受限 AI 兜底流程,自动运行已阻止", 409, "DEBUG_TARGET_AI_FALLBACK_REQUIRED");
150
+ }
151
+ await this.failRuntimePlan(launchPlan.runtimeSession.id, launchPlan.services.find((item) => item.failureStage)?.failureStage ?? "launch_requirements", "当前启动计划缺少必要的服务发现、HMR 或 callback 处理,暂不允许自动启动", 409, "DEBUG_TARGET_RUN_NOT_ALLOWED");
152
+ }
153
+ const startedServices = [];
154
+ try {
155
+ for (const item of launchPlan.services) {
156
+ const service = this.getServiceOrThrow(item.serviceId, input.targetId);
157
+ const analysis = this.frameworkAnalysisResultRepository
158
+ .listByTargetId(input.targetId)
159
+ .find((candidate) => candidate.id === item.frameworkAnalysisId) ?? null;
160
+ const terminal = await this.terminalService.createTerminal({
161
+ workspaceId: this.getTargetOrThrow(input.targetId).workspaceId,
162
+ name: `${service.name} 运行`,
163
+ cwd: service.cwd,
164
+ shell: input.shell,
165
+ runtimeType: input.runtimeType,
166
+ createdByUserId: input.userId,
167
+ env: {
168
+ ...service.env,
169
+ ...item.envPatch
170
+ },
171
+ debugRuntimeSessionId: launchPlan.runtimeSession.id,
172
+ debugTargetId: input.targetId,
173
+ debugServiceId: service.id,
174
+ frameworkAnalysisId: analysis?.id ?? null,
175
+ launcherSourceType: "debug_service",
176
+ launchStage: "command_dispatched",
177
+ failureStage: null,
178
+ adapterKind: item.adapterKind ?? null,
179
+ envPatchSummary: item.envPatch,
180
+ artifactRef: item.artifactRef
181
+ });
182
+ const commandLine = buildTemplateCommandLine(buildEphemeralTemplate(service, item, input.runtimeType ?? terminal.runtimeType), terminal.shell);
183
+ await this.terminalService.writeInput(terminal.id, `${commandLine}${getShellEnterSequence(terminal.shell)}`);
184
+ this.runtimeBindingRepository.update({
185
+ id: item.runtimeBindingId,
186
+ runtimeId: launchPlan.runtimeSession.id,
187
+ serviceId: service.id,
188
+ processInstanceId: terminal.id,
189
+ expectedPort: item.expectedPort,
190
+ leasedPort: item.leasedPort,
191
+ observedPort: null,
192
+ proxyPath: null,
193
+ status: "ALLOCATED",
194
+ updatedAt: nowIso()
195
+ });
196
+ startedServices.push({
197
+ serviceId: service.id,
198
+ processInstanceId: terminal.id,
199
+ terminalId: terminal.id,
200
+ leasedPort: item.leasedPort,
201
+ runtimeBindingId: item.runtimeBindingId
202
+ });
203
+ }
204
+ }
205
+ catch (error) {
206
+ for (const service of startedServices) {
207
+ try {
208
+ await this.terminalService.closeTerminal(service.terminalId);
209
+ }
210
+ catch {
211
+ // 失败回滚阶段只做尽力而为,避免覆盖原始错误。
212
+ }
213
+ }
214
+ await this.failRuntimePlan(launchPlan.runtimeSession.id, "command_execution", error instanceof Error ? error.message : "调试目标启动失败", 500, "DEBUG_TARGET_RUN_FAILED");
215
+ }
216
+ const updatedRuntimeSession = this.debugRuntimeSessionRepository.update({
217
+ ...launchPlan.runtimeSession,
218
+ status: "RUNNING",
219
+ failureStage: null,
220
+ startedAt: nowIso(),
221
+ updatedAt: nowIso()
222
+ });
223
+ return {
224
+ runtimeSession: updatedRuntimeSession ?? launchPlan.runtimeSession,
225
+ services: startedServices
226
+ };
227
+ }
228
+ async createManagedRuntimeLaunchPlan(targetId, portRequests, userId) {
138
229
  const target = this.getTargetOrThrow(targetId);
139
230
  const services = this.debugServiceRepository.listByTargetId(targetId);
140
231
  const analyses = this.frameworkAnalysisResultRepository.listByTargetId(targetId);
@@ -166,6 +257,7 @@ export class DebugTargetService {
166
257
  portRequests,
167
258
  portLeaseRepository: this.portLeaseRepository
168
259
  });
260
+ const portPoolConfig = this.resolvePortPoolConfig(userId);
169
261
  for (const service of services) {
170
262
  const analysis = analysisByServiceId.get(service.id) ?? analyses[0] ?? null;
171
263
  if (!analysis) {
@@ -176,7 +268,7 @@ export class DebugTargetService {
176
268
  });
177
269
  }
178
270
  const allocatedPort = requestedPorts.get(service.id)
179
- ?? await allocateManagedPort(service.role, this.portLeaseRepository);
271
+ ?? await allocateManagedPort(service.role, this.portLeaseRepository, portPoolConfig);
180
272
  const injectionPlan = resolveLaunchPlan({
181
273
  targetRootPath: target.rootPath,
182
274
  service,
@@ -269,90 +361,60 @@ export class DebugTargetService {
269
361
  services: planItems
270
362
  };
271
363
  }
272
- async run(input) {
273
- const launchPlan = await this.createLaunchPlan(input.targetId, input.portRequests ?? []);
274
- if (!launchPlan.autoStartAllowed) {
275
- const aiFallbackItem = launchPlan.services.find((item) => item.aiFallback?.eligible);
276
- if (aiFallbackItem) {
277
- await this.failRuntimePlan(launchPlan.runtimeSession.id, aiFallbackItem.failureStage ?? "ai_fallback_required", "当前服务需要进入受限 AI 兜底流程,自动运行已阻止", 409, "DEBUG_TARGET_AI_FALLBACK_REQUIRED");
278
- }
279
- await this.failRuntimePlan(launchPlan.runtimeSession.id, launchPlan.services.find((item) => item.failureStage)?.failureStage ?? "launch_requirements", "当前启动计划缺少必要的服务发现、HMR 或 callback 处理,暂不允许自动启动", 409, "DEBUG_TARGET_RUN_NOT_ALLOWED");
280
- }
281
- const startedServices = [];
282
- try {
283
- for (const item of launchPlan.services) {
284
- const service = this.getServiceOrThrow(item.serviceId, input.targetId);
285
- const analysis = this.frameworkAnalysisResultRepository
286
- .listByTargetId(input.targetId)
287
- .find((candidate) => candidate.id === item.frameworkAnalysisId) ?? null;
288
- const terminal = await this.terminalService.createTerminal({
289
- workspaceId: this.getTargetOrThrow(input.targetId).workspaceId,
290
- name: `${service.name} 运行`,
291
- cwd: service.cwd,
292
- shell: input.shell,
293
- runtimeType: input.runtimeType,
294
- createdByUserId: input.userId,
295
- env: {
296
- ...service.env,
297
- ...item.envPatch
298
- },
299
- debugRuntimeSessionId: launchPlan.runtimeSession.id,
300
- debugTargetId: input.targetId,
301
- debugServiceId: service.id,
302
- frameworkAnalysisId: analysis?.id ?? null,
303
- launcherSourceType: "debug_service",
304
- launchStage: "command_dispatched",
305
- failureStage: null,
306
- adapterKind: item.adapterKind ?? null,
307
- envPatchSummary: item.envPatch,
308
- artifactRef: item.artifactRef
309
- });
310
- const commandLine = buildTemplateCommandLine(buildEphemeralTemplate(service, item, input.runtimeType ?? terminal.runtimeType), terminal.shell);
311
- await this.terminalService.writeInput(terminal.id, `${commandLine}${getShellEnterSequence(terminal.shell)}`);
312
- this.runtimeBindingRepository.update({
313
- id: item.runtimeBindingId,
314
- runtimeId: launchPlan.runtimeSession.id,
315
- serviceId: service.id,
316
- processInstanceId: terminal.id,
317
- expectedPort: item.expectedPort,
318
- leasedPort: item.leasedPort,
319
- observedPort: null,
320
- proxyPath: null,
321
- status: "ALLOCATED",
322
- updatedAt: nowIso()
323
- });
324
- startedServices.push({
325
- serviceId: service.id,
326
- processInstanceId: terminal.id,
327
- terminalId: terminal.id,
328
- leasedPort: item.leasedPort,
329
- runtimeBindingId: item.runtimeBindingId
330
- });
331
- }
364
+ async createRegisteredTemplatePreviewPlan(targetId, portRequests, userId) {
365
+ const target = this.getTargetOrThrow(targetId);
366
+ const analyses = this.frameworkAnalysisResultRepository.listByTargetId(targetId);
367
+ if (analyses.length === 0) {
368
+ throw new AppError({
369
+ statusCode: 409,
370
+ errorCode: "DEBUG_TARGET_ANALYSIS_REQUIRED",
371
+ detail: "当前调试目标还没有可用的框架分析结果,请先执行分析"
372
+ });
332
373
  }
333
- catch (error) {
334
- for (const service of startedServices) {
335
- try {
336
- await this.terminalService.closeTerminal(service.terminalId);
337
- }
338
- catch {
339
- // 失败回滚阶段只做尽力而为,避免覆盖原始错误。
340
- }
341
- }
342
- await this.failRuntimePlan(launchPlan.runtimeSession.id, "command_execution", error instanceof Error ? error.message : "调试目标启动失败", 500, "DEBUG_TARGET_RUN_FAILED");
374
+ const timestamp = nowIso();
375
+ const runtimeSession = buildPreviewRuntimeSession(target.id, timestamp);
376
+ const templates = listRegisteredTemplatesForTarget(this.terminalCommandTemplateRepository.listByWorkspace(target.workspaceId), target.rootPath);
377
+ if (templates.length === 0) {
378
+ return await this.createManagedRuntimeLaunchPlan(targetId, portRequests, userId);
343
379
  }
344
- const updatedRuntimeSession = this.debugRuntimeSessionRepository.update({
345
- ...launchPlan.runtimeSession,
346
- status: "RUNNING",
347
- failureStage: null,
348
- startedAt: nowIso(),
349
- updatedAt: nowIso()
380
+ const services = this.debugServiceRepository.listByTargetId(targetId);
381
+ const runtimeStatuses = await discoverTemplateRuntimeStatuses(templates
382
+ .filter((template) => template.port !== null)
383
+ .map((template) => ({
384
+ templateId: template.id,
385
+ port: template.port
386
+ })));
387
+ const runtimeStatusByTemplateId = new Map(runtimeStatuses.map((item) => [item.templateId, item]));
388
+ const portPoolConfig = this.resolvePortPoolConfig(userId);
389
+ const requestedPorts = await resolveTemplateRequestedPorts({
390
+ targetRootPath: target.rootPath,
391
+ templates,
392
+ portRequests,
393
+ portLeaseRepository: this.portLeaseRepository
394
+ });
395
+ const templatePlans = await buildRegisteredTemplatePlanItems({
396
+ target,
397
+ templates,
398
+ services,
399
+ analyses,
400
+ runtimeStatusByTemplateId,
401
+ requestedPorts,
402
+ portPoolConfig,
403
+ portLeaseRepository: this.portLeaseRepository
350
404
  });
351
405
  return {
352
- runtimeSession: updatedRuntimeSession ?? launchPlan.runtimeSession,
353
- services: startedServices
406
+ runtimeSession,
407
+ targetId: target.id,
408
+ autoStartAllowed: templatePlans.length > 0 && templatePlans.every((item) => item.autoStartAllowed),
409
+ services: templatePlans
354
410
  };
355
411
  }
412
+ resolvePortPoolConfig(userId) {
413
+ if (!userId) {
414
+ return clonePortPoolConfig(DEFAULT_DEBUG_PORT_POOLS);
415
+ }
416
+ return clonePortPoolConfig(this.preferenceProfileService.getProfile(userId).debugPortPools);
417
+ }
356
418
  async handleTerminalExit(event) {
357
419
  const runtimeId = event.terminal.debugRuntimeSessionId ?? null;
358
420
  if (!runtimeId) {
@@ -1558,6 +1620,394 @@ function resolveMissingRequirements(analysis) {
1558
1620
  function isFrameworkEligible(level) {
1559
1621
  return level === "supported" || level === "conditional";
1560
1622
  }
1623
+ async function buildRegisteredTemplatePlanItems(input) {
1624
+ const matchedContexts = input.templates.map((template) => matchTemplateAnalysisContext(template, input.services, input.analyses));
1625
+ const configuredPortCount = new Map();
1626
+ const plannedPortByTemplateId = new Map();
1627
+ const usedPorts = new Set();
1628
+ for (const template of input.templates) {
1629
+ if (template.port !== null) {
1630
+ configuredPortCount.set(template.port, (configuredPortCount.get(template.port) ?? 0) + 1);
1631
+ }
1632
+ }
1633
+ for (const context of matchedContexts) {
1634
+ const requestedPort = input.requestedPorts.get(context.template.id);
1635
+ if (requestedPort !== undefined) {
1636
+ plannedPortByTemplateId.set(context.template.id, requestedPort);
1637
+ usedPorts.add(requestedPort);
1638
+ continue;
1639
+ }
1640
+ if (context.template.port === null) {
1641
+ plannedPortByTemplateId.set(context.template.id, null);
1642
+ continue;
1643
+ }
1644
+ const runtimeStatus = input.runtimeStatusByTemplateId.get(context.template.id) ?? null;
1645
+ const needsOrchestration = input.target.sourceType === "worktree"
1646
+ || (configuredPortCount.get(context.template.port) ?? 0) > 1
1647
+ || runtimeStatus?.occupied === true
1648
+ || input.portLeaseRepository.findActiveByPort(context.template.port, "tcp") !== null
1649
+ || usedPorts.has(context.template.port);
1650
+ if (!needsOrchestration) {
1651
+ plannedPortByTemplateId.set(context.template.id, context.template.port);
1652
+ usedPorts.add(context.template.port);
1653
+ continue;
1654
+ }
1655
+ const allocatedPort = await allocateManagedPort(context.role, input.portLeaseRepository, input.portPoolConfig, usedPorts);
1656
+ plannedPortByTemplateId.set(context.template.id, allocatedPort);
1657
+ usedPorts.add(allocatedPort);
1658
+ }
1659
+ const backendEndpoint = resolveRegisteredTemplateBackendEndpoint(matchedContexts, plannedPortByTemplateId);
1660
+ return matchedContexts.map((context) => {
1661
+ const plannedPort = plannedPortByTemplateId.get(context.template.id) ?? null;
1662
+ const runtimeBindingId = createId();
1663
+ const analysis = context.analysis;
1664
+ if (!analysis) {
1665
+ return {
1666
+ serviceId: context.template.id,
1667
+ role: context.role,
1668
+ frameworkAnalysisId: null,
1669
+ primaryFramework: null,
1670
+ compatibilityLevel: "unknown",
1671
+ adapterKind: context.template.adapterKind ?? null,
1672
+ injectionMode: context.template.injectionMode ?? null,
1673
+ command: context.template.command,
1674
+ args: context.template.args,
1675
+ envPatch: {},
1676
+ expectedPort: context.template.port,
1677
+ leasedPort: plannedPort,
1678
+ artifactRef: context.template.generatedArtifactRef ?? null,
1679
+ runtimeBindingId,
1680
+ portLeaseId: null,
1681
+ requiresServiceDiscoveryHandling: false,
1682
+ requiresHmrHandling: false,
1683
+ requiresCallbackHandling: false,
1684
+ failureStage: "framework_analysis_missing",
1685
+ adapterAttempts: [],
1686
+ aiFallback: null,
1687
+ missingRequirements: ["analysis"],
1688
+ autoStartAllowed: false
1689
+ };
1690
+ }
1691
+ if (plannedPort === null) {
1692
+ return {
1693
+ serviceId: context.template.id,
1694
+ role: context.role,
1695
+ frameworkAnalysisId: analysis.id,
1696
+ primaryFramework: analysis.primaryFramework ?? null,
1697
+ compatibilityLevel: analysis.compatibilityLevel,
1698
+ adapterKind: context.template.adapterKind ?? null,
1699
+ injectionMode: context.template.injectionMode ?? null,
1700
+ command: context.template.command,
1701
+ args: context.template.args,
1702
+ envPatch: {},
1703
+ expectedPort: context.template.port,
1704
+ leasedPort: null,
1705
+ artifactRef: context.template.generatedArtifactRef ?? null,
1706
+ runtimeBindingId,
1707
+ portLeaseId: null,
1708
+ requiresServiceDiscoveryHandling: analysis.requiresServiceDiscoveryHandling,
1709
+ requiresHmrHandling: analysis.requiresHmrHandling,
1710
+ requiresCallbackHandling: analysis.requiresCallbackHandling,
1711
+ failureStage: "port_unconfigured",
1712
+ adapterAttempts: [],
1713
+ aiFallback: null,
1714
+ missingRequirements: ["port"],
1715
+ autoStartAllowed: false
1716
+ };
1717
+ }
1718
+ const injectionPlan = resolveLaunchPlan({
1719
+ targetRootPath: input.target.rootPath,
1720
+ service: buildTemplateBackedDebugService(context.template, context.role, analysis.id),
1721
+ analysis,
1722
+ leasedPort: plannedPort
1723
+ });
1724
+ const serviceDiscoveryEnvPatch = buildServiceDiscoveryEnvPatch({
1725
+ role: context.role,
1726
+ analysis,
1727
+ backendEndpoint
1728
+ });
1729
+ const missingRequirements = resolveRegisteredTemplateMissingRequirements(analysis, {
1730
+ role: context.role,
1731
+ backendEndpoint
1732
+ });
1733
+ const envPatch = {
1734
+ ...injectionPlan.envPatch,
1735
+ ...serviceDiscoveryEnvPatch
1736
+ };
1737
+ const autoStartAllowed = isFrameworkEligible(analysis.compatibilityLevel)
1738
+ && missingRequirements.length === 0
1739
+ && injectionPlan.adapterKind !== "ai_fallback"
1740
+ && injectionPlan.adapterKind !== null;
1741
+ return {
1742
+ serviceId: context.template.id,
1743
+ role: context.role,
1744
+ frameworkAnalysisId: analysis.id,
1745
+ primaryFramework: analysis.primaryFramework ?? null,
1746
+ compatibilityLevel: analysis.compatibilityLevel,
1747
+ adapterKind: injectionPlan.adapterKind ?? null,
1748
+ injectionMode: injectionPlan.injectionMode ?? null,
1749
+ command: context.template.command,
1750
+ args: injectionPlan.args,
1751
+ envPatch,
1752
+ expectedPort: context.template.port,
1753
+ leasedPort: plannedPort,
1754
+ artifactRef: injectionPlan.artifactRef ?? context.template.generatedArtifactRef ?? null,
1755
+ runtimeBindingId,
1756
+ portLeaseId: null,
1757
+ requiresServiceDiscoveryHandling: analysis.requiresServiceDiscoveryHandling,
1758
+ requiresHmrHandling: analysis.requiresHmrHandling,
1759
+ requiresCallbackHandling: analysis.requiresCallbackHandling,
1760
+ failureStage: injectionPlan.failureStage,
1761
+ adapterAttempts: injectionPlan.adapterAttempts,
1762
+ aiFallback: injectionPlan.aiFallback,
1763
+ missingRequirements,
1764
+ autoStartAllowed
1765
+ };
1766
+ });
1767
+ }
1768
+ function buildPreviewRuntimeSession(targetId, timestamp) {
1769
+ return {
1770
+ id: `preview-${createId()}`,
1771
+ targetId,
1772
+ status: "PREPARING",
1773
+ failureStage: null,
1774
+ startedAt: null,
1775
+ stoppedAt: null,
1776
+ createdAt: timestamp,
1777
+ updatedAt: timestamp
1778
+ };
1779
+ }
1780
+ function listRegisteredTemplatesForTarget(templates, rootPath) {
1781
+ return templates
1782
+ .filter((template) => template.managedBySystem !== true)
1783
+ .filter((template) => isTemplateRelevantToRootPath(template, rootPath))
1784
+ .sort((left, right) => compareTemplatePriority(left, right, rootPath));
1785
+ }
1786
+ function matchTemplateAnalysisContext(template, services, analyses) {
1787
+ const analysisByServiceId = new Map(analyses
1788
+ .filter((analysis) => analysis.serviceId)
1789
+ .map((analysis) => [analysis.serviceId, analysis]));
1790
+ const normalizedTemplateCwd = path.resolve(template.cwd);
1791
+ const templateRole = inferTemplateRole(template);
1792
+ const matchedService = [...services]
1793
+ .sort((left, right) => compareServiceMatchPriority(left, right, normalizedTemplateCwd, templateRole))
1794
+ .find((service) => {
1795
+ const serviceCwd = path.resolve(service.cwd);
1796
+ return serviceCwd === normalizedTemplateCwd
1797
+ || isPathWithin(normalizedTemplateCwd, serviceCwd)
1798
+ || isPathWithin(serviceCwd, normalizedTemplateCwd);
1799
+ }) ?? null;
1800
+ const analysis = matchedService ? analysisByServiceId.get(matchedService.id) ?? null : null;
1801
+ return {
1802
+ template,
1803
+ role: matchedService?.role ?? templateRole,
1804
+ service: matchedService,
1805
+ analysis
1806
+ };
1807
+ }
1808
+ function compareServiceMatchPriority(left, right, templateCwd, templateRole) {
1809
+ const leftExact = Number(path.resolve(left.cwd) === templateCwd);
1810
+ const rightExact = Number(path.resolve(right.cwd) === templateCwd);
1811
+ if (rightExact !== leftExact) {
1812
+ return rightExact - leftExact;
1813
+ }
1814
+ const leftRoleMatch = Number(left.role === templateRole);
1815
+ const rightRoleMatch = Number(right.role === templateRole);
1816
+ if (rightRoleMatch !== leftRoleMatch) {
1817
+ return rightRoleMatch - leftRoleMatch;
1818
+ }
1819
+ const leftDistance = resolveRelativePathDistance(templateCwd, left.cwd);
1820
+ const rightDistance = resolveRelativePathDistance(templateCwd, right.cwd);
1821
+ if (leftDistance !== rightDistance) {
1822
+ return leftDistance - rightDistance;
1823
+ }
1824
+ return left.name.localeCompare(right.name);
1825
+ }
1826
+ function resolveRelativePathDistance(leftPath, rightPath) {
1827
+ const normalizedLeft = withTrailingSeparator(path.resolve(leftPath));
1828
+ const normalizedRight = withTrailingSeparator(path.resolve(rightPath));
1829
+ if (normalizedLeft === normalizedRight) {
1830
+ return 0;
1831
+ }
1832
+ if (normalizedLeft.startsWith(normalizedRight) || normalizedRight.startsWith(normalizedLeft)) {
1833
+ return 1;
1834
+ }
1835
+ return 2;
1836
+ }
1837
+ function inferTemplateRole(template) {
1838
+ const signal = [
1839
+ template.name,
1840
+ template.cwd,
1841
+ template.command,
1842
+ ...template.args
1843
+ ].join(" ").toLowerCase();
1844
+ if (signal.includes("frontend")
1845
+ || signal.includes("web")
1846
+ || signal.includes("ui")
1847
+ || signal.includes("vite")
1848
+ || signal.includes("next")
1849
+ || signal.includes("astro")
1850
+ || signal.includes("nuxt")
1851
+ || signal.includes("vue")
1852
+ || signal.includes("remix")) {
1853
+ return "frontend";
1854
+ }
1855
+ if (signal.includes("worker")) {
1856
+ return "worker";
1857
+ }
1858
+ if (signal.includes("mock")) {
1859
+ return "mock";
1860
+ }
1861
+ if (signal.includes("backend")
1862
+ || signal.includes("server")
1863
+ || signal.includes("api")
1864
+ || signal.includes("host")
1865
+ || signal.includes("express")
1866
+ || signal.includes("nestjs")
1867
+ || signal.includes("koa")
1868
+ || signal.includes("hono")
1869
+ || signal.includes("uvicorn")
1870
+ || signal.includes("flask")
1871
+ || signal.includes("django")
1872
+ || signal.includes("spring")
1873
+ || signal.includes("rails")
1874
+ || signal.includes("dotnet")) {
1875
+ return "backend";
1876
+ }
1877
+ return "custom";
1878
+ }
1879
+ function buildTemplateBackedDebugService(template, role, frameworkAnalysisId) {
1880
+ return {
1881
+ id: template.id,
1882
+ targetId: template.debugTargetId ?? "",
1883
+ role,
1884
+ name: template.name,
1885
+ cwd: template.cwd,
1886
+ command: template.command,
1887
+ args: template.args,
1888
+ env: template.env,
1889
+ defaultPortHint: template.port,
1890
+ protocol: "http",
1891
+ healthPath: null,
1892
+ adapterKind: template.adapterKind ?? null,
1893
+ frameworkAnalysisId,
1894
+ createdAt: template.createdAt,
1895
+ updatedAt: template.updatedAt
1896
+ };
1897
+ }
1898
+ function resolveRegisteredTemplateBackendEndpoint(contexts, plannedPortByTemplateId) {
1899
+ const candidate = contexts.find((context) => context.role === "backend" && plannedPortByTemplateId.get(context.template.id) !== null);
1900
+ if (!candidate) {
1901
+ return null;
1902
+ }
1903
+ return `http://127.0.0.1:${plannedPortByTemplateId.get(candidate.template.id)}`;
1904
+ }
1905
+ function buildServiceDiscoveryEnvPatch(input) {
1906
+ if (input.role === "backend" || !input.analysis.requiresServiceDiscoveryHandling || !input.backendEndpoint) {
1907
+ return {};
1908
+ }
1909
+ switch (input.analysis.primaryFramework) {
1910
+ case "vite":
1911
+ return {
1912
+ VITE_API_BASE_URL: input.backendEndpoint,
1913
+ API_BASE_URL: input.backendEndpoint
1914
+ };
1915
+ case "nextjs":
1916
+ return {
1917
+ NEXT_PUBLIC_API_BASE_URL: input.backendEndpoint,
1918
+ API_BASE_URL: input.backendEndpoint
1919
+ };
1920
+ case "cra":
1921
+ return {
1922
+ REACT_APP_API_BASE_URL: input.backendEndpoint,
1923
+ API_BASE_URL: input.backendEndpoint
1924
+ };
1925
+ case "astro":
1926
+ return {
1927
+ PUBLIC_API_BASE_URL: input.backendEndpoint,
1928
+ API_BASE_URL: input.backendEndpoint
1929
+ };
1930
+ case "nuxt":
1931
+ return {
1932
+ NUXT_PUBLIC_API_BASE_URL: input.backendEndpoint,
1933
+ API_BASE_URL: input.backendEndpoint
1934
+ };
1935
+ case "vue-cli":
1936
+ return {
1937
+ VUE_APP_API_BASE_URL: input.backendEndpoint,
1938
+ API_BASE_URL: input.backendEndpoint
1939
+ };
1940
+ case "remix":
1941
+ return {
1942
+ API_BASE_URL: input.backendEndpoint,
1943
+ PUBLIC_API_BASE_URL: input.backendEndpoint
1944
+ };
1945
+ default:
1946
+ return {
1947
+ API_BASE_URL: input.backendEndpoint,
1948
+ BACKEND_BASE_URL: input.backendEndpoint
1949
+ };
1950
+ }
1951
+ }
1952
+ function resolveRegisteredTemplateMissingRequirements(analysis, input) {
1953
+ const missing = [];
1954
+ if (analysis.requiresServiceDiscoveryHandling
1955
+ && input.role !== "backend"
1956
+ && !input.backendEndpoint) {
1957
+ missing.push("service_discovery");
1958
+ }
1959
+ if (analysis.requiresCallbackHandling) {
1960
+ missing.push("callback");
1961
+ }
1962
+ return missing;
1963
+ }
1964
+ async function resolveTemplateRequestedPorts(input) {
1965
+ const requestedPorts = new Map();
1966
+ const usedPorts = new Map();
1967
+ for (const request of input.portRequests) {
1968
+ validatePortRequestShape(request);
1969
+ const matches = input.templates.filter((template) => matchesTemplatePortRequest(template, input.targetRootPath, request));
1970
+ if (matches.length === 0) {
1971
+ throw new AppError({
1972
+ statusCode: 400,
1973
+ errorCode: "DEBUG_TARGET_PORT_REQUEST_UNMATCHED",
1974
+ detail: `显式端口请求没有匹配到任何启动项:${describePortRequest(request)}`,
1975
+ field: "portRequests"
1976
+ });
1977
+ }
1978
+ if (matches.length > 1) {
1979
+ throw new AppError({
1980
+ statusCode: 409,
1981
+ errorCode: "DEBUG_TARGET_PORT_REQUEST_AMBIGUOUS",
1982
+ detail: `显式端口请求匹配到了多个启动项:${describePortRequest(request)}`,
1983
+ field: "portRequests"
1984
+ });
1985
+ }
1986
+ const matchedTemplate = matches[0];
1987
+ const previousPort = requestedPorts.get(matchedTemplate.id);
1988
+ if (previousPort !== undefined && previousPort !== request.port) {
1989
+ throw new AppError({
1990
+ statusCode: 409,
1991
+ errorCode: "DEBUG_TARGET_PORT_REQUEST_CONFLICT",
1992
+ detail: `同一个启动项收到了多个不同的显式端口请求:${matchedTemplate.name}`,
1993
+ field: "portRequests"
1994
+ });
1995
+ }
1996
+ const occupiedBy = usedPorts.get(request.port);
1997
+ if (occupiedBy && occupiedBy !== matchedTemplate.id) {
1998
+ throw new AppError({
1999
+ statusCode: 409,
2000
+ errorCode: "DEBUG_TARGET_PORT_REQUEST_CONFLICT",
2001
+ detail: `多个启动项请求了同一个端口:${request.port}`,
2002
+ field: "portRequests"
2003
+ });
2004
+ }
2005
+ await ensureRequestedPortAvailable(request.port, inferTemplateRole(matchedTemplate), input.portLeaseRepository);
2006
+ requestedPorts.set(matchedTemplate.id, request.port);
2007
+ usedPorts.set(request.port, matchedTemplate.id);
2008
+ }
2009
+ return requestedPorts;
2010
+ }
1561
2011
  async function resolveRequestedPorts(input) {
1562
2012
  const requestedPorts = new Map();
1563
2013
  const usedPorts = new Map();
@@ -1605,10 +2055,12 @@ async function resolveRequestedPorts(input) {
1605
2055
  }
1606
2056
  return requestedPorts;
1607
2057
  }
1608
- async function allocateManagedPort(role, portLeaseRepository) {
1609
- const startPort = resolvePortRangeStart(role);
1610
- for (let offset = 0; offset < 200; offset += 1) {
1611
- const port = startPort + offset;
2058
+ async function allocateManagedPort(role, portLeaseRepository, portPoolConfig, usedPorts = new Set()) {
2059
+ const range = portPoolConfig;
2060
+ for (let port = range.start; port <= range.end; port += 1) {
2061
+ if (usedPorts.has(port)) {
2062
+ continue;
2063
+ }
1612
2064
  if (portLeaseRepository.findActiveByPort(port, "tcp")) {
1613
2065
  continue;
1614
2066
  }
@@ -1620,7 +2072,7 @@ async function allocateManagedPort(role, portLeaseRepository) {
1620
2072
  throw new AppError({
1621
2073
  statusCode: 409,
1622
2074
  errorCode: "PORT_LEASE_EXHAUSTED",
1623
- detail: `当前服务角色没有可分配的空闲端口:${role}`
2075
+ detail: `当前服务角色没有可分配的空闲端口:${role}(范围 ${range.start}-${range.end})`
1624
2076
  });
1625
2077
  }
1626
2078
  async function ensureRequestedPortAvailable(port, role, portLeaseRepository) {
@@ -1641,20 +2093,6 @@ async function ensureRequestedPortAvailable(port, role, portLeaseRepository) {
1641
2093
  });
1642
2094
  }
1643
2095
  }
1644
- function resolvePortRangeStart(role) {
1645
- switch (role) {
1646
- case "frontend":
1647
- return 43000;
1648
- case "backend":
1649
- return 44000;
1650
- case "worker":
1651
- return 45000;
1652
- case "mock":
1653
- return 46000;
1654
- default:
1655
- return 47000;
1656
- }
1657
- }
1658
2096
  async function isPortAvailable(port) {
1659
2097
  return await new Promise((resolve) => {
1660
2098
  const server = net.createServer();
@@ -1746,6 +2184,31 @@ function matchesPortRequest(service, targetRootPath, request) {
1746
2184
  }
1747
2185
  return true;
1748
2186
  }
2187
+ function matchesTemplatePortRequest(template, targetRootPath, request) {
2188
+ if (request.serviceId?.trim() && request.serviceId.trim() !== template.id) {
2189
+ return false;
2190
+ }
2191
+ if (request.role?.trim() && request.role.trim() !== inferTemplateRole(template)) {
2192
+ return false;
2193
+ }
2194
+ if (request.name?.trim() && request.name.trim() !== template.name) {
2195
+ return false;
2196
+ }
2197
+ if (request.command?.trim() && request.command.trim() !== template.command) {
2198
+ return false;
2199
+ }
2200
+ if (request.cwd?.trim()) {
2201
+ const selectorPath = normalizePortRequestCwd(request.cwd, targetRootPath);
2202
+ const templatePath = path.resolve(template.cwd);
2203
+ if (selectorPath !== templatePath) {
2204
+ return false;
2205
+ }
2206
+ }
2207
+ return true;
2208
+ }
2209
+ function clonePortPoolConfig(config) {
2210
+ return { ...config };
2211
+ }
1749
2212
  function normalizePortRequestCwd(value, targetRootPath) {
1750
2213
  const trimmed = value?.trim() || "";
1751
2214
  if (!trimmed) {