agent-tower 0.4.15 → 0.4.16-beta.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/dist/app.test.js +2 -0
- package/dist/app.test.js.map +1 -1
- package/dist/core/event-bus.d.ts +2 -0
- package/dist/core/event-bus.d.ts.map +1 -1
- package/dist/core/event-bus.js.map +1 -1
- package/dist/executors/__tests__/codex.executor.test.d.ts +2 -0
- package/dist/executors/__tests__/codex.executor.test.d.ts.map +1 -0
- package/dist/executors/__tests__/codex.executor.test.js +28 -0
- package/dist/executors/__tests__/codex.executor.test.js.map +1 -0
- package/dist/executors/codex.executor.d.ts +1 -0
- package/dist/executors/codex.executor.d.ts.map +1 -1
- package/dist/executors/codex.executor.js +19 -1
- package/dist/executors/codex.executor.js.map +1 -1
- package/dist/git/git-cli.d.ts +18 -1
- package/dist/git/git-cli.d.ts.map +1 -1
- package/dist/git/git-cli.js +17 -1
- package/dist/git/git-cli.js.map +1 -1
- package/dist/git/worktree.manager.d.ts +29 -2
- package/dist/git/worktree.manager.d.ts.map +1 -1
- package/dist/git/worktree.manager.js +137 -16
- package/dist/git/worktree.manager.js.map +1 -1
- package/dist/git/worktree.manager.test.d.ts +2 -0
- package/dist/git/worktree.manager.test.d.ts.map +1 -0
- package/dist/git/worktree.manager.test.js +104 -0
- package/dist/git/worktree.manager.test.js.map +1 -0
- package/dist/mcp/context.d.ts +3 -0
- package/dist/mcp/context.d.ts.map +1 -1
- package/dist/mcp/context.js +10 -1
- package/dist/mcp/context.js.map +1 -1
- package/dist/mcp/http-client.d.ts +24 -1
- package/dist/mcp/http-client.d.ts.map +1 -1
- package/dist/mcp/http-client.js +37 -3
- package/dist/mcp/http-client.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +190 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/middleware/tunnel-auth.d.ts.map +1 -1
- package/dist/middleware/tunnel-auth.js +2 -0
- package/dist/middleware/tunnel-auth.js.map +1 -1
- package/dist/output/__tests__/codex-parser.test.d.ts +2 -0
- package/dist/output/__tests__/codex-parser.test.d.ts.map +1 -0
- package/dist/output/__tests__/codex-parser.test.js +148 -0
- package/dist/output/__tests__/codex-parser.test.js.map +1 -0
- package/dist/output/codex-parser.d.ts +12 -0
- package/dist/output/codex-parser.d.ts.map +1 -1
- package/dist/output/codex-parser.js +129 -12
- package/dist/output/codex-parser.js.map +1 -1
- package/dist/routes/__tests__/attachments.test.d.ts +2 -0
- package/dist/routes/__tests__/attachments.test.d.ts.map +1 -0
- package/dist/routes/__tests__/attachments.test.js +86 -0
- package/dist/routes/__tests__/attachments.test.js.map +1 -0
- package/dist/routes/__tests__/filesystem.test.d.ts +2 -0
- package/dist/routes/__tests__/filesystem.test.d.ts.map +1 -0
- package/dist/routes/__tests__/filesystem.test.js +80 -0
- package/dist/routes/__tests__/filesystem.test.js.map +1 -0
- package/dist/routes/__tests__/previews.test.d.ts +2 -0
- package/dist/routes/__tests__/previews.test.d.ts.map +1 -0
- package/dist/routes/__tests__/previews.test.js +89 -0
- package/dist/routes/__tests__/previews.test.js.map +1 -0
- package/dist/routes/__tests__/tasks.test.d.ts +2 -0
- package/dist/routes/__tests__/tasks.test.d.ts.map +1 -0
- package/dist/routes/__tests__/tasks.test.js +72 -0
- package/dist/routes/__tests__/tasks.test.js.map +1 -0
- package/dist/routes/attachments.d.ts.map +1 -1
- package/dist/routes/attachments.js +36 -16
- package/dist/routes/attachments.js.map +1 -1
- package/dist/routes/filesystem.d.ts.map +1 -1
- package/dist/routes/filesystem.js +24 -3
- package/dist/routes/filesystem.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +6 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/previews.d.ts +6 -0
- package/dist/routes/previews.d.ts.map +1 -0
- package/dist/routes/previews.js +413 -0
- package/dist/routes/previews.js.map +1 -0
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +1 -0
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/system.d.ts.map +1 -1
- package/dist/routes/system.js +35 -1
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/tasks.js +2 -2
- package/dist/routes/tasks.js.map +1 -1
- package/dist/routes/team-runs.d.ts +11 -0
- package/dist/routes/team-runs.d.ts.map +1 -0
- package/dist/routes/team-runs.js +309 -0
- package/dist/routes/team-runs.js.map +1 -0
- package/dist/routes/tunnel.d.ts.map +1 -1
- package/dist/routes/tunnel.js +20 -0
- package/dist/routes/tunnel.js.map +1 -1
- package/dist/routes/workspaces.d.ts.map +1 -1
- package/dist/routes/workspaces.js +15 -1
- package/dist/routes/workspaces.js.map +1 -1
- package/dist/services/__tests__/preview.service.test.d.ts +2 -0
- package/dist/services/__tests__/preview.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/preview.service.test.js +29 -0
- package/dist/services/__tests__/preview.service.test.js.map +1 -0
- package/dist/services/__tests__/session-manager.team-run.test.d.ts +2 -0
- package/dist/services/__tests__/session-manager.team-run.test.d.ts.map +1 -0
- package/dist/services/__tests__/session-manager.team-run.test.js +286 -0
- package/dist/services/__tests__/session-manager.team-run.test.js.map +1 -0
- package/dist/services/__tests__/task.service.test.d.ts +2 -0
- package/dist/services/__tests__/task.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/task.service.test.js +65 -0
- package/dist/services/__tests__/task.service.test.js.map +1 -0
- package/dist/services/__tests__/team-lock.service.test.d.ts +2 -0
- package/dist/services/__tests__/team-lock.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/team-lock.service.test.js +81 -0
- package/dist/services/__tests__/team-lock.service.test.js.map +1 -0
- package/dist/services/__tests__/team-reconciler.service.test.d.ts +2 -0
- package/dist/services/__tests__/team-reconciler.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/team-reconciler.service.test.js +1536 -0
- package/dist/services/__tests__/team-reconciler.service.test.js.map +1 -0
- package/dist/services/__tests__/team-run.service.test.d.ts +2 -0
- package/dist/services/__tests__/team-run.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/team-run.service.test.js +699 -0
- package/dist/services/__tests__/team-run.service.test.js.map +1 -0
- package/dist/services/__tests__/team-scheduler.service.test.d.ts +2 -0
- package/dist/services/__tests__/team-scheduler.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/team-scheduler.service.test.js +1688 -0
- package/dist/services/__tests__/team-scheduler.service.test.js.map +1 -0
- package/dist/services/__tests__/tunnel.service.test.d.ts +2 -0
- package/dist/services/__tests__/tunnel.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/tunnel.service.test.js +138 -0
- package/dist/services/__tests__/tunnel.service.test.js.map +1 -0
- package/dist/services/__tests__/workspace.service.test.d.ts +2 -0
- package/dist/services/__tests__/workspace.service.test.d.ts.map +1 -0
- package/dist/services/__tests__/workspace.service.test.js +695 -0
- package/dist/services/__tests__/workspace.service.test.js.map +1 -0
- package/dist/services/attachment-context.d.ts +3 -0
- package/dist/services/attachment-context.d.ts.map +1 -0
- package/dist/services/attachment-context.js +34 -0
- package/dist/services/attachment-context.js.map +1 -0
- package/dist/services/preview.service.d.ts +19 -0
- package/dist/services/preview.service.d.ts.map +1 -0
- package/dist/services/preview.service.js +147 -0
- package/dist/services/preview.service.js.map +1 -0
- package/dist/services/project.service.d.ts +2 -0
- package/dist/services/project.service.d.ts.map +1 -1
- package/dist/services/project.service.js +87 -18
- package/dist/services/project.service.js.map +1 -1
- package/dist/services/session-manager.d.ts +43 -1
- package/dist/services/session-manager.d.ts.map +1 -1
- package/dist/services/session-manager.js +110 -2
- package/dist/services/session-manager.js.map +1 -1
- package/dist/services/task.service.d.ts +6 -0
- package/dist/services/task.service.d.ts.map +1 -1
- package/dist/services/task.service.js +15 -3
- package/dist/services/task.service.js.map +1 -1
- package/dist/services/team-lock.service.d.ts +25 -0
- package/dist/services/team-lock.service.d.ts.map +1 -0
- package/dist/services/team-lock.service.js +56 -0
- package/dist/services/team-lock.service.js.map +1 -0
- package/dist/services/team-reconciler.service.d.ts +44 -0
- package/dist/services/team-reconciler.service.d.ts.map +1 -0
- package/dist/services/team-reconciler.service.js +286 -0
- package/dist/services/team-reconciler.service.js.map +1 -0
- package/dist/services/team-run-events.d.ts +13 -0
- package/dist/services/team-run-events.d.ts.map +1 -0
- package/dist/services/team-run-events.js +27 -0
- package/dist/services/team-run-events.js.map +1 -0
- package/dist/services/team-run.service.d.ts +92 -0
- package/dist/services/team-run.service.d.ts.map +1 -0
- package/dist/services/team-run.service.js +835 -0
- package/dist/services/team-run.service.js.map +1 -0
- package/dist/services/team-scheduler.service.d.ts +104 -0
- package/dist/services/team-scheduler.service.d.ts.map +1 -0
- package/dist/services/team-scheduler.service.js +843 -0
- package/dist/services/team-scheduler.service.js.map +1 -0
- package/dist/services/tunnel.service.d.ts +31 -5
- package/dist/services/tunnel.service.d.ts.map +1 -1
- package/dist/services/tunnel.service.js +293 -32
- package/dist/services/tunnel.service.js.map +1 -1
- package/dist/services/workspace.service.d.ts +161 -7
- package/dist/services/workspace.service.d.ts.map +1 -1
- package/dist/services/workspace.service.js +396 -51
- package/dist/services/workspace.service.js.map +1 -1
- package/dist/socket/events.d.ts +1 -1
- package/dist/socket/events.d.ts.map +1 -1
- package/dist/socket/events.js.map +1 -1
- package/dist/socket/socket-gateway.d.ts.map +1 -1
- package/dist/socket/socket-gateway.js +5 -1
- package/dist/socket/socket-gateway.js.map +1 -1
- package/dist/web/assets/AgentDemoPage-Bf6labVB.js +1 -0
- package/dist/web/assets/{DemoPage-XwuS8vNB.js → DemoPage-DlfG47rV.js} +3 -3
- package/dist/web/assets/{GeneralSettingsPage-CliIgpwf.js → GeneralSettingsPage-DefqwzVn.js} +1 -1
- package/dist/web/assets/MemberAvatar-DVw_TedB.js +1 -0
- package/dist/web/assets/NotificationSettingsPage-C9h1U1Za.js +1 -0
- package/dist/web/assets/{ProfileSettingsPage-CkU_kZKG.js → ProfileSettingsPage-BkZE2yVP.js} +1 -1
- package/dist/web/assets/ProjectKanbanPage-B1Ckl1uY.js +89 -0
- package/dist/web/assets/ProjectSettingsPage-ByZ13awb.js +2 -0
- package/dist/web/assets/{ProviderSettingsPage-CfvdeoEU.js → ProviderSettingsPage-DSQYe8B6.js} +12 -12
- package/dist/web/assets/TeamSettingsPage-DUukJ_Ih.js +1 -0
- package/dist/web/assets/agent-tower-logo-COx9gy77.png +0 -0
- package/dist/web/assets/{button-BWFTEdOr.js → button-Bpm98eOV.js} +1 -1
- package/dist/web/assets/{chevron-down-CuPdBAx-.js → chevron-down-DSKKXCi8.js} +1 -1
- package/dist/web/assets/{chevron-right-Cs8vYTMn.js → chevron-right-CZdDV9GU.js} +1 -1
- package/dist/web/assets/chevron-up-gnnlwvYe.js +1 -0
- package/dist/web/assets/{circle-check-BXZTzqw0.js → circle-check-DeD_VuLK.js} +1 -1
- package/dist/web/assets/{code-block-OCS4YCEC-BxUpvXK_.js → code-block-OCS4YCEC-BrGjkdjS.js} +1 -1
- package/dist/web/assets/{confirm-dialog-CDLHRthd.js → confirm-dialog-CEVVvAcE.js} +1 -1
- package/dist/web/assets/folder-picker-ZBQlFEWL.js +1 -0
- package/dist/web/assets/index-B5g4V0NU.js +13 -0
- package/dist/web/assets/index-ltjI8o6A.css +1 -0
- package/dist/web/assets/loader-circle-GMfBClX0.js +1 -0
- package/dist/web/assets/{log-adapter-CeKrvZcz.js → log-adapter-DKKM3sxS.js} +1 -1
- package/dist/web/assets/{mermaid-NOHMQCX5-BOSwJqP0.js → mermaid-NOHMQCX5-D5USvUiZ.js} +44 -44
- package/dist/web/assets/modal-JMpuh-LG.js +1 -0
- package/dist/web/assets/{pencil-BMxBxIhw.js → pencil-QrCW47nn.js} +1 -1
- package/dist/web/assets/{select-BUmRG0LY.js → select-CINRzLiE.js} +1 -1
- package/dist/web/assets/upload-vFxZxKHo.js +1 -0
- package/dist/web/assets/{use-profiles-C1vlPE-2.js → use-profiles-SrVWPYv0.js} +1 -1
- package/dist/web/assets/{use-providers-Cdxr4Jbz.js → use-providers-BihMydl0.js} +1 -1
- package/dist/web/avatars/presets/avatar-preset-01-developer.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-02-architect.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-03-tester.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-04-devops.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-05-data-scientist.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-06-frontend.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-07-backend.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-08-security.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-09-project-manager.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-10-product-manager.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-11-scrum-master.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-12-tech-lead.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-13-coordinator.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-14-mentor.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-15-reviewer.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-16-ui-designer.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-17-ux-researcher.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-18-documenter.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-19-translator.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-20-analyst.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-21-consultant.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-22-creative-director.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-23-support.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-24-assistant.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-25-robot.png +0 -0
- package/dist/web/avatars/presets/avatar-preset-grid.png +0 -0
- package/dist/web/index.html +2 -2
- package/node_modules/@agent-tower/shared/dist/socket/events.d.ts +10 -0
- package/node_modules/@agent-tower/shared/dist/socket/events.d.ts.map +1 -1
- package/node_modules/@agent-tower/shared/dist/socket/events.js +1 -0
- package/node_modules/@agent-tower/shared/dist/socket/events.js.map +1 -1
- package/node_modules/@agent-tower/shared/dist/types.d.ts +161 -0
- package/node_modules/@agent-tower/shared/dist/types.d.ts.map +1 -1
- package/node_modules/@agent-tower/shared/dist/types.js.map +1 -1
- package/node_modules/@prisma/client/.prisma/client/default.d.ts +1 -0
- package/node_modules/@prisma/client/.prisma/client/default.js +1 -0
- package/node_modules/@prisma/client/.prisma/client/edge.d.ts +1 -0
- package/node_modules/@prisma/client/.prisma/client/edge.js +396 -0
- package/node_modules/@prisma/client/.prisma/client/index-browser.js +385 -0
- package/node_modules/@prisma/client/.prisma/client/index.d.ts +26996 -0
- package/node_modules/@prisma/client/.prisma/client/index.js +421 -0
- package/node_modules/@prisma/client/.prisma/client/libquery_engine-darwin-arm64.dylib.node +0 -0
- package/node_modules/@prisma/client/.prisma/client/package.json +97 -0
- package/node_modules/@prisma/client/.prisma/client/query_engine-windows.dll.node +0 -0
- package/node_modules/@prisma/client/.prisma/client/schema.prisma +296 -0
- package/node_modules/@prisma/client/.prisma/client/wasm.d.ts +1 -0
- package/node_modules/@prisma/client/.prisma/client/wasm.js +385 -0
- package/node_modules/@prisma/client/package.json +3 -2
- package/package.json +2 -1
- package/prisma/migrations/20260515000000_add_workspace_preview_target/migration.sql +2 -0
- package/prisma/migrations/20260518000000_add_team_run_collaboration/migration.sql +150 -0
- package/prisma/migrations/20260522000000_add_team_member_session_policy/migration.sql +2 -0
- package/prisma/migrations/20260526000000_add_team_run_main_and_dedicated_workspaces/migration.sql +21 -0
- package/prisma/schema.prisma +147 -1
- package/dist/web/assets/AgentDemoPage-ClnGPAV9.js +0 -1
- package/dist/web/assets/NotificationSettingsPage-y3vhVgPv.js +0 -1
- package/dist/web/assets/ProjectKanbanPage-BddzfZRV.js +0 -87
- package/dist/web/assets/ProjectSettingsPage-B6xhbziO.js +0 -2
- package/dist/web/assets/circle-alert-EUyZcWhp.js +0 -1
- package/dist/web/assets/folder-picker-CUbhsnhi.js +0 -1
- package/dist/web/assets/index-BGvfX18x.css +0 -1
- package/dist/web/assets/index-CHN8jahE.js +0 -13
- package/dist/web/assets/loader-circle-BHzDVpxt.js +0 -1
- package/dist/web/assets/modal-D_AU4URz.js +0 -1
- package/dist/web/assets/use-projects-Bcd5hIOY.js +0 -1
|
@@ -1,21 +1,75 @@
|
|
|
1
1
|
import { prisma } from '../utils/index.js';
|
|
2
2
|
import { WorkspaceStatus, TaskStatus, SessionStatus, SessionPurpose } from '../types/index.js';
|
|
3
3
|
import { WorktreeManager } from '../git/worktree.manager.js';
|
|
4
|
-
import { execGit } from '../git/git-cli.js';
|
|
4
|
+
import { execGit, MergeConflictError } from '../git/git-cli.js';
|
|
5
5
|
import { NotFoundError, ServiceError } from '../errors.js';
|
|
6
6
|
import { getSessionManager, getEventBus } from '../core/container.js';
|
|
7
7
|
import { copyProjectFiles } from './copy-files.service.js';
|
|
8
|
+
import { defaultTeamLockService } from './team-lock.service.js';
|
|
8
9
|
import { exec } from 'node:child_process';
|
|
9
10
|
import { promisify } from 'node:util';
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
10
12
|
import fs from 'node:fs/promises';
|
|
13
|
+
import path from 'node:path';
|
|
11
14
|
import { ensureProjectIsMutable } from './project-guards.js';
|
|
12
15
|
const DEFAULT_IDLE_THRESHOLD_HOURS = 24;
|
|
16
|
+
const WORKSPACE_READY_RETRY_COUNT = 20;
|
|
17
|
+
const WORKSPACE_READY_RETRY_DELAY_MS = 50;
|
|
13
18
|
const execAsync = promisify(exec);
|
|
14
19
|
/** 过滤条件:只返回用户可见的 CHAT session */
|
|
15
20
|
const visibleSessionsFilter = { where: { purpose: { not: SessionPurpose.COMMIT_MSG } } };
|
|
21
|
+
const activeSessionStatuses = [SessionStatus.PENDING, SessionStatus.RUNNING];
|
|
22
|
+
const finalChildWorkspaceStatuses = [WorkspaceStatus.MERGED, WorkspaceStatus.ABANDONED];
|
|
23
|
+
function normalizeCreateOptions(input) {
|
|
24
|
+
if (typeof input === 'string') {
|
|
25
|
+
return {
|
|
26
|
+
branchName: input,
|
|
27
|
+
branchNamePrefix: '',
|
|
28
|
+
startPoint: null,
|
|
29
|
+
parentWorkspaceId: null,
|
|
30
|
+
ownerMemberId: null,
|
|
31
|
+
reuseInactive: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
branchName: input?.branchName ?? '',
|
|
36
|
+
branchNamePrefix: input?.branchNamePrefix ?? '',
|
|
37
|
+
startPoint: input?.startPoint ?? null,
|
|
38
|
+
parentWorkspaceId: input?.parentWorkspaceId ?? null,
|
|
39
|
+
ownerMemberId: input?.ownerMemberId ?? null,
|
|
40
|
+
reuseInactive: input?.reuseInactive ?? true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function isUniqueConstraintError(error) {
|
|
44
|
+
return typeof error === 'object'
|
|
45
|
+
&& error !== null
|
|
46
|
+
&& 'code' in error
|
|
47
|
+
&& error.code === 'P2002';
|
|
48
|
+
}
|
|
49
|
+
function branchFromOptions(workspaceId, options) {
|
|
50
|
+
if (options.branchName) {
|
|
51
|
+
return options.branchName;
|
|
52
|
+
}
|
|
53
|
+
if (options.branchNamePrefix) {
|
|
54
|
+
return `${options.branchNamePrefix}/${workspaceId.slice(0, 8)}`;
|
|
55
|
+
}
|
|
56
|
+
return `at/${workspaceId.slice(0, 8)}`;
|
|
57
|
+
}
|
|
58
|
+
function teamRunBranchPrefix(teamRunId) {
|
|
59
|
+
return `at/team/${teamRunId.slice(0, 8)}`;
|
|
60
|
+
}
|
|
61
|
+
function sleep(ms) {
|
|
62
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
63
|
+
}
|
|
16
64
|
export class WorkspaceService {
|
|
65
|
+
lockService;
|
|
66
|
+
static mainWorkspaceClaims = new Map();
|
|
67
|
+
static dedicatedWorkspaceClaims = new Map();
|
|
17
68
|
sessionService = getSessionManager();
|
|
18
69
|
eventBus = getEventBus();
|
|
70
|
+
constructor(lockService = defaultTeamLockService) {
|
|
71
|
+
this.lockService = lockService;
|
|
72
|
+
}
|
|
19
73
|
getBaseBranch(workspace) {
|
|
20
74
|
return workspace.baseBranch || workspace.task.project.mainBranch;
|
|
21
75
|
}
|
|
@@ -50,7 +104,8 @@ export class WorkspaceService {
|
|
|
50
104
|
* - 创建后自动将关联 Task 状态改为 IN_PROGRESS
|
|
51
105
|
* - 失败时回滚已创建的数据库记录
|
|
52
106
|
*/
|
|
53
|
-
async create(taskId,
|
|
107
|
+
async create(taskId, branchNameOrOptions) {
|
|
108
|
+
const options = normalizeCreateOptions(branchNameOrOptions);
|
|
54
109
|
const task = await prisma.task.findUnique({
|
|
55
110
|
where: { id: taskId },
|
|
56
111
|
include: { project: true },
|
|
@@ -61,34 +116,29 @@ export class WorkspaceService {
|
|
|
61
116
|
ensureProjectIsMutable(task.project, 'create workspaces');
|
|
62
117
|
const worktreeManager = new WorktreeManager(task.project.repoPath);
|
|
63
118
|
// 查找可复用的 MERGED 或 HIBERNATED workspace
|
|
64
|
-
if (!branchName) {
|
|
119
|
+
if (!options.branchName && options.reuseInactive) {
|
|
65
120
|
const reusableWorkspace = await prisma.workspace.findFirst({
|
|
66
121
|
where: {
|
|
67
122
|
taskId,
|
|
123
|
+
parentWorkspaceId: options.parentWorkspaceId,
|
|
124
|
+
ownerMemberId: options.ownerMemberId,
|
|
68
125
|
status: { in: [WorkspaceStatus.MERGED, WorkspaceStatus.HIBERNATED] },
|
|
69
126
|
},
|
|
70
127
|
orderBy: { updatedAt: 'desc' },
|
|
71
128
|
});
|
|
72
129
|
if (reusableWorkspace) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const updated = await prisma.workspace.update({
|
|
77
|
-
where: { id: reusableWorkspace.id },
|
|
78
|
-
data: {
|
|
79
|
-
status: WorkspaceStatus.ACTIVE,
|
|
80
|
-
worktreePath,
|
|
81
|
-
hibernatedAt: null,
|
|
82
|
-
},
|
|
83
|
-
include: { sessions: true, task: { include: { project: true } } },
|
|
130
|
+
return this.restoreInactiveWorkspace({
|
|
131
|
+
...reusableWorkspace,
|
|
132
|
+
task: { ...task, project: task.project },
|
|
84
133
|
});
|
|
85
|
-
return updated;
|
|
86
134
|
}
|
|
87
135
|
}
|
|
88
136
|
// 先在数据库创建记录以获取 ID(用于生成默认分支名)
|
|
89
137
|
const workspace = await prisma.workspace.create({
|
|
90
138
|
data: {
|
|
91
139
|
taskId,
|
|
140
|
+
parentWorkspaceId: options.parentWorkspaceId,
|
|
141
|
+
ownerMemberId: options.ownerMemberId,
|
|
92
142
|
branchName: '', // 占位,稍后更新
|
|
93
143
|
worktreePath: '', // 占位,稍后更新
|
|
94
144
|
status: WorkspaceStatus.ACTIVE,
|
|
@@ -96,18 +146,21 @@ export class WorkspaceService {
|
|
|
96
146
|
});
|
|
97
147
|
try {
|
|
98
148
|
// 生成分支名:用户指定 or 自动生成 at/{shortId}
|
|
99
|
-
const branch =
|
|
149
|
+
const branch = branchFromOptions(workspace.id, options);
|
|
100
150
|
// 空仓库(无任何 commit)无法创建 worktree,提前报错
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
151
|
+
const startPoint = options.startPoint || null;
|
|
152
|
+
let baseBranch = startPoint;
|
|
153
|
+
if (!startPoint) {
|
|
154
|
+
try {
|
|
155
|
+
const currentBranch = (await execGit(task.project.repoPath, ['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
156
|
+
baseBranch = currentBranch && currentBranch !== 'HEAD' ? currentBranch : null;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
throw new Error('仓库尚无任何提交记录,无法创建 Workspace。请重新编辑项目以触发自动初始化,或手动执行 git commit。');
|
|
160
|
+
}
|
|
108
161
|
}
|
|
109
162
|
// WorktreeManager.create 内部已做分支名合法性校验和重复检查
|
|
110
|
-
const worktreePath = await worktreeManager.create(branch);
|
|
163
|
+
const worktreePath = await worktreeManager.create(branch, startPoint ?? undefined);
|
|
111
164
|
// worktree 创建后:复制文件 + 异步执行 setup 脚本(fire-and-forget)
|
|
112
165
|
this.runCopyFiles(task.project.repoPath, worktreePath, task.project.copyFiles);
|
|
113
166
|
this.fireSetupScript(workspace.id, taskId, worktreePath, task.project.setupScript);
|
|
@@ -127,6 +180,201 @@ export class WorkspaceService {
|
|
|
127
180
|
throw err;
|
|
128
181
|
}
|
|
129
182
|
}
|
|
183
|
+
async getOrCreateMainWorkspace(teamRunId) {
|
|
184
|
+
const existingClaim = WorkspaceService.mainWorkspaceClaims.get(teamRunId);
|
|
185
|
+
if (existingClaim) {
|
|
186
|
+
return existingClaim;
|
|
187
|
+
}
|
|
188
|
+
const claim = this.findOrCreateMainWorkspace(teamRunId);
|
|
189
|
+
WorkspaceService.mainWorkspaceClaims.set(teamRunId, claim);
|
|
190
|
+
try {
|
|
191
|
+
return await claim;
|
|
192
|
+
}
|
|
193
|
+
finally {
|
|
194
|
+
if (WorkspaceService.mainWorkspaceClaims.get(teamRunId) === claim) {
|
|
195
|
+
WorkspaceService.mainWorkspaceClaims.delete(teamRunId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async getOrCreateDedicatedWorkspace(teamRunId, memberId) {
|
|
200
|
+
const mainWorkspace = await this.getOrCreateMainWorkspace(teamRunId);
|
|
201
|
+
const claimKey = `${mainWorkspace.id}:${memberId}`;
|
|
202
|
+
const existingClaim = WorkspaceService.dedicatedWorkspaceClaims.get(claimKey);
|
|
203
|
+
if (existingClaim) {
|
|
204
|
+
return existingClaim;
|
|
205
|
+
}
|
|
206
|
+
const claim = this.findOrCreateDedicatedWorkspace(teamRunId, memberId, mainWorkspace);
|
|
207
|
+
WorkspaceService.dedicatedWorkspaceClaims.set(claimKey, claim);
|
|
208
|
+
try {
|
|
209
|
+
return await claim;
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
if (WorkspaceService.dedicatedWorkspaceClaims.get(claimKey) === claim) {
|
|
213
|
+
WorkspaceService.dedicatedWorkspaceClaims.delete(claimKey);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async findOrCreateMainWorkspace(teamRunId) {
|
|
218
|
+
const teamRun = await prisma.teamRun.findUnique({
|
|
219
|
+
where: { id: teamRunId },
|
|
220
|
+
include: {
|
|
221
|
+
task: { include: { project: true } },
|
|
222
|
+
mainWorkspace: { include: { task: { include: { project: true } } } },
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
if (!teamRun) {
|
|
226
|
+
throw new NotFoundError('TeamRun', teamRunId);
|
|
227
|
+
}
|
|
228
|
+
ensureProjectIsMutable(teamRun.task.project, 'create workspaces');
|
|
229
|
+
if (teamRun.mainWorkspace
|
|
230
|
+
&& teamRun.mainWorkspace.taskId === teamRun.taskId
|
|
231
|
+
&& teamRun.mainWorkspace.parentWorkspaceId == null
|
|
232
|
+
&& teamRun.mainWorkspace.ownerMemberId == null
|
|
233
|
+
&& teamRun.mainWorkspace.status === WorkspaceStatus.ACTIVE) {
|
|
234
|
+
return this.ensureActiveWorkspaceWorktree(teamRun.mainWorkspace);
|
|
235
|
+
}
|
|
236
|
+
const activeRoot = await prisma.workspace.findFirst({
|
|
237
|
+
where: {
|
|
238
|
+
taskId: teamRun.taskId,
|
|
239
|
+
parentWorkspaceId: null,
|
|
240
|
+
ownerMemberId: null,
|
|
241
|
+
status: WorkspaceStatus.ACTIVE,
|
|
242
|
+
},
|
|
243
|
+
orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
|
|
244
|
+
include: { task: { include: { project: true } } },
|
|
245
|
+
});
|
|
246
|
+
if (activeRoot) {
|
|
247
|
+
await prisma.teamRun.update({
|
|
248
|
+
where: { id: teamRun.id },
|
|
249
|
+
data: { mainWorkspaceId: activeRoot.id },
|
|
250
|
+
});
|
|
251
|
+
return this.ensureActiveWorkspaceWorktree(activeRoot);
|
|
252
|
+
}
|
|
253
|
+
const workspace = await this.create(teamRun.taskId, {
|
|
254
|
+
branchNamePrefix: `${teamRunBranchPrefix(teamRun.id)}/main`,
|
|
255
|
+
parentWorkspaceId: null,
|
|
256
|
+
ownerMemberId: null,
|
|
257
|
+
reuseInactive: false,
|
|
258
|
+
});
|
|
259
|
+
await prisma.teamRun.update({
|
|
260
|
+
where: { id: teamRun.id },
|
|
261
|
+
data: { mainWorkspaceId: workspace.id },
|
|
262
|
+
});
|
|
263
|
+
return workspace;
|
|
264
|
+
}
|
|
265
|
+
async findOrCreateDedicatedWorkspace(teamRunId, memberId, mainWorkspace) {
|
|
266
|
+
const teamRun = await prisma.teamRun.findUnique({
|
|
267
|
+
where: { id: teamRunId },
|
|
268
|
+
include: { task: { include: { project: true } } },
|
|
269
|
+
});
|
|
270
|
+
if (!teamRun) {
|
|
271
|
+
throw new NotFoundError('TeamRun', teamRunId);
|
|
272
|
+
}
|
|
273
|
+
const member = await prisma.teamMember.findFirst({
|
|
274
|
+
where: { id: memberId, teamRunId },
|
|
275
|
+
select: { id: true },
|
|
276
|
+
});
|
|
277
|
+
if (!member) {
|
|
278
|
+
throw new NotFoundError('TeamMember', memberId);
|
|
279
|
+
}
|
|
280
|
+
ensureProjectIsMutable(teamRun.task.project, 'create workspaces');
|
|
281
|
+
const existing = await this.findDedicatedWorkspace(mainWorkspace.id, memberId);
|
|
282
|
+
if (existing) {
|
|
283
|
+
return this.activateDedicatedWorkspace(existing);
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
return await this.create(teamRun.taskId, {
|
|
287
|
+
branchNamePrefix: `${teamRunBranchPrefix(teamRun.id)}/member-${memberId.slice(0, 8)}`,
|
|
288
|
+
startPoint: mainWorkspace.branchName,
|
|
289
|
+
parentWorkspaceId: mainWorkspace.id,
|
|
290
|
+
ownerMemberId: memberId,
|
|
291
|
+
reuseInactive: false,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
if (!isUniqueConstraintError(error)) {
|
|
296
|
+
throw error;
|
|
297
|
+
}
|
|
298
|
+
const raced = await this.findDedicatedWorkspace(mainWorkspace.id, memberId);
|
|
299
|
+
if (!raced) {
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
return this.activateDedicatedWorkspace(await this.waitForWorkspaceReady(raced));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async findDedicatedWorkspace(mainWorkspaceId, memberId) {
|
|
306
|
+
return prisma.workspace.findFirst({
|
|
307
|
+
where: {
|
|
308
|
+
parentWorkspaceId: mainWorkspaceId,
|
|
309
|
+
ownerMemberId: memberId,
|
|
310
|
+
},
|
|
311
|
+
include: { task: { include: { project: true } } },
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async activateDedicatedWorkspace(workspace) {
|
|
315
|
+
if (!workspace.branchName) {
|
|
316
|
+
workspace = await this.waitForWorkspaceReady(workspace);
|
|
317
|
+
}
|
|
318
|
+
if (workspace.status === WorkspaceStatus.ACTIVE) {
|
|
319
|
+
return this.ensureActiveWorkspaceWorktree(workspace);
|
|
320
|
+
}
|
|
321
|
+
if (workspace.status === WorkspaceStatus.HIBERNATED) {
|
|
322
|
+
return this.restoreInactiveWorkspace(workspace);
|
|
323
|
+
}
|
|
324
|
+
throw new ServiceError(`Cannot reuse dedicated workspace in ${workspace.status} status`, 'DEDICATED_WORKSPACE_UNAVAILABLE', 409);
|
|
325
|
+
}
|
|
326
|
+
async waitForWorkspaceReady(workspace) {
|
|
327
|
+
if (workspace.branchName) {
|
|
328
|
+
return workspace;
|
|
329
|
+
}
|
|
330
|
+
for (let attempt = 0; attempt < WORKSPACE_READY_RETRY_COUNT; attempt++) {
|
|
331
|
+
await sleep(WORKSPACE_READY_RETRY_DELAY_MS);
|
|
332
|
+
const reloaded = await prisma.workspace.findUnique({
|
|
333
|
+
where: { id: workspace.id },
|
|
334
|
+
include: { task: { include: { project: true } } },
|
|
335
|
+
});
|
|
336
|
+
if (!reloaded) {
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
if (reloaded.branchName) {
|
|
340
|
+
return reloaded;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
throw new ServiceError(`Workspace ${workspace.id} is still initializing`, 'WORKSPACE_INITIALIZING', 409);
|
|
344
|
+
}
|
|
345
|
+
async ensureActiveWorkspaceWorktree(workspace) {
|
|
346
|
+
if (!workspace.branchName) {
|
|
347
|
+
workspace = await this.waitForWorkspaceReady(workspace);
|
|
348
|
+
}
|
|
349
|
+
if (workspace.worktreePath) {
|
|
350
|
+
const gitFileExists = await fs
|
|
351
|
+
.access(path.join(workspace.worktreePath, '.git'))
|
|
352
|
+
.then(() => true)
|
|
353
|
+
.catch(() => false);
|
|
354
|
+
if (gitFileExists) {
|
|
355
|
+
return prisma.workspace.findUniqueOrThrow({
|
|
356
|
+
where: { id: workspace.id },
|
|
357
|
+
include: { sessions: visibleSessionsFilter, task: { include: { project: true } } },
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return this.restoreInactiveWorkspace(workspace);
|
|
362
|
+
}
|
|
363
|
+
async restoreInactiveWorkspace(workspace) {
|
|
364
|
+
const worktreeManager = new WorktreeManager(workspace.task.project.repoPath);
|
|
365
|
+
const worktreePath = await worktreeManager.ensureWorktreeExists(workspace.branchName);
|
|
366
|
+
this.runCopyFiles(workspace.task.project.repoPath, worktreePath, workspace.task.project.copyFiles);
|
|
367
|
+
this.fireSetupScript(workspace.id, workspace.taskId, worktreePath, workspace.task.project.setupScript);
|
|
368
|
+
return prisma.workspace.update({
|
|
369
|
+
where: { id: workspace.id },
|
|
370
|
+
data: {
|
|
371
|
+
status: WorkspaceStatus.ACTIVE,
|
|
372
|
+
worktreePath,
|
|
373
|
+
hibernatedAt: null,
|
|
374
|
+
},
|
|
375
|
+
include: { sessions: visibleSessionsFilter, task: { include: { project: true } } },
|
|
376
|
+
});
|
|
377
|
+
}
|
|
130
378
|
// ── Delete ───────────────────────────────────────────────────────────────────
|
|
131
379
|
/**
|
|
132
380
|
* 删除 Workspace
|
|
@@ -229,57 +477,150 @@ export class WorkspaceService {
|
|
|
229
477
|
const worktreeManager = new WorktreeManager(workspace.task.project.repoPath);
|
|
230
478
|
await worktreeManager.abortOperation(workspace.worktreePath);
|
|
231
479
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
* @param commitMessage - 可选的自定义 commit message
|
|
237
|
-
* @returns squash commit 的 SHA
|
|
238
|
-
*/
|
|
239
|
-
async merge(id, commitMessage) {
|
|
240
|
-
const totalStart = performance.now();
|
|
241
|
-
const step = (label, start) => console.log(`[WorkspaceService.merge] ${label}: ${(performance.now() - start).toFixed(0)}ms`);
|
|
242
|
-
let t = performance.now();
|
|
480
|
+
async merge(id, commitMessageOrOptions) {
|
|
481
|
+
const options = typeof commitMessageOrOptions === 'string'
|
|
482
|
+
? { commitMessage: commitMessageOrOptions }
|
|
483
|
+
: (commitMessageOrOptions ?? {});
|
|
243
484
|
const workspace = await prisma.workspace.findUnique({
|
|
244
485
|
where: { id },
|
|
245
|
-
include: { task: { include: { project: true } } },
|
|
486
|
+
include: { task: { include: { project: true, teamRun: true } } },
|
|
246
487
|
});
|
|
247
|
-
step('db findUnique', t);
|
|
248
488
|
if (!workspace) {
|
|
249
489
|
throw new NotFoundError('Workspace', id);
|
|
250
490
|
}
|
|
251
491
|
ensureProjectIsMutable(workspace.task.project, 'merge workspaces');
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
492
|
+
return this.withProjectMergeLock(workspace.task.projectId, options.lockOwnerId, () => this.mergeWithLock(workspace, options.commitMessage));
|
|
493
|
+
}
|
|
494
|
+
async mergeWithLock(workspace, commitMessage) {
|
|
495
|
+
if (workspace.status !== WorkspaceStatus.ACTIVE) {
|
|
496
|
+
throw new ServiceError(`Cannot merge workspace in ${workspace.status} status`, 'INVALID_WORKSPACE_STATE', 400);
|
|
497
|
+
}
|
|
498
|
+
if (workspace.parentWorkspaceId) {
|
|
499
|
+
return this.mergeChildIntoParent(workspace, commitMessage);
|
|
500
|
+
}
|
|
501
|
+
return this.mergeRootWorkspaceToMain(workspace, commitMessage);
|
|
502
|
+
}
|
|
503
|
+
async mergeChildIntoParent(workspace, commitMessage) {
|
|
504
|
+
const parentWorkspace = await prisma.workspace.findUnique({
|
|
505
|
+
where: { id: workspace.parentWorkspaceId ?? '' },
|
|
506
|
+
include: { task: { include: { project: true } } },
|
|
507
|
+
});
|
|
508
|
+
if (!parentWorkspace) {
|
|
509
|
+
throw new NotFoundError('Workspace', workspace.parentWorkspaceId ?? '');
|
|
510
|
+
}
|
|
511
|
+
if (parentWorkspace.taskId !== workspace.taskId || parentWorkspace.ownerMemberId != null) {
|
|
512
|
+
throw new ServiceError('Dedicated child workspace parent is not a valid TeamRun main workspace', 'INVALID_PARENT_WORKSPACE', 400);
|
|
513
|
+
}
|
|
514
|
+
if (parentWorkspace.status !== WorkspaceStatus.ACTIVE) {
|
|
515
|
+
throw new ServiceError(`Cannot merge into parent workspace in ${parentWorkspace.status} status`, 'INVALID_PARENT_WORKSPACE_STATE', 409);
|
|
516
|
+
}
|
|
517
|
+
await this.assertNoActiveWriteSessions(parentWorkspace.id);
|
|
518
|
+
const worktreeManager = new WorktreeManager(workspace.task.project.repoPath);
|
|
519
|
+
let sha;
|
|
520
|
+
try {
|
|
521
|
+
({ sha } = await worktreeManager.mergeIntoWorktree(workspace.worktreePath, parentWorkspace.worktreePath, { commitMessage: commitMessage || workspace.commitMessage || undefined }));
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
if (error instanceof MergeConflictError) {
|
|
525
|
+
error.sourceWorkspaceId = workspace.id;
|
|
526
|
+
error.targetWorkspaceId = parentWorkspace.id;
|
|
527
|
+
error.sourceWorktreePath ??= workspace.worktreePath;
|
|
528
|
+
error.targetWorktreePath ??= parentWorkspace.worktreePath;
|
|
529
|
+
error.sourceBranch ??= workspace.branchName;
|
|
530
|
+
error.targetBranch ??= parentWorkspace.branchName;
|
|
531
|
+
}
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
await prisma.workspace.update({
|
|
535
|
+
where: { id: workspace.id },
|
|
536
|
+
data: { status: WorkspaceStatus.MERGED },
|
|
537
|
+
});
|
|
538
|
+
return sha;
|
|
539
|
+
}
|
|
540
|
+
async mergeRootWorkspaceToMain(workspace, commitMessage) {
|
|
541
|
+
await this.assertTeamRunFinalMergeAllowed(workspace);
|
|
255
542
|
const worktreeManager = new WorktreeManager(workspace.task.project.repoPath);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
543
|
+
let sha;
|
|
544
|
+
const targetBranch = this.getBaseBranch(workspace);
|
|
545
|
+
try {
|
|
546
|
+
({ sha } = await worktreeManager.merge(workspace.worktreePath, targetBranch, { commitMessage: commitMessage || workspace.commitMessage || undefined }));
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
if (error instanceof MergeConflictError) {
|
|
550
|
+
error.sourceWorkspaceId = workspace.id;
|
|
551
|
+
error.sourceWorktreePath ??= workspace.worktreePath;
|
|
552
|
+
error.sourceBranch ??= workspace.branchName;
|
|
553
|
+
error.targetBranch ??= targetBranch;
|
|
554
|
+
}
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
260
557
|
await prisma.workspace.update({
|
|
261
|
-
where: { id },
|
|
558
|
+
where: { id: workspace.id },
|
|
262
559
|
data: { status: WorkspaceStatus.MERGED },
|
|
263
560
|
});
|
|
264
|
-
step('db update workspace', t);
|
|
265
|
-
// Task 推进到 DONE
|
|
266
561
|
const advanceableStatuses = [TaskStatus.IN_PROGRESS, TaskStatus.IN_REVIEW];
|
|
267
562
|
if (advanceableStatuses.includes(workspace.task.status)) {
|
|
268
|
-
t = performance.now();
|
|
269
563
|
await prisma.task.update({
|
|
270
564
|
where: { id: workspace.task.id },
|
|
271
565
|
data: { status: TaskStatus.DONE },
|
|
272
566
|
});
|
|
273
|
-
step('db update task', t);
|
|
274
567
|
this.eventBus.emit('task:updated', {
|
|
275
568
|
taskId: workspace.task.id,
|
|
276
569
|
projectId: workspace.task.projectId,
|
|
277
570
|
status: TaskStatus.DONE,
|
|
278
571
|
});
|
|
279
572
|
}
|
|
280
|
-
console.log(`[WorkspaceService.merge] TOTAL: ${(performance.now() - totalStart).toFixed(0)}ms`);
|
|
281
573
|
return sha;
|
|
282
574
|
}
|
|
575
|
+
async assertTeamRunFinalMergeAllowed(workspace) {
|
|
576
|
+
const teamRun = workspace.task.teamRun;
|
|
577
|
+
if (!teamRun) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (teamRun.mainWorkspaceId !== workspace.id) {
|
|
581
|
+
throw new ServiceError('Only the bound TeamRun main workspace can be merged into the project main branch', 'TEAM_RUN_NON_MAIN_WORKSPACE_FINAL_MERGE_FORBIDDEN', 409);
|
|
582
|
+
}
|
|
583
|
+
const blockingChildren = await prisma.workspace.findMany({
|
|
584
|
+
where: {
|
|
585
|
+
parentWorkspaceId: workspace.id,
|
|
586
|
+
status: { notIn: finalChildWorkspaceStatuses },
|
|
587
|
+
},
|
|
588
|
+
select: { id: true, status: true },
|
|
589
|
+
orderBy: [{ createdAt: 'asc' }, { id: 'asc' }],
|
|
590
|
+
});
|
|
591
|
+
if (blockingChildren.length > 0) {
|
|
592
|
+
throw new ServiceError('Cannot merge TeamRun main workspace before all dedicated child workspaces are merged or abandoned', 'TEAM_RUN_CHILD_WORKSPACES_NOT_FINAL', 409);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async assertNoActiveWriteSessions(workspaceId) {
|
|
596
|
+
const activeSession = await prisma.session.findFirst({
|
|
597
|
+
where: {
|
|
598
|
+
workspaceId,
|
|
599
|
+
status: { in: activeSessionStatuses },
|
|
600
|
+
purpose: SessionPurpose.CHAT,
|
|
601
|
+
},
|
|
602
|
+
select: { id: true },
|
|
603
|
+
});
|
|
604
|
+
if (activeSession) {
|
|
605
|
+
throw new ServiceError('Cannot merge into parent workspace while it has an active write session', 'PARENT_WORKSPACE_HAS_ACTIVE_SESSION', 409);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
async withProjectMergeLock(projectId, requestedOwnerId, fn) {
|
|
609
|
+
const lockKey = `project:${projectId}:merge`;
|
|
610
|
+
const ownerId = requestedOwnerId ?? `workspace-merge:${randomUUID()}`;
|
|
611
|
+
const alreadyHeldByOwner = this.lockService.isHeldBy(ownerId, lockKey);
|
|
612
|
+
if (!alreadyHeldByOwner && !this.lockService.acquire(ownerId, [lockKey])) {
|
|
613
|
+
throw new ServiceError('Another workspace merge is already running for this project', 'PROJECT_MERGE_LOCKED', 409);
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
return await fn();
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
if (!alreadyHeldByOwner) {
|
|
620
|
+
this.lockService.release(ownerId, [lockKey]);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
283
624
|
// ── Archive ──────────────────────────────────────────────────────────────────
|
|
284
625
|
/**
|
|
285
626
|
* 归档 Workspace(标记状态为 ABANDONED)
|
|
@@ -475,7 +816,11 @@ export class WorkspaceService {
|
|
|
475
816
|
const worktreeManager = new WorktreeManager(workspace.task.project.repoPath);
|
|
476
817
|
// 清理残留 worktree(如果还存在)
|
|
477
818
|
if (workspace.worktreePath) {
|
|
478
|
-
await worktreeManager.remove(workspace.worktreePath);
|
|
819
|
+
const removeResult = await worktreeManager.remove(workspace.worktreePath);
|
|
820
|
+
if (removeResult.status === 'unregistered') {
|
|
821
|
+
console.warn(`[WorkspaceService] cleanup: workspace ${workspace.id} path is unregistered or unsafe to remove: ${removeResult.path}`);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
479
824
|
}
|
|
480
825
|
// Task 已 DONE,branch 不再需要,删除
|
|
481
826
|
if (workspace.branchName) {
|