xtep-workspace 1.10.10
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/CHANGELOG.md +525 -0
- package/README.md +188 -0
- package/config/component-versions.json +16 -0
- package/config/scenarioCapabilities.json +29 -0
- package/config/version-notes.yaml +244 -0
- package/dist/adapters/OutputAdapter.d.ts +79 -0
- package/dist/adapters/OutputAdapter.d.ts.map +1 -0
- package/dist/adapters/OutputAdapter.js +124 -0
- package/dist/adapters/OutputAdapter.js.map +1 -0
- package/dist/cli/check-node-version.d.ts +3 -0
- package/dist/cli/check-node-version.d.ts.map +1 -0
- package/dist/cli/check-node-version.js +153 -0
- package/dist/cli/check-node-version.js.map +1 -0
- package/dist/cli/plugins.d.ts +41 -0
- package/dist/cli/plugins.d.ts.map +1 -0
- package/dist/cli/plugins.js +742 -0
- package/dist/cli/plugins.js.map +1 -0
- package/dist/cli/rebuild.d.ts +63 -0
- package/dist/cli/rebuild.d.ts.map +1 -0
- package/dist/cli/rebuild.js +989 -0
- package/dist/cli/rebuild.js.map +1 -0
- package/dist/cli/repair.d.ts +7 -0
- package/dist/cli/repair.d.ts.map +1 -0
- package/dist/cli/repair.js +925 -0
- package/dist/cli/repair.js.map +1 -0
- package/dist/cli/setup.d.ts +7 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +452 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/update.d.ts +10 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/cli/update.js +426 -0
- package/dist/cli/update.js.map +1 -0
- package/dist/cli/webui.d.ts +6 -0
- package/dist/cli/webui.d.ts.map +1 -0
- package/dist/cli/webui.js +210 -0
- package/dist/cli/webui.js.map +1 -0
- package/dist/http/index.d.ts +3 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +15 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/middleware/errorHandler.d.ts +16 -0
- package/dist/http/middleware/errorHandler.d.ts.map +1 -0
- package/dist/http/middleware/errorHandler.js +79 -0
- package/dist/http/middleware/errorHandler.js.map +1 -0
- package/dist/http/routes/admin.d.ts +3 -0
- package/dist/http/routes/admin.d.ts.map +1 -0
- package/dist/http/routes/admin.js +730 -0
- package/dist/http/routes/admin.js.map +1 -0
- package/dist/http/routes/backup.d.ts +3 -0
- package/dist/http/routes/backup.d.ts.map +1 -0
- package/dist/http/routes/backup.js +172 -0
- package/dist/http/routes/backup.js.map +1 -0
- package/dist/http/routes/config.d.ts +3 -0
- package/dist/http/routes/config.d.ts.map +1 -0
- package/dist/http/routes/config.js +157 -0
- package/dist/http/routes/config.js.map +1 -0
- package/dist/http/routes/context.d.ts +3 -0
- package/dist/http/routes/context.d.ts.map +1 -0
- package/dist/http/routes/context.js +82 -0
- package/dist/http/routes/context.js.map +1 -0
- package/dist/http/routes/log.d.ts +3 -0
- package/dist/http/routes/log.d.ts.map +1 -0
- package/dist/http/routes/log.js +105 -0
- package/dist/http/routes/log.js.map +1 -0
- package/dist/http/routes/memo.d.ts +6 -0
- package/dist/http/routes/memo.d.ts.map +1 -0
- package/dist/http/routes/memo.js +29 -0
- package/dist/http/routes/memo.js.map +1 -0
- package/dist/http/routes/node.d.ts +3 -0
- package/dist/http/routes/node.d.ts.map +1 -0
- package/dist/http/routes/node.js +251 -0
- package/dist/http/routes/node.js.map +1 -0
- package/dist/http/routes/state.d.ts +3 -0
- package/dist/http/routes/state.d.ts.map +1 -0
- package/dist/http/routes/state.js +48 -0
- package/dist/http/routes/state.js.map +1 -0
- package/dist/http/routes/workspace.d.ts +3 -0
- package/dist/http/routes/workspace.d.ts.map +1 -0
- package/dist/http/routes/workspace.js +249 -0
- package/dist/http/routes/workspace.js.map +1 -0
- package/dist/http/server.d.ts +10 -0
- package/dist/http/server.d.ts.map +1 -0
- package/dist/http/server.js +284 -0
- package/dist/http/server.js.map +1 -0
- package/dist/http/services.d.ts +93 -0
- package/dist/http/services.d.ts.map +1 -0
- package/dist/http/services.js +297 -0
- package/dist/http/services.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1073 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/guidanceContent.d.ts +18 -0
- package/dist/prompts/guidanceContent.d.ts.map +1 -0
- package/dist/prompts/guidanceContent.js +814 -0
- package/dist/prompts/guidanceContent.js.map +1 -0
- package/dist/prompts/index.d.ts +2 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +4 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/instructions.d.ts +56 -0
- package/dist/prompts/instructions.d.ts.map +1 -0
- package/dist/prompts/instructions.js +1343 -0
- package/dist/prompts/instructions.js.map +1 -0
- package/dist/services/BackupService.d.ts +104 -0
- package/dist/services/BackupService.d.ts.map +1 -0
- package/dist/services/BackupService.js +549 -0
- package/dist/services/BackupService.js.map +1 -0
- package/dist/services/CapabilityService.d.ts +38 -0
- package/dist/services/CapabilityService.d.ts.map +1 -0
- package/dist/services/CapabilityService.js +256 -0
- package/dist/services/CapabilityService.js.map +1 -0
- package/dist/services/ConfigService.d.ts +35 -0
- package/dist/services/ConfigService.d.ts.map +1 -0
- package/dist/services/ConfigService.js +105 -0
- package/dist/services/ConfigService.js.map +1 -0
- package/dist/services/ContextService.d.ts +65 -0
- package/dist/services/ContextService.d.ts.map +1 -0
- package/dist/services/ContextService.js +503 -0
- package/dist/services/ContextService.js.map +1 -0
- package/dist/services/DetectionService.d.ts +76 -0
- package/dist/services/DetectionService.d.ts.map +1 -0
- package/dist/services/DetectionService.js +262 -0
- package/dist/services/DetectionService.js.map +1 -0
- package/dist/services/DispatchService.d.ts +267 -0
- package/dist/services/DispatchService.d.ts.map +1 -0
- package/dist/services/DispatchService.js +1357 -0
- package/dist/services/DispatchService.js.map +1 -0
- package/dist/services/EventService.d.ts +81 -0
- package/dist/services/EventService.d.ts.map +1 -0
- package/dist/services/EventService.js +187 -0
- package/dist/services/EventService.js.map +1 -0
- package/dist/services/GuidanceService.d.ts +64 -0
- package/dist/services/GuidanceService.d.ts.map +1 -0
- package/dist/services/GuidanceService.js +259 -0
- package/dist/services/GuidanceService.js.map +1 -0
- package/dist/services/HealthService.d.ts +43 -0
- package/dist/services/HealthService.d.ts.map +1 -0
- package/dist/services/HealthService.js +276 -0
- package/dist/services/HealthService.js.map +1 -0
- package/dist/services/InstallationService.d.ts +62 -0
- package/dist/services/InstallationService.d.ts.map +1 -0
- package/dist/services/InstallationService.js +204 -0
- package/dist/services/InstallationService.js.map +1 -0
- package/dist/services/LogService.d.ts +35 -0
- package/dist/services/LogService.d.ts.map +1 -0
- package/dist/services/LogService.js +189 -0
- package/dist/services/LogService.js.map +1 -0
- package/dist/services/MemoService.d.ts +39 -0
- package/dist/services/MemoService.d.ts.map +1 -0
- package/dist/services/MemoService.js +288 -0
- package/dist/services/MemoService.js.map +1 -0
- package/dist/services/NodeService.d.ts +90 -0
- package/dist/services/NodeService.d.ts.map +1 -0
- package/dist/services/NodeService.js +958 -0
- package/dist/services/NodeService.js.map +1 -0
- package/dist/services/OpenSpecParser.d.ts +43 -0
- package/dist/services/OpenSpecParser.d.ts.map +1 -0
- package/dist/services/OpenSpecParser.js +191 -0
- package/dist/services/OpenSpecParser.js.map +1 -0
- package/dist/services/ReferenceService.d.ts +35 -0
- package/dist/services/ReferenceService.d.ts.map +1 -0
- package/dist/services/ReferenceService.js +195 -0
- package/dist/services/ReferenceService.js.map +1 -0
- package/dist/services/RepairService.d.ts +36 -0
- package/dist/services/RepairService.d.ts.map +1 -0
- package/dist/services/RepairService.js +429 -0
- package/dist/services/RepairService.js.map +1 -0
- package/dist/services/SearchService.d.ts +34 -0
- package/dist/services/SearchService.d.ts.map +1 -0
- package/dist/services/SearchService.js +293 -0
- package/dist/services/SearchService.js.map +1 -0
- package/dist/services/SessionService.d.ts +136 -0
- package/dist/services/SessionService.d.ts.map +1 -0
- package/dist/services/SessionService.js +297 -0
- package/dist/services/SessionService.js.map +1 -0
- package/dist/services/StateService.d.ts +97 -0
- package/dist/services/StateService.d.ts.map +1 -0
- package/dist/services/StateService.js +846 -0
- package/dist/services/StateService.js.map +1 -0
- package/dist/services/TutorialService.d.ts +114 -0
- package/dist/services/TutorialService.d.ts.map +1 -0
- package/dist/services/TutorialService.js +1262 -0
- package/dist/services/TutorialService.js.map +1 -0
- package/dist/services/WorkspaceService.d.ts +273 -0
- package/dist/services/WorkspaceService.d.ts.map +1 -0
- package/dist/services/WorkspaceService.js +1764 -0
- package/dist/services/WorkspaceService.js.map +1 -0
- package/dist/services/index.d.ts +15 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +14 -0
- package/dist/services/index.js.map +1 -0
- package/dist/storage/FileSystemAdapter.d.ts +223 -0
- package/dist/storage/FileSystemAdapter.d.ts.map +1 -0
- package/dist/storage/FileSystemAdapter.js +384 -0
- package/dist/storage/FileSystemAdapter.js.map +1 -0
- package/dist/storage/JsonStorage.d.ts +158 -0
- package/dist/storage/JsonStorage.d.ts.map +1 -0
- package/dist/storage/JsonStorage.js +613 -0
- package/dist/storage/JsonStorage.js.map +1 -0
- package/dist/storage/MarkdownStorage.d.ts +178 -0
- package/dist/storage/MarkdownStorage.d.ts.map +1 -0
- package/dist/storage/MarkdownStorage.js +918 -0
- package/dist/storage/MarkdownStorage.js.map +1 -0
- package/dist/storage/SessionBindingStorage.d.ts +69 -0
- package/dist/storage/SessionBindingStorage.d.ts.map +1 -0
- package/dist/storage/SessionBindingStorage.js +131 -0
- package/dist/storage/SessionBindingStorage.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +6 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/tools/capability.d.ts +18 -0
- package/dist/tools/capability.d.ts.map +1 -0
- package/dist/tools/capability.js +73 -0
- package/dist/tools/capability.js.map +1 -0
- package/dist/tools/config.d.ts +14 -0
- package/dist/tools/config.d.ts.map +1 -0
- package/dist/tools/config.js +61 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/context.d.ts +22 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +139 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/dispatch.d.ts +41 -0
- package/dist/tools/dispatch.d.ts.map +1 -0
- package/dist/tools/dispatch.js +380 -0
- package/dist/tools/dispatch.js.map +1 -0
- package/dist/tools/help.d.ts +44 -0
- package/dist/tools/help.d.ts.map +1 -0
- package/dist/tools/help.js +227 -0
- package/dist/tools/help.js.map +1 -0
- package/dist/tools/import.d.ts +17 -0
- package/dist/tools/import.d.ts.map +1 -0
- package/dist/tools/import.js +96 -0
- package/dist/tools/import.js.map +1 -0
- package/dist/tools/index.d.ts +12 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +13 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/log.d.ts +21 -0
- package/dist/tools/log.d.ts.map +1 -0
- package/dist/tools/log.js +93 -0
- package/dist/tools/log.js.map +1 -0
- package/dist/tools/memo.d.ts +26 -0
- package/dist/tools/memo.d.ts.map +1 -0
- package/dist/tools/memo.js +188 -0
- package/dist/tools/memo.js.map +1 -0
- package/dist/tools/node.d.ts +34 -0
- package/dist/tools/node.d.ts.map +1 -0
- package/dist/tools/node.js +328 -0
- package/dist/tools/node.js.map +1 -0
- package/dist/tools/search.d.ts +14 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +95 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/session.d.ts +22 -0
- package/dist/tools/session.d.ts.map +1 -0
- package/dist/tools/session.js +127 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/state.d.ts +10 -0
- package/dist/tools/state.d.ts.map +1 -0
- package/dist/tools/state.js +79 -0
- package/dist/tools/state.js.map +1 -0
- package/dist/tools/workspace.d.ts +38 -0
- package/dist/tools/workspace.d.ts.map +1 -0
- package/dist/tools/workspace.js +240 -0
- package/dist/tools/workspace.js.map +1 -0
- package/dist/types/capability.d.ts +36 -0
- package/dist/types/capability.d.ts.map +1 -0
- package/dist/types/capability.js +3 -0
- package/dist/types/capability.js.map +1 -0
- package/dist/types/confirmation.d.ts +35 -0
- package/dist/types/confirmation.d.ts.map +1 -0
- package/dist/types/confirmation.js +3 -0
- package/dist/types/confirmation.js.map +1 -0
- package/dist/types/context.d.ts +174 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/context.js +3 -0
- package/dist/types/context.js.map +1 -0
- package/dist/types/errors.d.ts +81 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +154 -0
- package/dist/types/errors.js.map +1 -0
- package/dist/types/guidance.d.ts +162 -0
- package/dist/types/guidance.d.ts.map +1 -0
- package/dist/types/guidance.js +4 -0
- package/dist/types/guidance.js.map +1 -0
- package/dist/types/health.d.ts +61 -0
- package/dist/types/health.d.ts.map +1 -0
- package/dist/types/health.js +3 -0
- package/dist/types/health.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +11 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/memo.d.ts +132 -0
- package/dist/types/memo.d.ts.map +1 -0
- package/dist/types/memo.js +3 -0
- package/dist/types/memo.js.map +1 -0
- package/dist/types/node.d.ts +316 -0
- package/dist/types/node.d.ts.map +1 -0
- package/dist/types/node.js +3 -0
- package/dist/types/node.js.map +1 -0
- package/dist/types/repair.d.ts +62 -0
- package/dist/types/repair.d.ts.map +1 -0
- package/dist/types/repair.js +4 -0
- package/dist/types/repair.js.map +1 -0
- package/dist/types/search.d.ts +58 -0
- package/dist/types/search.d.ts.map +1 -0
- package/dist/types/search.js +3 -0
- package/dist/types/search.js.map +1 -0
- package/dist/types/settings.d.ts +109 -0
- package/dist/types/settings.d.ts.map +1 -0
- package/dist/types/settings.js +30 -0
- package/dist/types/settings.js.map +1 -0
- package/dist/types/workspace.d.ts +357 -0
- package/dist/types/workspace.d.ts.map +1 -0
- package/dist/types/workspace.js +3 -0
- package/dist/types/workspace.js.map +1 -0
- package/dist/utils/contentValidation.d.ts +47 -0
- package/dist/utils/contentValidation.d.ts.map +1 -0
- package/dist/utils/contentValidation.js +93 -0
- package/dist/utils/contentValidation.js.map +1 -0
- package/dist/utils/devLog.d.ts +43 -0
- package/dist/utils/devLog.d.ts.map +1 -0
- package/dist/utils/devLog.js +94 -0
- package/dist/utils/devLog.js.map +1 -0
- package/dist/utils/errorLogger.d.ts +27 -0
- package/dist/utils/errorLogger.d.ts.map +1 -0
- package/dist/utils/errorLogger.js +105 -0
- package/dist/utils/errorLogger.js.map +1 -0
- package/dist/utils/git.d.ts +123 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +400 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/hash.d.ts +32 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +37 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/id.d.ts +54 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +96 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +42 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +228 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/manualChangeFormatter.d.ts +8 -0
- package/dist/utils/manualChangeFormatter.d.ts.map +1 -0
- package/dist/utils/manualChangeFormatter.js +21 -0
- package/dist/utils/manualChangeFormatter.js.map +1 -0
- package/dist/utils/paramValidator.d.ts +35 -0
- package/dist/utils/paramValidator.d.ts.map +1 -0
- package/dist/utils/paramValidator.js +214 -0
- package/dist/utils/paramValidator.js.map +1 -0
- package/dist/utils/port.d.ts +7 -0
- package/dist/utils/port.d.ts.map +1 -0
- package/dist/utils/port.js +28 -0
- package/dist/utils/port.js.map +1 -0
- package/dist/utils/processManager.d.ts +53 -0
- package/dist/utils/processManager.d.ts.map +1 -0
- package/dist/utils/processManager.js +267 -0
- package/dist/utils/processManager.js.map +1 -0
- package/dist/utils/sessionLogger.d.ts +28 -0
- package/dist/utils/sessionLogger.d.ts.map +1 -0
- package/dist/utils/sessionLogger.js +142 -0
- package/dist/utils/sessionLogger.js.map +1 -0
- package/dist/utils/time.d.ts +15 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +32 -0
- package/dist/utils/time.js.map +1 -0
- package/dist/utils/validation.d.ts +23 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +88 -0
- package/dist/utils/validation.js.map +1 -0
- package/docs//346/227/245/345/277/227/347/263/273/347/273/237.md +389 -0
- package/docs//347/224/250/346/210/267/346/211/213/345/206/214.md +1446 -0
- package/docs//347/224/250/346/210/267/346/211/213/345/206/214/344/270/216/346/212/200/346/234/257/346/214/207/345/215/227.md +873 -0
- package/package.json +89 -0
- package/plugin/README.md +141 -0
- package/plugin/agents/xtep-executor.md +114 -0
- package/plugin/agents/xtep-reviewer.md +133 -0
- package/plugin/docs/diagnostic-guide.md +128 -0
- package/plugin/hooks/hooks.json.deprecated +70 -0
- package/plugin/scripts/cursor-hook-entry.cjs +217 -0
- package/plugin/scripts/hook-entry.cjs +663 -0
- package/plugin/scripts/openspec-import.cjs +714 -0
- package/plugin/scripts/shared/binding.cjs +98 -0
- package/plugin/scripts/shared/config.cjs +65 -0
- package/plugin/scripts/shared/context.cjs +120 -0
- package/plugin/scripts/shared/index.cjs +34 -0
- package/plugin/scripts/shared/logger.cjs +196 -0
- package/plugin/scripts/shared/reminder.cjs +261 -0
- package/plugin/scripts/shared/utils.cjs +62 -0
- package/plugin/scripts/shared/workspace.cjs +322 -0
- package/plugin/skills/aligning-intent/SKILL.md +275 -0
- package/plugin/skills/analyzing-measurements/SKILL.md +223 -0
- package/plugin/skills/bootstrapping-workspace/SKILL.md +260 -0
- package/plugin/skills/designing-solutions/SKILL.md +363 -0
- package/plugin/skills/diagnosing-issues/SKILL.md +219 -0
- package/plugin/skills/discovering-context/SKILL.md +283 -0
- package/plugin/skills/dispatching-parent/SKILL.md +399 -0
- package/plugin/skills/executing-task/SKILL.md +340 -0
- package/plugin/skills/memo-create/SKILL.md +222 -0
- package/plugin/skills/planning-verification/SKILL.md +245 -0
- package/plugin/skills/preparing-dispatch/SKILL.md +299 -0
- package/plugin/skills/researching-tech/SKILL.md +223 -0
- package/plugin/skills/reviewing-quality/SKILL.md +354 -0
- package/plugin/skills/reviewing-spec/SKILL.md +333 -0
- package/plugin/skills/starting-info-flow/SKILL.md +196 -0
- package/web/README.md +5 -0
- package//351/205/215/347/275/256/346/226/271/345/274/217.md +330 -0
|
@@ -0,0 +1,1764 @@
|
|
|
1
|
+
// src/services/WorkspaceService.ts
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as crypto from "node:crypto";
|
|
4
|
+
import * as fs from "node:fs/promises";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
import archiver from "archiver";
|
|
10
|
+
import AdmZip from "adm-zip";
|
|
11
|
+
import { logError } from "../utils/errorLogger.js";
|
|
12
|
+
import { XtepError } from "../types/errors.js";
|
|
13
|
+
import { generateWorkspaceId, generateWorkspaceDirName, extractShortId } from "../utils/id.js";
|
|
14
|
+
import { now } from "../utils/time.js";
|
|
15
|
+
import { validateWorkspaceName, validateProjectRoot } from "../utils/validation.js";
|
|
16
|
+
import { devLog } from "../utils/devLog.js";
|
|
17
|
+
import { taskScenarioToGuidance, getGuidanceConfig } from "../prompts/guidanceContent.js";
|
|
18
|
+
import { eventService } from "./EventService.js";
|
|
19
|
+
/**
|
|
20
|
+
* 获取 HTTP 服务端口
|
|
21
|
+
* 开发模式默认 19541,正式模式默认 19540
|
|
22
|
+
*/
|
|
23
|
+
function getHttpPort() {
|
|
24
|
+
const isDev = process.env.NODE_ENV === "development" || process.env.XTEP_DEV === "true";
|
|
25
|
+
const defaultPort = isDev ? "19541" : "19540";
|
|
26
|
+
return parseInt(process.env.HTTP_PORT ?? process.env.PORT ?? defaultPort, 10);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 工作区服务
|
|
30
|
+
* 处理工作区相关的业务逻辑
|
|
31
|
+
*
|
|
32
|
+
* 架构:
|
|
33
|
+
* - 全局索引:~/.xtep-workspace/index.json
|
|
34
|
+
* - 项目数据:{projectRoot}/.xtep-workspace/
|
|
35
|
+
*/
|
|
36
|
+
export class WorkspaceService {
|
|
37
|
+
json;
|
|
38
|
+
md;
|
|
39
|
+
fs;
|
|
40
|
+
stateService;
|
|
41
|
+
constructor(json, md, fs) {
|
|
42
|
+
this.json = json;
|
|
43
|
+
this.md = md;
|
|
44
|
+
this.fs = fs;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 设置 StateService 依赖(用于 token 生成)
|
|
48
|
+
*/
|
|
49
|
+
setStateService(stateService) {
|
|
50
|
+
this.stateService = stateService;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 根据 workspaceId 获取工作区信息(包括 dirName 和归档状态)
|
|
54
|
+
* 用于从 workspaceId 查找 dirName 以访问文件系统
|
|
55
|
+
*/
|
|
56
|
+
async resolveWorkspaceInfo(workspaceId) {
|
|
57
|
+
const index = await this.json.readIndex();
|
|
58
|
+
const wsEntry = index.workspaces.find(ws => ws.id === workspaceId);
|
|
59
|
+
if (!wsEntry) {
|
|
60
|
+
devLog.workspaceLookup(workspaceId, false);
|
|
61
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 不存在`);
|
|
62
|
+
}
|
|
63
|
+
const isArchived = wsEntry.status === "archived";
|
|
64
|
+
// 如果旧工作区没有 dirName,回退到使用 id
|
|
65
|
+
const dirName = wsEntry.dirName || wsEntry.id;
|
|
66
|
+
devLog.workspaceLookup(workspaceId, true, wsEntry.status);
|
|
67
|
+
return {
|
|
68
|
+
projectRoot: wsEntry.projectRoot,
|
|
69
|
+
dirName,
|
|
70
|
+
isArchived,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 初始化工作区
|
|
75
|
+
*/
|
|
76
|
+
async init(params) {
|
|
77
|
+
// 1. 验证名称合法性
|
|
78
|
+
validateWorkspaceName(params.name);
|
|
79
|
+
// 2. 确定并验证项目根目录(默认为当前工作目录)
|
|
80
|
+
const projectRoot = params.projectRoot
|
|
81
|
+
? validateProjectRoot(params.projectRoot)
|
|
82
|
+
: process.cwd();
|
|
83
|
+
// 3. 检查同一项目下是否存在同名工作区(允许多工作区,但名称需唯一)
|
|
84
|
+
if (await this.json.hasWorkspaceByName(projectRoot, params.name)) {
|
|
85
|
+
throw new XtepError("WORKSPACE_EXISTS", `项目 "${projectRoot}" 下工作区 "${params.name}" 已存在`);
|
|
86
|
+
}
|
|
87
|
+
// 4. 读取索引(后续更新用)
|
|
88
|
+
const index = await this.json.readIndex();
|
|
89
|
+
// 5. 生成工作区 ID 和目录名
|
|
90
|
+
const workspaceId = generateWorkspaceId();
|
|
91
|
+
const wsDirName = generateWorkspaceDirName(params.name, workspaceId);
|
|
92
|
+
const currentTime = now();
|
|
93
|
+
const rootNodeId = "root";
|
|
94
|
+
const rootNodeDirName = "root"; // 根节点目录名固定为 "root"
|
|
95
|
+
// 5.1 处理场景参数(默认 misc)
|
|
96
|
+
const scenario = params.scenario || 'misc';
|
|
97
|
+
// 6. 创建项目内目录结构(使用可读的目录名)
|
|
98
|
+
await this.fs.ensureProjectDir(projectRoot);
|
|
99
|
+
await this.fs.ensureWorkspaceDir(projectRoot, wsDirName);
|
|
100
|
+
await this.fs.mkdir(this.fs.getNodesDir(projectRoot, wsDirName));
|
|
101
|
+
await this.fs.mkdir(this.fs.getNodePath(projectRoot, wsDirName, rootNodeDirName));
|
|
102
|
+
// 7. 写入 workspace.json
|
|
103
|
+
const config = {
|
|
104
|
+
id: workspaceId,
|
|
105
|
+
name: params.name,
|
|
106
|
+
dirName: wsDirName,
|
|
107
|
+
status: "active",
|
|
108
|
+
createdAt: currentTime,
|
|
109
|
+
updatedAt: currentTime,
|
|
110
|
+
rootNodeId,
|
|
111
|
+
scenario, // 保存场景类型
|
|
112
|
+
};
|
|
113
|
+
await this.json.writeWorkspaceConfig(projectRoot, wsDirName, config);
|
|
114
|
+
// 8. 写入 graph.json(含根节点,类型为 planning)
|
|
115
|
+
const rootNode = {
|
|
116
|
+
id: rootNodeId,
|
|
117
|
+
dirName: rootNodeDirName,
|
|
118
|
+
type: "planning", // 根节点固定为规划节点
|
|
119
|
+
parentId: null,
|
|
120
|
+
children: [],
|
|
121
|
+
status: "pending",
|
|
122
|
+
isolate: false,
|
|
123
|
+
references: [],
|
|
124
|
+
conclusion: null,
|
|
125
|
+
createdAt: currentTime,
|
|
126
|
+
updatedAt: currentTime,
|
|
127
|
+
};
|
|
128
|
+
const graph = {
|
|
129
|
+
version: "4.0", // 新版本支持 dirName
|
|
130
|
+
currentFocus: rootNodeId,
|
|
131
|
+
nodes: {
|
|
132
|
+
[rootNodeId]: rootNode,
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
await this.json.writeGraph(projectRoot, wsDirName, graph);
|
|
136
|
+
// 9. 写入 Workspace.md(goal 已移至根节点 requirement,不再写入 Workspace.md)
|
|
137
|
+
await this.md.writeWorkspaceMd(projectRoot, wsDirName, {
|
|
138
|
+
name: params.name,
|
|
139
|
+
createdAt: currentTime,
|
|
140
|
+
updatedAt: currentTime,
|
|
141
|
+
rules: params.rules || [],
|
|
142
|
+
docs: params.docs || [],
|
|
143
|
+
});
|
|
144
|
+
// 10. 创建空的 Log.md 和 Problem.md (工作区级别)
|
|
145
|
+
await this.md.createEmptyLog(projectRoot, wsDirName);
|
|
146
|
+
await this.md.createEmptyProblem(projectRoot, wsDirName);
|
|
147
|
+
// 11. 创建根节点文件(规划节点)
|
|
148
|
+
await this.md.writeNodeInfo(projectRoot, wsDirName, rootNodeDirName, {
|
|
149
|
+
id: rootNodeId,
|
|
150
|
+
type: "planning", // 根节点固定为规划节点
|
|
151
|
+
title: params.name,
|
|
152
|
+
status: "pending",
|
|
153
|
+
createdAt: currentTime,
|
|
154
|
+
updatedAt: currentTime,
|
|
155
|
+
requirement: params.goal,
|
|
156
|
+
docs: params.docs || [],
|
|
157
|
+
notes: "",
|
|
158
|
+
conclusion: "",
|
|
159
|
+
});
|
|
160
|
+
await this.md.createEmptyLog(projectRoot, wsDirName, rootNodeDirName);
|
|
161
|
+
await this.md.createEmptyProblem(projectRoot, wsDirName, rootNodeDirName);
|
|
162
|
+
// 12. 更新全局索引
|
|
163
|
+
await this.fs.ensureIndex();
|
|
164
|
+
index.workspaces.push({
|
|
165
|
+
id: workspaceId,
|
|
166
|
+
name: params.name,
|
|
167
|
+
dirName: wsDirName,
|
|
168
|
+
projectRoot,
|
|
169
|
+
status: "active",
|
|
170
|
+
createdAt: currentTime,
|
|
171
|
+
updatedAt: currentTime,
|
|
172
|
+
});
|
|
173
|
+
await this.json.writeIndex(index);
|
|
174
|
+
// 13. 追加日志
|
|
175
|
+
await this.md.appendLog(projectRoot, wsDirName, {
|
|
176
|
+
time: currentTime,
|
|
177
|
+
operator: "system",
|
|
178
|
+
event: `工作区 "${params.name}" 已创建`,
|
|
179
|
+
});
|
|
180
|
+
// 14. 扫描项目文档
|
|
181
|
+
const projectDocs = await this.scanProjectDocs(projectRoot);
|
|
182
|
+
// 15. 生成 hint(包含项目文档信息)
|
|
183
|
+
// 使用 guidanceContent.ts 中的 workspace_init L0 引导
|
|
184
|
+
const wsInitGuidance = getGuidanceConfig("workspace_init");
|
|
185
|
+
let hint = `💡 ${wsInitGuidance?.l0 || "工作区已创建。下一步:调用 capability_list 获取场景推荐能力,然后创建信息收集节点。"}`;
|
|
186
|
+
if (projectDocs.totalFound > 0) {
|
|
187
|
+
hint += `\n\n📚 项目文档扫描结果:发现 ${projectDocs.totalFound} 个 .md 文件`;
|
|
188
|
+
if (projectDocs.degraded) {
|
|
189
|
+
hint += `(超过限制,仅显示部分)`;
|
|
190
|
+
}
|
|
191
|
+
// 统计无元文件的文档
|
|
192
|
+
const noFrontmatter = projectDocs.files.filter(f => !f.hasFrontmatter);
|
|
193
|
+
if (noFrontmatter.length > 0) {
|
|
194
|
+
hint += `\n⚠️ 其中 ${noFrontmatter.length} 个文档缺少元文件(frontmatter),建议在相关任务中补充。`;
|
|
195
|
+
}
|
|
196
|
+
if (projectDocs.folders.length > 0) {
|
|
197
|
+
hint += `\n📁 文档文件夹: ${projectDocs.folders.join(", ")}`;
|
|
198
|
+
}
|
|
199
|
+
hint += `\n💡 使用 node_reference 引用相关文档,便于任务跟踪和文档同步。`;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
hint += `\n\n📭 未在项目中发现 .md 文档文件。`;
|
|
203
|
+
}
|
|
204
|
+
// 生成场景化引导内容
|
|
205
|
+
const scenarioGuidance = this.getScenarioGuidance(scenario);
|
|
206
|
+
// 构建返回结果
|
|
207
|
+
const result = {
|
|
208
|
+
workspaceId,
|
|
209
|
+
path: this.fs.getWorkspacePath(projectRoot, wsDirName),
|
|
210
|
+
projectRoot,
|
|
211
|
+
rootNodeId,
|
|
212
|
+
scenario,
|
|
213
|
+
webUrl: `http://localhost:${getHttpPort()}/workspace/${workspaceId}`,
|
|
214
|
+
hint,
|
|
215
|
+
projectDocs,
|
|
216
|
+
};
|
|
217
|
+
// 强制调用 bootstrapping-workspace skill
|
|
218
|
+
result.actionRequired = {
|
|
219
|
+
type: "invoke_skill",
|
|
220
|
+
message: `⚠️ MUST 调用 Skill(bootstrapping-workspace) 完成工作区启动流程。
|
|
221
|
+
|
|
222
|
+
${scenarioGuidance}
|
|
223
|
+
|
|
224
|
+
**强制规则**:
|
|
225
|
+
- MUST 调用 Skill(bootstrapping-workspace)
|
|
226
|
+
- NEVER 直接 node_create
|
|
227
|
+
- NEVER 跳过 capability_list → capability_select 流程
|
|
228
|
+
|
|
229
|
+
**如果 Skill 不可用**,使用 plugin_path 获取路径后 Read:
|
|
230
|
+
\`\`\`
|
|
231
|
+
plugin_path(type: "skill", name: "bootstrapping-workspace") → 获取路径
|
|
232
|
+
Read(file_path: <返回的路径>/SKILL.md)
|
|
233
|
+
\`\`\``,
|
|
234
|
+
data: {
|
|
235
|
+
skill: "bootstrapping-workspace",
|
|
236
|
+
scenario,
|
|
237
|
+
workspaceId,
|
|
238
|
+
webUrl: result.webUrl,
|
|
239
|
+
projectDocs: projectDocs.totalFound > 0 ? {
|
|
240
|
+
totalFound: projectDocs.totalFound,
|
|
241
|
+
folders: projectDocs.folders,
|
|
242
|
+
} : null,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
// 16. 发送事件通知
|
|
246
|
+
eventService.emitWorkspaceUpdate(workspaceId);
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* 列出工作区
|
|
251
|
+
*/
|
|
252
|
+
async list(params) {
|
|
253
|
+
const index = await this.json.readIndex();
|
|
254
|
+
const statusFilter = params.status || "active";
|
|
255
|
+
const cwd = params.cwd;
|
|
256
|
+
let filteredWorkspaces = index.workspaces;
|
|
257
|
+
if (statusFilter !== "all") {
|
|
258
|
+
filteredWorkspaces = filteredWorkspaces.filter(ws => ws.status === statusFilter);
|
|
259
|
+
}
|
|
260
|
+
// 排序逻辑:置顶 > cwd匹配 > 更新时间
|
|
261
|
+
filteredWorkspaces = [...filteredWorkspaces].sort((a, b) => {
|
|
262
|
+
// 1. 置顶优先
|
|
263
|
+
const aPinned = a.pinned === true;
|
|
264
|
+
const bPinned = b.pinned === true;
|
|
265
|
+
if (aPinned && !bPinned)
|
|
266
|
+
return -1;
|
|
267
|
+
if (!aPinned && bPinned)
|
|
268
|
+
return 1;
|
|
269
|
+
// 2. cwd 匹配优先(如果提供了 cwd)
|
|
270
|
+
if (cwd) {
|
|
271
|
+
const aMatch = a.projectRoot === cwd || cwd.startsWith(a.projectRoot + "/");
|
|
272
|
+
const bMatch = b.projectRoot === cwd || cwd.startsWith(b.projectRoot + "/");
|
|
273
|
+
if (aMatch && !bMatch)
|
|
274
|
+
return -1;
|
|
275
|
+
if (!aMatch && bMatch)
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
// 3. 按更新时间降序
|
|
279
|
+
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
|
280
|
+
});
|
|
281
|
+
// 为每个工作区添加 webUrl 和 hasWarning 状态
|
|
282
|
+
const port = getHttpPort();
|
|
283
|
+
const workspaces = filteredWorkspaces.map(ws => ({
|
|
284
|
+
...ws,
|
|
285
|
+
webUrl: `http://localhost:${port}/workspace/${ws.id}`,
|
|
286
|
+
hasWarning: ws.hasUnresolvedIssues === true,
|
|
287
|
+
}));
|
|
288
|
+
// 检查是否有错误状态的工作区,添加排查提示
|
|
289
|
+
const errorWorkspaces = workspaces.filter(ws => ws.status === "error");
|
|
290
|
+
let hint;
|
|
291
|
+
if (errorWorkspaces.length > 0) {
|
|
292
|
+
const errorIds = errorWorkspaces.map(ws => ws.id).join(", ");
|
|
293
|
+
hint = `⚠️ 发现 ${errorWorkspaces.length} 个错误状态的工作区(${errorIds})。\n\n` +
|
|
294
|
+
"**排查建议**:\n" +
|
|
295
|
+
"1. 检查工作区目录是否存在(可能被误删或移动)\n" +
|
|
296
|
+
"2. 检查 workspace.json 和 graph.json 文件是否完整\n" +
|
|
297
|
+
"3. 查看 error.log 获取详细错误信息:~/.xtep-workspace[-dev]/error.log\n" +
|
|
298
|
+
"4. 如果无法修复,可使用 workspace_delete(force=true) 删除错误工作区";
|
|
299
|
+
}
|
|
300
|
+
return { workspaces, hint };
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 获取工作区详情
|
|
304
|
+
*/
|
|
305
|
+
async get(params) {
|
|
306
|
+
const { workspaceId } = params;
|
|
307
|
+
// 通过索引查找工作区条目(获取 projectRoot 和 status)
|
|
308
|
+
const index = await this.json.readIndex();
|
|
309
|
+
const wsEntry = index.workspaces.find(ws => ws.id === workspaceId);
|
|
310
|
+
if (!wsEntry) {
|
|
311
|
+
devLog.workspaceLookup(workspaceId, false);
|
|
312
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 不存在`);
|
|
313
|
+
}
|
|
314
|
+
const { projectRoot, status } = wsEntry;
|
|
315
|
+
let wsDirName = wsEntry.dirName || wsEntry.id; // 向后兼容
|
|
316
|
+
const isArchived = status === "archived";
|
|
317
|
+
devLog.workspaceLookup(workspaceId, true, status);
|
|
318
|
+
// 如果工作区处于 error 状态,返回错误信息而不是尝试读取文件
|
|
319
|
+
if (status === "error" && wsEntry.errorInfo) {
|
|
320
|
+
throw new XtepError("WORKSPACE_ERROR", `工作区 "${workspaceId}" 处于错误状态: ${wsEntry.errorInfo.message}\n` +
|
|
321
|
+
`错误类型: ${wsEntry.errorInfo.type}\n` +
|
|
322
|
+
`检测时间: ${wsEntry.errorInfo.detectedAt}\n\n` +
|
|
323
|
+
`**修复建议**:\n` +
|
|
324
|
+
`- WebUI: 在首页找到该工作区,点击"查看错误"进行诊断和修复\n` +
|
|
325
|
+
`- CLI: 运行 xtep-workspace repair ${workspaceId}\n` +
|
|
326
|
+
`- 强制删除: workspace_delete(workspaceId, force=true)`);
|
|
327
|
+
}
|
|
328
|
+
// 验证项目目录存在(根据归档状态选择正确路径)
|
|
329
|
+
let workspacePath = this.fs.getWorkspaceBasePath(projectRoot, wsDirName, isArchived);
|
|
330
|
+
devLog.archivePath(workspaceId, isArchived, workspacePath);
|
|
331
|
+
if (!(await this.fs.exists(workspacePath))) {
|
|
332
|
+
// 尝试自动修复:查找可能存在的正确目录
|
|
333
|
+
const fixedDirName = await this.tryFixWorkspaceDir(workspaceId, projectRoot, wsDirName, isArchived);
|
|
334
|
+
if (fixedDirName) {
|
|
335
|
+
wsDirName = fixedDirName;
|
|
336
|
+
workspacePath = this.fs.getWorkspaceBasePath(projectRoot, wsDirName, isArchived);
|
|
337
|
+
// 更新 index.json 中的 dirName
|
|
338
|
+
wsEntry.dirName = fixedDirName;
|
|
339
|
+
await this.json.writeIndex(index);
|
|
340
|
+
devLog.debug(`自动修复工作区目录名: ${wsEntry.dirName} → ${fixedDirName}`);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
devLog.fileError("exists", workspacePath, new Error("目录不存在"));
|
|
344
|
+
// 标记为 error 状态而不是删除
|
|
345
|
+
await this.markAsError(workspaceId, "dir_missing", `工作区目录不存在: ${workspacePath}`);
|
|
346
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 的项目目录不存在(已标记为错误状态,可通过 workspace_list 查看)`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const config = await this.json.readWorkspaceConfig(projectRoot, wsDirName, isArchived);
|
|
350
|
+
// 读取 graph 时捕获版本过高错误
|
|
351
|
+
let graph;
|
|
352
|
+
try {
|
|
353
|
+
graph = await this.json.readGraph(projectRoot, wsDirName, isArchived);
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
if (e instanceof XtepError && e.code === "VERSION_TOO_HIGH") {
|
|
357
|
+
await this.markAsError(workspaceId, "version_too_high", e.message);
|
|
358
|
+
throw e;
|
|
359
|
+
}
|
|
360
|
+
throw e;
|
|
361
|
+
}
|
|
362
|
+
const workspaceMd = await this.md.readWorkspaceMdRaw(projectRoot, wsDirName, isArchived);
|
|
363
|
+
// 读取完整日志(原始表格格式)
|
|
364
|
+
// 压缩处理在 MCP 层通过 OutputAdapter 完成
|
|
365
|
+
const logMd = await this.md.readLogRaw(projectRoot, wsDirName, undefined, isArchived);
|
|
366
|
+
// 解析规则并计算哈希
|
|
367
|
+
const workspaceMdData = await this.md.readWorkspaceMd(projectRoot, wsDirName, isArchived);
|
|
368
|
+
const rulesCount = workspaceMdData.rules.length;
|
|
369
|
+
const rulesHash = rulesCount > 0
|
|
370
|
+
? crypto.createHash("md5").update(workspaceMdData.rules.join("\n")).digest("hex").substring(0, 8)
|
|
371
|
+
: "";
|
|
372
|
+
// 清除手动变更清单(AI 已获取工作区详情,无需再提醒历史变更)
|
|
373
|
+
// 注意:只在非归档状态下清除,归档工作区为只读
|
|
374
|
+
if (!isArchived) {
|
|
375
|
+
try {
|
|
376
|
+
await this.clearManualChanges(workspaceId);
|
|
377
|
+
}
|
|
378
|
+
catch (error) {
|
|
379
|
+
// 清除失败不影响主流程
|
|
380
|
+
devLog.warn("清除手动变更失败", { workspaceId, error });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// 节点完整性检测(并行检测)
|
|
384
|
+
const issues = await this.validateNodesIntegrity(projectRoot, wsDirName, graph, isArchived);
|
|
385
|
+
// 构建轻量级拓扑结构
|
|
386
|
+
// 1. 计算 focusPath(从 currentFocus 到 root 的路径)
|
|
387
|
+
const focusPath = new Set();
|
|
388
|
+
let currentNodeId = graph.currentFocus;
|
|
389
|
+
while (currentNodeId) {
|
|
390
|
+
focusPath.add(currentNodeId);
|
|
391
|
+
const node = graph.nodes[currentNodeId];
|
|
392
|
+
if (!node || currentNodeId === "root")
|
|
393
|
+
break;
|
|
394
|
+
currentNodeId = node.parentId || "";
|
|
395
|
+
}
|
|
396
|
+
// 2. 收集各节点的标题(并行读取所有节点的 info.md)
|
|
397
|
+
const nodeIds = Object.keys(graph.nodes);
|
|
398
|
+
const titles = {};
|
|
399
|
+
await Promise.all(nodeIds.map(async (nodeId) => {
|
|
400
|
+
const node = graph.nodes[nodeId];
|
|
401
|
+
const nodeDirName = node.dirName || nodeId;
|
|
402
|
+
try {
|
|
403
|
+
const nodeInfo = await this.md.readNodeInfo(projectRoot, wsDirName, nodeDirName, isArchived);
|
|
404
|
+
titles[nodeId] = nodeInfo.title || nodeId;
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// 读取失败时使用 nodeId 作为标题
|
|
408
|
+
titles[nodeId] = nodeId;
|
|
409
|
+
}
|
|
410
|
+
}));
|
|
411
|
+
// 3. 构建拓扑结构
|
|
412
|
+
const topology = this.buildTopology(graph.nodes, "root", focusPath, titles);
|
|
413
|
+
// 构建轻量 graph(移除 nodes,用 topology 替代)
|
|
414
|
+
// 压缩 memos:只保留 id, title, summary
|
|
415
|
+
const lightMemos = graph.memos
|
|
416
|
+
? Object.fromEntries(Object.entries(graph.memos).map(([key, memo]) => [
|
|
417
|
+
key,
|
|
418
|
+
{ id: memo.id, title: memo.title, summary: memo.summary },
|
|
419
|
+
]))
|
|
420
|
+
: undefined;
|
|
421
|
+
const lightGraph = {
|
|
422
|
+
version: graph.version,
|
|
423
|
+
currentFocus: graph.currentFocus,
|
|
424
|
+
lastWriteCodeVersion: graph.lastWriteCodeVersion,
|
|
425
|
+
memos: lightMemos,
|
|
426
|
+
};
|
|
427
|
+
const result = {
|
|
428
|
+
config,
|
|
429
|
+
graph: lightGraph,
|
|
430
|
+
workspaceMd,
|
|
431
|
+
logMd,
|
|
432
|
+
webUrl: `http://localhost:${getHttpPort()}/workspace/${workspaceId}`,
|
|
433
|
+
rulesCount,
|
|
434
|
+
rulesHash,
|
|
435
|
+
topology,
|
|
436
|
+
};
|
|
437
|
+
// 如果有问题,添加 warning 字段并设置警告标记
|
|
438
|
+
if (issues.length > 0) {
|
|
439
|
+
// 检查是否应该显示警告(24小时内只警告一次)
|
|
440
|
+
const shouldWarn = await this.shouldShowWarning(workspaceId);
|
|
441
|
+
if (shouldWarn) {
|
|
442
|
+
await this.setWarningFlag(workspaceId, issues);
|
|
443
|
+
}
|
|
444
|
+
result.warning = {
|
|
445
|
+
message: `检测到 ${issues.length} 个节点完整性问题`,
|
|
446
|
+
issues,
|
|
447
|
+
suggestion: "可使用 workspace_health 工具查看详情并进行修复",
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
// 检测通过,清除警告标记
|
|
452
|
+
await this.clearWarningFlag(workspaceId);
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* 删除工作区
|
|
458
|
+
*/
|
|
459
|
+
async delete(params) {
|
|
460
|
+
const { workspaceId, force = false } = params;
|
|
461
|
+
// 通过索引查找
|
|
462
|
+
const index = await this.json.readIndex();
|
|
463
|
+
const wsEntry = index.workspaces.find(ws => ws.id === workspaceId);
|
|
464
|
+
if (!wsEntry) {
|
|
465
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 不存在`);
|
|
466
|
+
}
|
|
467
|
+
// 检查状态
|
|
468
|
+
if (wsEntry.status === "active" && !force) {
|
|
469
|
+
throw new XtepError("WORKSPACE_ACTIVE", `工作区 "${workspaceId}" 处于活动状态,使用 force=true 强制删除`);
|
|
470
|
+
}
|
|
471
|
+
// 注意:不清理 Git 分支,用户可能选择保留分支用于对比
|
|
472
|
+
// 孤儿分支可通过 git branch -D xtep_workspace/... 手动清理
|
|
473
|
+
// 删除项目内目录(使用 dirName)
|
|
474
|
+
const wsDirName = wsEntry.dirName || wsEntry.id; // 向后兼容
|
|
475
|
+
const workspacePath = this.fs.getWorkspacePath(wsEntry.projectRoot, wsDirName);
|
|
476
|
+
if (await this.fs.exists(workspacePath)) {
|
|
477
|
+
await this.fs.rmdir(workspacePath);
|
|
478
|
+
}
|
|
479
|
+
// 更新全局索引
|
|
480
|
+
index.workspaces = index.workspaces.filter(ws => ws.id !== workspaceId);
|
|
481
|
+
await this.json.writeIndex(index);
|
|
482
|
+
// 发送事件通知
|
|
483
|
+
eventService.emitWorkspaceUpdate(workspaceId);
|
|
484
|
+
return { success: true };
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* 获取工作区状态(可视化输出)
|
|
488
|
+
*/
|
|
489
|
+
async status(params) {
|
|
490
|
+
const { workspaceId, format = "box" } = params;
|
|
491
|
+
// 通过索引查找工作区条目(获取 projectRoot 和 status)
|
|
492
|
+
const index = await this.json.readIndex();
|
|
493
|
+
const wsEntry = index.workspaces.find(ws => ws.id === workspaceId);
|
|
494
|
+
if (!wsEntry) {
|
|
495
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 不存在`);
|
|
496
|
+
}
|
|
497
|
+
const { projectRoot, status } = wsEntry;
|
|
498
|
+
const wsDirName = wsEntry.dirName || wsEntry.id; // 向后兼容
|
|
499
|
+
const isArchived = status === "archived";
|
|
500
|
+
// 验证项目目录存在(根据归档状态选择正确路径)
|
|
501
|
+
const workspacePath = this.fs.getWorkspaceBasePath(projectRoot, wsDirName, isArchived);
|
|
502
|
+
if (!(await this.fs.exists(workspacePath))) {
|
|
503
|
+
// 标记为 error 状态而不是删除
|
|
504
|
+
await this.markAsError(workspaceId, "dir_missing", `工作区目录不存在: ${workspacePath}`);
|
|
505
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 的项目目录不存在(已标记为错误状态)`);
|
|
506
|
+
}
|
|
507
|
+
const config = await this.json.readWorkspaceConfig(projectRoot, wsDirName, isArchived);
|
|
508
|
+
const graph = await this.json.readGraph(projectRoot, wsDirName, isArchived);
|
|
509
|
+
// 从根节点读取 goal(requirement 字段)- goal 已统一到根节点
|
|
510
|
+
const rootNodeId = config.rootNodeId || "root";
|
|
511
|
+
const rootNodeMeta = graph.nodes[rootNodeId];
|
|
512
|
+
const rootNodeDirName = rootNodeMeta?.dirName || rootNodeId;
|
|
513
|
+
let rootNodeInfo = await this.md.readNodeInfo(projectRoot, wsDirName, rootNodeDirName, isArchived);
|
|
514
|
+
let goal = rootNodeInfo.requirement || "";
|
|
515
|
+
// 懒迁移:如果根节点 requirement 为空,检查旧版 Workspace.md 中是否有 goal
|
|
516
|
+
// 仅对非归档工作区执行迁移(归档工作区为只读)
|
|
517
|
+
if (!goal && !isArchived) {
|
|
518
|
+
const legacyGoal = await this.md.readLegacyGoal(projectRoot, wsDirName, false);
|
|
519
|
+
if (legacyGoal) {
|
|
520
|
+
// 将旧版 goal 迁移到根节点 requirement
|
|
521
|
+
rootNodeInfo = {
|
|
522
|
+
...rootNodeInfo,
|
|
523
|
+
requirement: legacyGoal,
|
|
524
|
+
};
|
|
525
|
+
await this.md.writeNodeInfo(projectRoot, wsDirName, rootNodeDirName, rootNodeInfo);
|
|
526
|
+
goal = legacyGoal;
|
|
527
|
+
devLog.debug("懒迁移完成", { workspaceId, goal: legacyGoal.substring(0, 50) });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// 计算统计信息(终态 = completed + failed + cancelled)
|
|
531
|
+
const nodes = Object.values(graph.nodes);
|
|
532
|
+
const totalNodes = nodes.length;
|
|
533
|
+
const terminalStatuses = new Set(["completed", "failed", "cancelled"]);
|
|
534
|
+
const completedNodes = nodes.filter(n => terminalStatuses.has(n.status)).length;
|
|
535
|
+
const summary = {
|
|
536
|
+
name: config.name,
|
|
537
|
+
goal,
|
|
538
|
+
status: config.status,
|
|
539
|
+
totalNodes,
|
|
540
|
+
completedNodes,
|
|
541
|
+
currentFocus: graph.currentFocus,
|
|
542
|
+
};
|
|
543
|
+
// 生成输出
|
|
544
|
+
let output;
|
|
545
|
+
if (format === "markdown") {
|
|
546
|
+
output = await this.generateMarkdownStatus(projectRoot, wsDirName, config, graph, summary, isArchived);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
output = await this.generateBoxStatus(projectRoot, wsDirName, config, graph, summary, isArchived);
|
|
550
|
+
}
|
|
551
|
+
// 收集 memo 信息
|
|
552
|
+
const memosIndex = graph.memos || {};
|
|
553
|
+
const memosList = Object.values(memosIndex);
|
|
554
|
+
// 收集所有已使用的 tags
|
|
555
|
+
const allTagsSet = new Set();
|
|
556
|
+
memosList.forEach(memo => {
|
|
557
|
+
memo.tags.forEach(tag => allTagsSet.add(tag));
|
|
558
|
+
});
|
|
559
|
+
const allTags = Array.from(allTagsSet).sort();
|
|
560
|
+
// 按更新时间倒序排序
|
|
561
|
+
memosList.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
562
|
+
return {
|
|
563
|
+
output,
|
|
564
|
+
summary,
|
|
565
|
+
webUrl: `http://localhost:${getHttpPort()}/workspace/${workspaceId}`,
|
|
566
|
+
memos: memosList.length > 0 ? {
|
|
567
|
+
items: memosList.map(m => ({
|
|
568
|
+
id: m.id,
|
|
569
|
+
title: m.title,
|
|
570
|
+
summary: m.summary,
|
|
571
|
+
tags: m.tags,
|
|
572
|
+
updatedAt: m.updatedAt,
|
|
573
|
+
})),
|
|
574
|
+
allTags,
|
|
575
|
+
totalCount: memosList.length,
|
|
576
|
+
} : undefined,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* 根据 workspaceId 获取 projectRoot(供其他服务使用)
|
|
581
|
+
*/
|
|
582
|
+
async resolveProjectRoot(workspaceId) {
|
|
583
|
+
const projectRoot = await this.json.getProjectRoot(workspaceId);
|
|
584
|
+
if (!projectRoot) {
|
|
585
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 不存在`);
|
|
586
|
+
}
|
|
587
|
+
return projectRoot;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 解析工作区位置信息(projectRoot 和 dirName)
|
|
591
|
+
* 用于需要访问文件系统的操作
|
|
592
|
+
*/
|
|
593
|
+
async resolveWorkspaceLocation(workspaceId) {
|
|
594
|
+
const location = await this.json.getWorkspaceLocation(workspaceId);
|
|
595
|
+
if (!location) {
|
|
596
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 不存在`);
|
|
597
|
+
}
|
|
598
|
+
return location;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* 生成 Box 格式状态输出
|
|
602
|
+
*/
|
|
603
|
+
async generateBoxStatus(projectRoot, wsDirName, config, graph, summary, isArchived = false) {
|
|
604
|
+
const lines = [];
|
|
605
|
+
const width = 60;
|
|
606
|
+
lines.push("┌" + "─".repeat(width - 2) + "┐");
|
|
607
|
+
lines.push("│" + ` 工作区: ${config.name}`.padEnd(width - 2) + "│");
|
|
608
|
+
lines.push("│" + ` 状态: ${config.status}`.padEnd(width - 2) + "│");
|
|
609
|
+
lines.push("├" + "─".repeat(width - 2) + "┤");
|
|
610
|
+
lines.push("│" + ` 目标: ${summary.goal.substring(0, width - 10)}`.padEnd(width - 2) + "│");
|
|
611
|
+
lines.push("├" + "─".repeat(width - 2) + "┤");
|
|
612
|
+
lines.push("│" + ` 节点统计: ${summary.completedNodes}/${summary.totalNodes} 已处理`.padEnd(width - 2) + "│");
|
|
613
|
+
lines.push("│" + ` 当前聚焦: ${summary.currentFocus || "无"}`.padEnd(width - 2) + "│");
|
|
614
|
+
// 派发模式信息
|
|
615
|
+
if (config.dispatch?.enabled) {
|
|
616
|
+
const dispatchMode = config.dispatch.useGit ? "Git 模式" : "无 Git 模式";
|
|
617
|
+
lines.push("│" + ` 派发: 已启用 (${dispatchMode})`.padEnd(width - 2) + "│");
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
lines.push("│" + ` 派发: 未启用`.padEnd(width - 2) + "│");
|
|
621
|
+
}
|
|
622
|
+
lines.push("├" + "─".repeat(width - 2) + "┤");
|
|
623
|
+
lines.push("│" + " 节点树:".padEnd(width - 2) + "│");
|
|
624
|
+
// 生成节点树
|
|
625
|
+
const treeLines = await this.generateNodeTree(projectRoot, wsDirName, graph, config.rootNodeId, 0, isArchived);
|
|
626
|
+
for (const treeLine of treeLines) {
|
|
627
|
+
const truncated = treeLine.length > width - 4 ? treeLine.substring(0, width - 7) + "..." : treeLine;
|
|
628
|
+
lines.push("│" + ` ${truncated}`.padEnd(width - 2) + "│");
|
|
629
|
+
}
|
|
630
|
+
lines.push("└" + "─".repeat(width - 2) + "┘");
|
|
631
|
+
return lines.join("\n");
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* 生成 Markdown 格式状态输出
|
|
635
|
+
*/
|
|
636
|
+
async generateMarkdownStatus(projectRoot, wsDirName, config, graph, summary, isArchived = false) {
|
|
637
|
+
const lines = [];
|
|
638
|
+
lines.push(`# ${config.name}`);
|
|
639
|
+
lines.push("");
|
|
640
|
+
lines.push(`**状态**: ${config.status}`);
|
|
641
|
+
lines.push(`**目标**: ${summary.goal}`);
|
|
642
|
+
// 派发模式信息
|
|
643
|
+
if (config.dispatch?.enabled) {
|
|
644
|
+
const dispatchMode = config.dispatch.useGit ? "Git 模式" : "无 Git 模式";
|
|
645
|
+
lines.push(`**派发模式**: 已启用 (${dispatchMode})`);
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
lines.push(`**派发模式**: 未启用`);
|
|
649
|
+
}
|
|
650
|
+
lines.push("");
|
|
651
|
+
lines.push("## 统计");
|
|
652
|
+
lines.push(`- 节点总数: ${summary.totalNodes}`);
|
|
653
|
+
lines.push(`- 已处理: ${summary.completedNodes}`);
|
|
654
|
+
lines.push(`- 当前聚焦: ${summary.currentFocus || "无"}`);
|
|
655
|
+
lines.push("");
|
|
656
|
+
lines.push("## 节点树");
|
|
657
|
+
lines.push("");
|
|
658
|
+
const treeLines = await this.generateNodeTreeMd(projectRoot, wsDirName, graph, config.rootNodeId, 0, isArchived);
|
|
659
|
+
lines.push(...treeLines);
|
|
660
|
+
return lines.join("\n");
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* 生成节点树(Box 格式)
|
|
664
|
+
*/
|
|
665
|
+
async generateNodeTree(projectRoot, wsDirName, graph, nodeId, depth, isArchived = false) {
|
|
666
|
+
const node = graph.nodes[nodeId];
|
|
667
|
+
if (!node)
|
|
668
|
+
return [];
|
|
669
|
+
const lines = [];
|
|
670
|
+
const indent = " ".repeat(depth);
|
|
671
|
+
const statusIcon = this.getStatusIcon(node.status);
|
|
672
|
+
const focusIndicator = graph.currentFocus === nodeId ? " ◄" : "";
|
|
673
|
+
// 读取节点标题(使用 dirName 或回退到 nodeId)
|
|
674
|
+
const nodeDirName = node.dirName || nodeId;
|
|
675
|
+
const nodeInfo = await this.md.readNodeInfo(projectRoot, wsDirName, nodeDirName, isArchived);
|
|
676
|
+
const title = nodeInfo.title || nodeId;
|
|
677
|
+
lines.push(`${indent}${statusIcon} ${title}${focusIndicator}`);
|
|
678
|
+
for (const childId of node.children) {
|
|
679
|
+
lines.push(...await this.generateNodeTree(projectRoot, wsDirName, graph, childId, depth + 1, isArchived));
|
|
680
|
+
}
|
|
681
|
+
return lines;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* 生成节点树(Markdown 格式)
|
|
685
|
+
*/
|
|
686
|
+
async generateNodeTreeMd(projectRoot, wsDirName, graph, nodeId, depth, isArchived = false) {
|
|
687
|
+
const node = graph.nodes[nodeId];
|
|
688
|
+
if (!node)
|
|
689
|
+
return [];
|
|
690
|
+
const lines = [];
|
|
691
|
+
const indent = " ".repeat(depth);
|
|
692
|
+
const statusIcon = this.getStatusIcon(node.status);
|
|
693
|
+
const focusIndicator = graph.currentFocus === nodeId ? " **◄ 当前聚焦**" : "";
|
|
694
|
+
// 读取节点标题(使用 dirName 或回退到 nodeId)
|
|
695
|
+
const nodeDirName = node.dirName || nodeId;
|
|
696
|
+
const nodeInfo = await this.md.readNodeInfo(projectRoot, wsDirName, nodeDirName, isArchived);
|
|
697
|
+
const title = nodeInfo.title || nodeId;
|
|
698
|
+
lines.push(`${indent}- ${statusIcon} ${title}${focusIndicator}`);
|
|
699
|
+
for (const childId of node.children) {
|
|
700
|
+
lines.push(...await this.generateNodeTreeMd(projectRoot, wsDirName, graph, childId, depth + 1, isArchived));
|
|
701
|
+
}
|
|
702
|
+
return lines;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* 获取状态图标
|
|
706
|
+
*/
|
|
707
|
+
getStatusIcon(status) {
|
|
708
|
+
switch (status) {
|
|
709
|
+
// 通用状态
|
|
710
|
+
case "pending":
|
|
711
|
+
return "○";
|
|
712
|
+
case "completed":
|
|
713
|
+
return "●";
|
|
714
|
+
// 执行节点状态
|
|
715
|
+
case "implementing":
|
|
716
|
+
return "◐";
|
|
717
|
+
case "validating":
|
|
718
|
+
return "◑";
|
|
719
|
+
case "failed":
|
|
720
|
+
return "✕";
|
|
721
|
+
// 规划节点状态
|
|
722
|
+
case "planning":
|
|
723
|
+
return "◇";
|
|
724
|
+
case "monitoring":
|
|
725
|
+
return "◈";
|
|
726
|
+
case "cancelled":
|
|
727
|
+
return "⊘";
|
|
728
|
+
default:
|
|
729
|
+
return "?";
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* 切换工作区置顶状态
|
|
734
|
+
* @param workspaceId 工作区 ID
|
|
735
|
+
* @returns 新的置顶状态
|
|
736
|
+
*/
|
|
737
|
+
async togglePin(workspaceId) {
|
|
738
|
+
const index = await this.json.readIndex();
|
|
739
|
+
const wsEntry = index.workspaces.find(ws => ws.id === workspaceId);
|
|
740
|
+
if (!wsEntry) {
|
|
741
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 不存在`);
|
|
742
|
+
}
|
|
743
|
+
// 切换置顶状态(false 时删除字段以节省空间)
|
|
744
|
+
const newPinned = !wsEntry.pinned;
|
|
745
|
+
wsEntry.pinned = newPinned ? true : undefined;
|
|
746
|
+
wsEntry.updatedAt = now();
|
|
747
|
+
await this.json.writeIndex(index);
|
|
748
|
+
// 发送事件通知
|
|
749
|
+
eventService.emitWorkspaceUpdate(workspaceId);
|
|
750
|
+
return { pinned: newPinned };
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* 更新工作区规则
|
|
754
|
+
*/
|
|
755
|
+
async updateRules(params) {
|
|
756
|
+
const { workspaceId, action, rule, rules } = params;
|
|
757
|
+
// 获取工作区位置信息
|
|
758
|
+
const { projectRoot, dirName: wsDirName } = await this.resolveWorkspaceLocation(workspaceId);
|
|
759
|
+
// 读取当前工作区数据
|
|
760
|
+
const workspaceMdData = await this.md.readWorkspaceMd(projectRoot, wsDirName);
|
|
761
|
+
let currentRules = [...workspaceMdData.rules];
|
|
762
|
+
// 执行操作
|
|
763
|
+
switch (action) {
|
|
764
|
+
case "add":
|
|
765
|
+
if (!rule) {
|
|
766
|
+
throw new XtepError("INVALID_PARAMS", "add 操作需要提供 rule 参数");
|
|
767
|
+
}
|
|
768
|
+
if (!currentRules.includes(rule)) {
|
|
769
|
+
currentRules.push(rule);
|
|
770
|
+
}
|
|
771
|
+
break;
|
|
772
|
+
case "remove":
|
|
773
|
+
if (!rule) {
|
|
774
|
+
throw new XtepError("INVALID_PARAMS", "remove 操作需要提供 rule 参数");
|
|
775
|
+
}
|
|
776
|
+
currentRules = currentRules.filter(r => r !== rule);
|
|
777
|
+
break;
|
|
778
|
+
case "replace":
|
|
779
|
+
if (!rules) {
|
|
780
|
+
throw new XtepError("INVALID_PARAMS", "replace 操作需要提供 rules 参数");
|
|
781
|
+
}
|
|
782
|
+
currentRules = [...rules];
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
// 更新 Workspace.md
|
|
786
|
+
workspaceMdData.rules = currentRules;
|
|
787
|
+
workspaceMdData.updatedAt = now();
|
|
788
|
+
await this.md.writeWorkspaceMd(projectRoot, wsDirName, workspaceMdData);
|
|
789
|
+
// 计算新的哈希
|
|
790
|
+
const rulesHash = currentRules.length > 0
|
|
791
|
+
? crypto.createHash("md5").update(currentRules.join("\n")).digest("hex").substring(0, 8)
|
|
792
|
+
: "";
|
|
793
|
+
// 发送事件通知
|
|
794
|
+
eventService.emitWorkspaceUpdate(workspaceId);
|
|
795
|
+
return {
|
|
796
|
+
success: true,
|
|
797
|
+
rulesCount: currentRules.length,
|
|
798
|
+
rulesHash,
|
|
799
|
+
rules: currentRules,
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* 归档工作区
|
|
804
|
+
*/
|
|
805
|
+
async archive(params) {
|
|
806
|
+
const { workspaceId } = params;
|
|
807
|
+
// 1. 通过索引查找工作区
|
|
808
|
+
const index = await this.json.readIndex();
|
|
809
|
+
const wsEntry = index.workspaces.find(ws => ws.id === workspaceId);
|
|
810
|
+
if (!wsEntry) {
|
|
811
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 不存在`);
|
|
812
|
+
}
|
|
813
|
+
// 2. 验证状态为 active
|
|
814
|
+
if (wsEntry.status !== "active") {
|
|
815
|
+
throw new XtepError("WORKSPACE_ARCHIVED", `工作区 "${workspaceId}" 已经处于归档状态`);
|
|
816
|
+
}
|
|
817
|
+
const { projectRoot } = wsEntry;
|
|
818
|
+
const dirName = wsEntry.dirName || workspaceId;
|
|
819
|
+
const currentTime = now();
|
|
820
|
+
// 3. 验证源目录存在
|
|
821
|
+
const srcPath = this.fs.getWorkspacePath(projectRoot, dirName);
|
|
822
|
+
if (!(await this.fs.exists(srcPath))) {
|
|
823
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区目录不存在: ${srcPath}`);
|
|
824
|
+
}
|
|
825
|
+
// 注意:归档时不清理 Git 分支,用户可能选择保留分支用于对比
|
|
826
|
+
// 4. 确保归档目录存在
|
|
827
|
+
await this.fs.ensureArchiveDir(projectRoot);
|
|
828
|
+
// 5. 移动目录到归档位置
|
|
829
|
+
const archivePath = this.fs.getArchivePath(projectRoot, dirName);
|
|
830
|
+
await this.fs.moveDir(srcPath, archivePath);
|
|
831
|
+
// 6. 更新索引状态
|
|
832
|
+
wsEntry.status = "archived";
|
|
833
|
+
wsEntry.updatedAt = currentTime;
|
|
834
|
+
await this.json.writeIndex(index);
|
|
835
|
+
// 7. 更新 workspace.json 状态
|
|
836
|
+
const config = await this.json.readWorkspaceConfig(projectRoot, dirName, true);
|
|
837
|
+
config.status = "archived";
|
|
838
|
+
config.updatedAt = currentTime;
|
|
839
|
+
await this.json.writeWorkspaceConfig(projectRoot, dirName, config, true);
|
|
840
|
+
// 8. 追加日志
|
|
841
|
+
await this.md.appendLog(projectRoot, dirName, {
|
|
842
|
+
time: currentTime,
|
|
843
|
+
operator: "system",
|
|
844
|
+
event: `工作区已归档`,
|
|
845
|
+
}, true);
|
|
846
|
+
// 9. 发送事件通知
|
|
847
|
+
eventService.emitWorkspaceUpdate(workspaceId);
|
|
848
|
+
return {
|
|
849
|
+
success: true,
|
|
850
|
+
archivePath,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* 恢复归档的工作区
|
|
855
|
+
*/
|
|
856
|
+
async restore(params) {
|
|
857
|
+
const { workspaceId } = params;
|
|
858
|
+
// 1. 通过索引查找工作区
|
|
859
|
+
const index = await this.json.readIndex();
|
|
860
|
+
const wsEntry = index.workspaces.find(ws => ws.id === workspaceId);
|
|
861
|
+
if (!wsEntry) {
|
|
862
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区 "${workspaceId}" 不存在`);
|
|
863
|
+
}
|
|
864
|
+
// 2. 验证状态为 archived
|
|
865
|
+
if (wsEntry.status !== "archived") {
|
|
866
|
+
throw new XtepError("WORKSPACE_ACTIVE", `工作区 "${workspaceId}" 不是归档状态,无需恢复`);
|
|
867
|
+
}
|
|
868
|
+
const { projectRoot } = wsEntry;
|
|
869
|
+
const dirName = wsEntry.dirName || workspaceId;
|
|
870
|
+
const currentTime = now();
|
|
871
|
+
// 3. 验证归档目录存在
|
|
872
|
+
const archivePath = this.fs.getArchivePath(projectRoot, dirName);
|
|
873
|
+
if (!(await this.fs.exists(archivePath))) {
|
|
874
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `归档工作区目录不存在: ${archivePath}`);
|
|
875
|
+
}
|
|
876
|
+
// 4. 移动目录回原位置
|
|
877
|
+
const destPath = this.fs.getWorkspacePath(projectRoot, dirName);
|
|
878
|
+
await this.fs.moveDir(archivePath, destPath);
|
|
879
|
+
// 5. 更新索引状态
|
|
880
|
+
wsEntry.status = "active";
|
|
881
|
+
wsEntry.updatedAt = currentTime;
|
|
882
|
+
await this.json.writeIndex(index);
|
|
883
|
+
// 6. 更新 workspace.json 状态
|
|
884
|
+
const config = await this.json.readWorkspaceConfig(projectRoot, dirName);
|
|
885
|
+
config.status = "active";
|
|
886
|
+
config.updatedAt = currentTime;
|
|
887
|
+
await this.json.writeWorkspaceConfig(projectRoot, dirName, config);
|
|
888
|
+
// 7. 追加日志
|
|
889
|
+
await this.md.appendLog(projectRoot, dirName, {
|
|
890
|
+
time: currentTime,
|
|
891
|
+
operator: "system",
|
|
892
|
+
event: `工作区已从归档恢复`,
|
|
893
|
+
});
|
|
894
|
+
// 8. 发送事件通知
|
|
895
|
+
eventService.emitWorkspaceUpdate(workspaceId);
|
|
896
|
+
return {
|
|
897
|
+
success: true,
|
|
898
|
+
path: destPath,
|
|
899
|
+
webUrl: `http://localhost:${getHttpPort()}/workspace/${workspaceId}`,
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
// ========== 项目文档扫描 ==========
|
|
903
|
+
/** 排除的目录名 */
|
|
904
|
+
static EXCLUDED_DIRS = new Set([
|
|
905
|
+
"node_modules",
|
|
906
|
+
".git",
|
|
907
|
+
".xtep-workspace",
|
|
908
|
+
".xtep-workspace-dev",
|
|
909
|
+
"dist",
|
|
910
|
+
"build",
|
|
911
|
+
"coverage",
|
|
912
|
+
".next",
|
|
913
|
+
".nuxt",
|
|
914
|
+
".output",
|
|
915
|
+
"__pycache__",
|
|
916
|
+
".venv",
|
|
917
|
+
"venv",
|
|
918
|
+
]);
|
|
919
|
+
/** 文件数量限制 */
|
|
920
|
+
static MAX_FILES = 50;
|
|
921
|
+
/**
|
|
922
|
+
* 扫描项目文档
|
|
923
|
+
* 扫描 1-2 级目录的 .md 文件,检测元文件,限制文件数
|
|
924
|
+
*/
|
|
925
|
+
async scanProjectDocs(projectRoot) {
|
|
926
|
+
const files = [];
|
|
927
|
+
const folders = [];
|
|
928
|
+
let totalFound = 0;
|
|
929
|
+
try {
|
|
930
|
+
// 扫描根目录的 .md 文件
|
|
931
|
+
const rootEntries = await fs.readdir(projectRoot, { withFileTypes: true });
|
|
932
|
+
for (const entry of rootEntries) {
|
|
933
|
+
if (entry.name.startsWith(".") && entry.name !== ".")
|
|
934
|
+
continue;
|
|
935
|
+
if (WorkspaceService.EXCLUDED_DIRS.has(entry.name))
|
|
936
|
+
continue;
|
|
937
|
+
const entryPath = path.join(projectRoot, entry.name);
|
|
938
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
939
|
+
// 根目录 .md 文件
|
|
940
|
+
totalFound++;
|
|
941
|
+
if (files.length < WorkspaceService.MAX_FILES) {
|
|
942
|
+
const hasFrontmatter = await this.checkFrontmatter(entryPath);
|
|
943
|
+
files.push({ path: entry.name, hasFrontmatter });
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
else if (entry.isDirectory()) {
|
|
947
|
+
// 扫描一级子目录
|
|
948
|
+
await this.scanSubDirectory(projectRoot, entry.name, 1, files, folders, { total: totalFound }).then(count => { totalFound = count; });
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
// 扫描失败时返回空结果
|
|
954
|
+
return { files: [], folders: [], totalFound: 0, degraded: false };
|
|
955
|
+
}
|
|
956
|
+
const degraded = totalFound > WorkspaceService.MAX_FILES;
|
|
957
|
+
// 如果退化模式,只保留文件夹信息
|
|
958
|
+
if (degraded) {
|
|
959
|
+
return {
|
|
960
|
+
files: files.slice(0, 10), // 保留少量示例文件
|
|
961
|
+
folders,
|
|
962
|
+
totalFound,
|
|
963
|
+
degraded: true,
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
return { files, folders, totalFound, degraded: false };
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* 扫描子目录
|
|
970
|
+
*/
|
|
971
|
+
async scanSubDirectory(projectRoot, relativePath, depth, files, folders, counter) {
|
|
972
|
+
if (depth > 2)
|
|
973
|
+
return counter.total;
|
|
974
|
+
const fullPath = path.join(projectRoot, relativePath);
|
|
975
|
+
try {
|
|
976
|
+
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
|
977
|
+
let allMd = true;
|
|
978
|
+
let hasMd = false;
|
|
979
|
+
for (const entry of entries) {
|
|
980
|
+
if (entry.name.startsWith("."))
|
|
981
|
+
continue;
|
|
982
|
+
if (WorkspaceService.EXCLUDED_DIRS.has(entry.name))
|
|
983
|
+
continue;
|
|
984
|
+
const entryRelPath = path.join(relativePath, entry.name);
|
|
985
|
+
const entryFullPath = path.join(fullPath, entry.name);
|
|
986
|
+
if (entry.isFile()) {
|
|
987
|
+
if (entry.name.endsWith(".md")) {
|
|
988
|
+
hasMd = true;
|
|
989
|
+
counter.total++;
|
|
990
|
+
if (files.length < WorkspaceService.MAX_FILES) {
|
|
991
|
+
const hasFrontmatter = await this.checkFrontmatter(entryFullPath);
|
|
992
|
+
files.push({ path: entryRelPath, hasFrontmatter });
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
allMd = false;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
else if (entry.isDirectory()) {
|
|
1000
|
+
allMd = false;
|
|
1001
|
+
// 检查 3 级目录是否全是 .md
|
|
1002
|
+
if (depth === 2) {
|
|
1003
|
+
const isDocFolder = await this.isDocFolder(entryFullPath);
|
|
1004
|
+
if (isDocFolder) {
|
|
1005
|
+
folders.push(entryRelPath);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
// 继续扫描下一级
|
|
1010
|
+
await this.scanSubDirectory(projectRoot, entryRelPath, depth + 1, files, folders, counter);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// 如果当前目录全是 .md 文件且有文件,标记为文档文件夹
|
|
1015
|
+
if (depth === 2 && allMd && hasMd && !folders.includes(relativePath)) {
|
|
1016
|
+
folders.push(relativePath);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
catch {
|
|
1020
|
+
// 目录读取失败,跳过
|
|
1021
|
+
}
|
|
1022
|
+
return counter.total;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* 检查文件是否有 frontmatter(以 --- 开头)
|
|
1026
|
+
*/
|
|
1027
|
+
async checkFrontmatter(filePath) {
|
|
1028
|
+
try {
|
|
1029
|
+
const fd = await fs.open(filePath, "r");
|
|
1030
|
+
const buffer = Buffer.alloc(4);
|
|
1031
|
+
await fd.read(buffer, 0, 4, 0);
|
|
1032
|
+
await fd.close();
|
|
1033
|
+
return buffer.toString("utf-8").startsWith("---");
|
|
1034
|
+
}
|
|
1035
|
+
catch {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* 检查目录是否为文档文件夹(全是 .md 文件)
|
|
1041
|
+
*/
|
|
1042
|
+
async isDocFolder(dirPath) {
|
|
1043
|
+
try {
|
|
1044
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
1045
|
+
if (entries.length === 0)
|
|
1046
|
+
return false;
|
|
1047
|
+
for (const entry of entries) {
|
|
1048
|
+
if (entry.name.startsWith("."))
|
|
1049
|
+
continue;
|
|
1050
|
+
if (entry.isDirectory())
|
|
1051
|
+
return false;
|
|
1052
|
+
if (entry.isFile() && !entry.name.endsWith(".md"))
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
return true;
|
|
1056
|
+
}
|
|
1057
|
+
catch {
|
|
1058
|
+
return false;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* 验证节点完整性(并行检测)
|
|
1063
|
+
* 检测节点目录和 Info.md 是否存在
|
|
1064
|
+
*/
|
|
1065
|
+
async validateNodesIntegrity(projectRoot, wsDirName, graph, isArchived) {
|
|
1066
|
+
const issues = [];
|
|
1067
|
+
const nodeIds = Object.keys(graph.nodes);
|
|
1068
|
+
// 并行检测所有节点
|
|
1069
|
+
const results = await Promise.all(nodeIds.map(async (nodeId) => {
|
|
1070
|
+
const node = graph.nodes[nodeId];
|
|
1071
|
+
const nodeDirName = node.dirName || nodeId;
|
|
1072
|
+
const nodeIssues = [];
|
|
1073
|
+
// 跳过 root 节点(目录名固定为 "root")
|
|
1074
|
+
if (nodeId === "root") {
|
|
1075
|
+
nodeDirName === "root"; // 确保 root 节点目录名正确
|
|
1076
|
+
}
|
|
1077
|
+
// 获取节点目录路径
|
|
1078
|
+
const nodesDir = isArchived
|
|
1079
|
+
? `${this.fs.getArchivePath(projectRoot, wsDirName)}/nodes`
|
|
1080
|
+
: this.fs.getNodesDir(projectRoot, wsDirName);
|
|
1081
|
+
const nodePath = `${nodesDir}/${nodeDirName}`;
|
|
1082
|
+
// 1. 检测目录存在
|
|
1083
|
+
if (!(await this.fs.exists(nodePath))) {
|
|
1084
|
+
nodeIssues.push({
|
|
1085
|
+
type: "node_corrupt",
|
|
1086
|
+
severity: "error",
|
|
1087
|
+
target: nodeId,
|
|
1088
|
+
message: `节点目录不存在: ${nodeDirName}`,
|
|
1089
|
+
suggestion: "从备份恢复或删除该节点",
|
|
1090
|
+
});
|
|
1091
|
+
return nodeIssues; // 目录不存在则跳过文件检测
|
|
1092
|
+
}
|
|
1093
|
+
// 2. 检测 Info.md 存在
|
|
1094
|
+
const infoPath = `${nodePath}/Info.md`;
|
|
1095
|
+
if (!(await this.fs.exists(infoPath))) {
|
|
1096
|
+
nodeIssues.push({
|
|
1097
|
+
type: "node_corrupt",
|
|
1098
|
+
severity: "warning",
|
|
1099
|
+
target: nodeId,
|
|
1100
|
+
message: `节点 Info.md 缺失: ${nodeDirName}`,
|
|
1101
|
+
suggestion: "可尝试重建节点信息文件",
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
return nodeIssues;
|
|
1105
|
+
}));
|
|
1106
|
+
// 合并所有问题
|
|
1107
|
+
for (const nodeIssues of results) {
|
|
1108
|
+
issues.push(...nodeIssues);
|
|
1109
|
+
}
|
|
1110
|
+
return issues;
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* 尝试修复工作区目录名(当记录的目录不存在时,查找可能存在的正确目录)
|
|
1114
|
+
* @returns 修复后的目录名,如果无法修复则返回 undefined
|
|
1115
|
+
*/
|
|
1116
|
+
async tryFixWorkspaceDir(workspaceId, projectRoot, currentDirName, isArchived) {
|
|
1117
|
+
// 获取工作区根目录
|
|
1118
|
+
const baseDir = isArchived
|
|
1119
|
+
? this.fs.getArchiveDir(projectRoot)
|
|
1120
|
+
: this.fs.getWorkspaceRootPath(projectRoot);
|
|
1121
|
+
// 检查根目录是否存在
|
|
1122
|
+
if (!(await this.fs.exists(baseDir))) {
|
|
1123
|
+
return undefined;
|
|
1124
|
+
}
|
|
1125
|
+
// 提取工作区 ID 的短 ID
|
|
1126
|
+
const shortId = extractShortId(workspaceId);
|
|
1127
|
+
// 在目录中查找包含短 ID 的子目录
|
|
1128
|
+
try {
|
|
1129
|
+
const entries = await this.fs.readdir(baseDir);
|
|
1130
|
+
// 优先级 1: 精确匹配 `_shortId` 后缀(当前标准格式)
|
|
1131
|
+
for (const entry of entries) {
|
|
1132
|
+
if (entry.endsWith(`_${shortId}`)) {
|
|
1133
|
+
return entry;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
// 优先级 2: 匹配 `shortId` 后缀(无下划线,可能的早期格式)
|
|
1137
|
+
for (const entry of entries) {
|
|
1138
|
+
if (entry.endsWith(shortId) && !entry.endsWith(`_${shortId}`)) {
|
|
1139
|
+
return entry;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// 优先级 3: 包含短 ID 的任意目录
|
|
1143
|
+
for (const entry of entries) {
|
|
1144
|
+
if (entry.includes(shortId)) {
|
|
1145
|
+
return entry;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
// 读取目录失败
|
|
1151
|
+
}
|
|
1152
|
+
return undefined;
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* 将工作区标记为错误状态
|
|
1156
|
+
* @param workspaceId 工作区 ID
|
|
1157
|
+
* @param errorType 错误类型
|
|
1158
|
+
* @param message 错误信息
|
|
1159
|
+
*/
|
|
1160
|
+
async markAsError(workspaceId, errorType, message) {
|
|
1161
|
+
// 更新索引中的状态
|
|
1162
|
+
const index = await this.json.readIndex();
|
|
1163
|
+
const wsEntry = index.workspaces.find(ws => ws.id === workspaceId);
|
|
1164
|
+
// 保存原始状态(仅当不是已经处于 error 状态时)
|
|
1165
|
+
const previousStatus = wsEntry?.status !== "error" ? wsEntry?.status : wsEntry?.errorInfo?.previousStatus;
|
|
1166
|
+
const errorInfo = {
|
|
1167
|
+
message,
|
|
1168
|
+
detectedAt: now(),
|
|
1169
|
+
type: errorType,
|
|
1170
|
+
previousStatus,
|
|
1171
|
+
};
|
|
1172
|
+
if (wsEntry) {
|
|
1173
|
+
wsEntry.status = "error";
|
|
1174
|
+
wsEntry.errorInfo = errorInfo;
|
|
1175
|
+
wsEntry.updatedAt = now();
|
|
1176
|
+
await this.json.writeIndex(index);
|
|
1177
|
+
}
|
|
1178
|
+
// 记录到 error.log
|
|
1179
|
+
logError(errorType || "unknown", workspaceId, message);
|
|
1180
|
+
return errorInfo;
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* 清除工作区的错误状态(用于修复后)
|
|
1184
|
+
* 恢复为错误前的原始状态(active 或 archived)
|
|
1185
|
+
* @param workspaceId 工作区 ID
|
|
1186
|
+
*/
|
|
1187
|
+
async clearError(workspaceId) {
|
|
1188
|
+
const index = await this.json.readIndex();
|
|
1189
|
+
const wsEntry = index.workspaces.find(ws => ws.id === workspaceId);
|
|
1190
|
+
if (wsEntry && wsEntry.status === "error") {
|
|
1191
|
+
// 恢复为原始状态,默认为 active
|
|
1192
|
+
wsEntry.status = wsEntry.errorInfo?.previousStatus || "active";
|
|
1193
|
+
delete wsEntry.errorInfo;
|
|
1194
|
+
wsEntry.updatedAt = now();
|
|
1195
|
+
await this.json.writeIndex(index);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
// ========== 手动变更清单管理 ==========
|
|
1199
|
+
/**
|
|
1200
|
+
* 添加手动变更记录
|
|
1201
|
+
* @param workspaceId 工作区 ID
|
|
1202
|
+
* @param change 变更记录(不含 id,由方法生成)
|
|
1203
|
+
*/
|
|
1204
|
+
async addManualChange(workspaceId, change) {
|
|
1205
|
+
const { projectRoot, dirName } = await this.resolveWorkspaceLocation(workspaceId);
|
|
1206
|
+
const config = await this.json.readWorkspaceConfig(projectRoot, dirName);
|
|
1207
|
+
// 初始化变更清单
|
|
1208
|
+
if (!config.pendingManualChanges) {
|
|
1209
|
+
config.pendingManualChanges = [];
|
|
1210
|
+
}
|
|
1211
|
+
// 生成 ID 并添加变更
|
|
1212
|
+
const newChange = {
|
|
1213
|
+
id: `change-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
1214
|
+
...change,
|
|
1215
|
+
};
|
|
1216
|
+
config.pendingManualChanges.push(newChange);
|
|
1217
|
+
// 保持上限 20 条(移除最旧的)
|
|
1218
|
+
if (config.pendingManualChanges.length > 20) {
|
|
1219
|
+
config.pendingManualChanges = config.pendingManualChanges.slice(-20);
|
|
1220
|
+
}
|
|
1221
|
+
config.updatedAt = now();
|
|
1222
|
+
await this.json.writeWorkspaceConfig(projectRoot, dirName, config);
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* 获取手动变更清单
|
|
1226
|
+
* @param workspaceId 工作区 ID
|
|
1227
|
+
* @returns 变更清单(按时间顺序)
|
|
1228
|
+
*/
|
|
1229
|
+
async getManualChanges(workspaceId) {
|
|
1230
|
+
const { projectRoot, dirName } = await this.resolveWorkspaceLocation(workspaceId);
|
|
1231
|
+
const config = await this.json.readWorkspaceConfig(projectRoot, dirName);
|
|
1232
|
+
return config.pendingManualChanges || [];
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* 清除手动变更清单
|
|
1236
|
+
* @param workspaceId 工作区 ID
|
|
1237
|
+
*/
|
|
1238
|
+
async clearManualChanges(workspaceId) {
|
|
1239
|
+
const { projectRoot, dirName } = await this.resolveWorkspaceLocation(workspaceId);
|
|
1240
|
+
const config = await this.json.readWorkspaceConfig(projectRoot, dirName);
|
|
1241
|
+
config.pendingManualChanges = [];
|
|
1242
|
+
config.updatedAt = now();
|
|
1243
|
+
await this.json.writeWorkspaceConfig(projectRoot, dirName, config);
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* 获取场景化引导内容(用于 workspace_init)
|
|
1247
|
+
* @param scenario 任务场景类型
|
|
1248
|
+
* @returns 场景引导文本
|
|
1249
|
+
*/
|
|
1250
|
+
getScenarioGuidance(scenario) {
|
|
1251
|
+
const guidanceScenario = taskScenarioToGuidance(scenario);
|
|
1252
|
+
const config = getGuidanceConfig(guidanceScenario);
|
|
1253
|
+
if (!config) {
|
|
1254
|
+
return "";
|
|
1255
|
+
}
|
|
1256
|
+
// 返回 L1 级别的引导内容(关键步骤列表)
|
|
1257
|
+
return `**📋 ${this.getScenarioDisplayName(scenario)}场景引导**\n${config.l1}`;
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* 获取场景显示名称
|
|
1261
|
+
*/
|
|
1262
|
+
getScenarioDisplayName(scenario) {
|
|
1263
|
+
switch (scenario) {
|
|
1264
|
+
case "feature":
|
|
1265
|
+
return "功能开发";
|
|
1266
|
+
case "summary":
|
|
1267
|
+
return "文档总结";
|
|
1268
|
+
case "optimize":
|
|
1269
|
+
return "性能优化";
|
|
1270
|
+
case "debug":
|
|
1271
|
+
return "问题调试";
|
|
1272
|
+
case "misc":
|
|
1273
|
+
return "杂项任务";
|
|
1274
|
+
default:
|
|
1275
|
+
return "通用";
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// ========== 警告机制 ==========
|
|
1279
|
+
/** 警告频率限制:24 小时 */
|
|
1280
|
+
static WARNING_DEBOUNCE_MS = 24 * 60 * 60 * 1000;
|
|
1281
|
+
/**
|
|
1282
|
+
* 设置工作区警告标记
|
|
1283
|
+
* 用于标记检测到问题的工作区
|
|
1284
|
+
* @param workspaceId 工作区 ID
|
|
1285
|
+
* @param issues 检测到的问题列表(用于日志记录)
|
|
1286
|
+
*/
|
|
1287
|
+
async setWarningFlag(workspaceId, issues) {
|
|
1288
|
+
const index = await this.json.readIndex();
|
|
1289
|
+
const entry = index.workspaces.find(w => w.id === workspaceId);
|
|
1290
|
+
if (!entry)
|
|
1291
|
+
return;
|
|
1292
|
+
entry.hasUnresolvedIssues = true;
|
|
1293
|
+
entry.lastWarningAt = new Date().toISOString();
|
|
1294
|
+
entry.updatedAt = now();
|
|
1295
|
+
await this.json.writeIndex(index);
|
|
1296
|
+
// 记录日志
|
|
1297
|
+
devLog.debug(`工作区 ${workspaceId} 设置警告标记,发现 ${issues.length} 个问题`);
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* 清除工作区警告标记
|
|
1301
|
+
* 在健康检测通过时调用
|
|
1302
|
+
* @param workspaceId 工作区 ID
|
|
1303
|
+
*/
|
|
1304
|
+
async clearWarningFlag(workspaceId) {
|
|
1305
|
+
const index = await this.json.readIndex();
|
|
1306
|
+
const entry = index.workspaces.find(w => w.id === workspaceId);
|
|
1307
|
+
if (!entry)
|
|
1308
|
+
return;
|
|
1309
|
+
// 只有存在警告标记时才更新
|
|
1310
|
+
if (entry.hasUnresolvedIssues || entry.lastWarningAt) {
|
|
1311
|
+
delete entry.hasUnresolvedIssues;
|
|
1312
|
+
delete entry.lastWarningAt;
|
|
1313
|
+
entry.updatedAt = now();
|
|
1314
|
+
await this.json.writeIndex(index);
|
|
1315
|
+
devLog.debug(`工作区 ${workspaceId} 清除警告标记`);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* 检查是否应该显示警告
|
|
1320
|
+
* 同一工作区 24 小时内只警告一次
|
|
1321
|
+
* @param workspaceId 工作区 ID
|
|
1322
|
+
* @returns 是否应该警告
|
|
1323
|
+
*/
|
|
1324
|
+
async shouldShowWarning(workspaceId) {
|
|
1325
|
+
const index = await this.json.readIndex();
|
|
1326
|
+
const entry = index.workspaces.find(w => w.id === workspaceId);
|
|
1327
|
+
if (!entry)
|
|
1328
|
+
return false;
|
|
1329
|
+
// 如果没有上次警告时间,应该警告
|
|
1330
|
+
if (!entry.lastWarningAt) {
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
// 检查是否超过 24 小时
|
|
1334
|
+
const lastWarningTime = new Date(entry.lastWarningAt).getTime();
|
|
1335
|
+
return Date.now() - lastWarningTime > WorkspaceService.WARNING_DEBOUNCE_MS;
|
|
1336
|
+
}
|
|
1337
|
+
// ========== 拓扑构建 ==========
|
|
1338
|
+
/** 状态缩写映射表 */
|
|
1339
|
+
static STATUS_ABBREV = {
|
|
1340
|
+
completed: "com",
|
|
1341
|
+
implementing: "imp",
|
|
1342
|
+
validating: "val",
|
|
1343
|
+
planning: "pla",
|
|
1344
|
+
pending: "pen",
|
|
1345
|
+
monitoring: "mon",
|
|
1346
|
+
cancelled: "can",
|
|
1347
|
+
failed: "fai",
|
|
1348
|
+
};
|
|
1349
|
+
/** 递归深度限制 */
|
|
1350
|
+
static MAX_TOPOLOGY_DEPTH = 10;
|
|
1351
|
+
/** 结论摘要最大长度 */
|
|
1352
|
+
static MAX_CONCLUSION_LENGTH = 100;
|
|
1353
|
+
/**
|
|
1354
|
+
* 构建轻量级拓扑结构
|
|
1355
|
+
* 基于状态的智能折叠策略,压缩完整节点树
|
|
1356
|
+
*
|
|
1357
|
+
* 压缩策略(优先级从高到低):
|
|
1358
|
+
* 1. 焦点路径 - focusPath 中的节点 → children 递归展开
|
|
1359
|
+
* 2. 活跃状态 - implementing/validating/monitoring → children 递归展开
|
|
1360
|
+
* 3. 完成子树 - completed 节点:
|
|
1361
|
+
* - 一级子节点数 ≤5 → _done: [子节点标题列表]
|
|
1362
|
+
* - 一级子节点数 >5 且有 conclusion → _sum: conclusion
|
|
1363
|
+
* - 一级子节点数 >5 且无 conclusion → _c: 子节点数量
|
|
1364
|
+
* 4. 其他 - pending/cancelled/failed 等 → children 递归展开
|
|
1365
|
+
*
|
|
1366
|
+
* @param nodes 节点记录(从 graph.nodes)
|
|
1367
|
+
* @param rootId 根节点 ID
|
|
1368
|
+
* @param focusPath 焦点路径 ID 集合
|
|
1369
|
+
* @param titles 节点标题映射(nodeId → title)
|
|
1370
|
+
* @returns 压缩后的拓扑节点
|
|
1371
|
+
*/
|
|
1372
|
+
buildTopology(nodes, rootId, focusPath, titles) {
|
|
1373
|
+
const visited = new Set();
|
|
1374
|
+
return this.buildTopologyNode(nodes, rootId, focusPath, titles, visited, 0);
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* 递归构建拓扑节点
|
|
1378
|
+
*/
|
|
1379
|
+
buildTopologyNode(nodes, nodeId, focusPath, titles, visited, depth) {
|
|
1380
|
+
// 循环引用检测
|
|
1381
|
+
if (visited.has(nodeId)) {
|
|
1382
|
+
return {
|
|
1383
|
+
id: nodeId,
|
|
1384
|
+
title: titles[nodeId] || nodeId,
|
|
1385
|
+
status: "err",
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
visited.add(nodeId);
|
|
1389
|
+
// 深度限制
|
|
1390
|
+
if (depth >= WorkspaceService.MAX_TOPOLOGY_DEPTH) {
|
|
1391
|
+
const node = nodes[nodeId];
|
|
1392
|
+
return {
|
|
1393
|
+
id: nodeId,
|
|
1394
|
+
title: titles[nodeId] || nodeId,
|
|
1395
|
+
status: WorkspaceService.STATUS_ABBREV[node?.status] || "???",
|
|
1396
|
+
_c: node?.children?.length || 0,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
const node = nodes[nodeId];
|
|
1400
|
+
if (!node) {
|
|
1401
|
+
return {
|
|
1402
|
+
id: nodeId,
|
|
1403
|
+
title: titles[nodeId] || nodeId,
|
|
1404
|
+
status: "???",
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
const statusAbbrev = WorkspaceService.STATUS_ABBREV[node.status] || "???";
|
|
1408
|
+
const title = titles[nodeId] || nodeId;
|
|
1409
|
+
// 构建基础拓扑节点
|
|
1410
|
+
const result = {
|
|
1411
|
+
id: nodeId,
|
|
1412
|
+
title,
|
|
1413
|
+
status: statusAbbrev,
|
|
1414
|
+
};
|
|
1415
|
+
// 添加角色(如果存在)
|
|
1416
|
+
if (node.role) {
|
|
1417
|
+
result.role = node.role;
|
|
1418
|
+
}
|
|
1419
|
+
// 无子节点时直接返回
|
|
1420
|
+
if (!node.children || node.children.length === 0) {
|
|
1421
|
+
return result;
|
|
1422
|
+
}
|
|
1423
|
+
// 决定子节点展示策略
|
|
1424
|
+
const childIds = node.children;
|
|
1425
|
+
// 策略1: 焦点路径 → children 递归展开
|
|
1426
|
+
if (focusPath.has(nodeId)) {
|
|
1427
|
+
result.children = childIds.map(childId => this.buildTopologyNode(nodes, childId, focusPath, titles, visited, depth + 1));
|
|
1428
|
+
return result;
|
|
1429
|
+
}
|
|
1430
|
+
// 策略2: 活跃状态(implementing/validating/monitoring) → children 递归展开
|
|
1431
|
+
if (node.status === "implementing" || node.status === "validating" || node.status === "monitoring") {
|
|
1432
|
+
result.children = childIds.map(childId => this.buildTopologyNode(nodes, childId, focusPath, titles, visited, depth + 1));
|
|
1433
|
+
return result;
|
|
1434
|
+
}
|
|
1435
|
+
// 策略3: 完成子树(completed) → 折叠
|
|
1436
|
+
if (node.status === "completed") {
|
|
1437
|
+
if (childIds.length <= 5) {
|
|
1438
|
+
// 一级子节点 ≤5 → _done: [子节点标题列表]
|
|
1439
|
+
result._done = childIds.map(childId => titles[childId] || childId);
|
|
1440
|
+
}
|
|
1441
|
+
else if (node.conclusion) {
|
|
1442
|
+
// 一级子节点 >5 且有结论 → _sum: conclusion
|
|
1443
|
+
result._sum = node.conclusion.length > WorkspaceService.MAX_CONCLUSION_LENGTH
|
|
1444
|
+
? node.conclusion.substring(0, WorkspaceService.MAX_CONCLUSION_LENGTH) + "..."
|
|
1445
|
+
: node.conclusion;
|
|
1446
|
+
}
|
|
1447
|
+
else {
|
|
1448
|
+
// 一级子节点 >5 且无结论 → _c: 子节点数量
|
|
1449
|
+
result._c = childIds.length;
|
|
1450
|
+
}
|
|
1451
|
+
return result;
|
|
1452
|
+
}
|
|
1453
|
+
// 策略4: 其他状态(pending/cancelled/failed/planning) → children 递归展开
|
|
1454
|
+
result.children = childIds.map(childId => this.buildTopologyNode(nodes, childId, focusPath, titles, visited, depth + 1));
|
|
1455
|
+
return result;
|
|
1456
|
+
}
|
|
1457
|
+
// ========== 工作区导出 ==========
|
|
1458
|
+
/**
|
|
1459
|
+
* 导出工作区为 .twsp 格式
|
|
1460
|
+
* @param workspaceId 工作区 ID
|
|
1461
|
+
* @returns 包含 buffer 和 filename 的对象
|
|
1462
|
+
*/
|
|
1463
|
+
async exportAsTwsp(workspaceId) {
|
|
1464
|
+
// 1. 获取工作区位置信息
|
|
1465
|
+
const { projectRoot, dirName, isArchived } = await this.resolveWorkspaceInfo(workspaceId);
|
|
1466
|
+
// 2. 验证工作区目录存在
|
|
1467
|
+
const workspaceDir = this.fs.getWorkspaceBasePath(projectRoot, dirName, isArchived);
|
|
1468
|
+
if (!(await this.fs.exists(workspaceDir))) {
|
|
1469
|
+
throw new XtepError("WORKSPACE_NOT_FOUND", `工作区目录不存在: ${workspaceDir}`);
|
|
1470
|
+
}
|
|
1471
|
+
// 3. 读取工作区配置和图
|
|
1472
|
+
const config = await this.json.readWorkspaceConfig(projectRoot, dirName, isArchived);
|
|
1473
|
+
const graph = await this.json.readGraph(projectRoot, dirName, isArchived);
|
|
1474
|
+
// 4. 检查外部引用,生成警告
|
|
1475
|
+
const warnings = await this.checkExternalReferences(projectRoot, dirName, graph, isArchived);
|
|
1476
|
+
// 5. 清洗数据
|
|
1477
|
+
const cleanedConfig = this.cleanWorkspaceConfig(config);
|
|
1478
|
+
const cleanedGraph = this.cleanNodeGraph(graph);
|
|
1479
|
+
// 6. 获取当前版本号
|
|
1480
|
+
const xtepVersion = this.getCurrentVersion();
|
|
1481
|
+
// 6.1 从根节点读取 goal(requirement 字段)- goal 已统一到根节点
|
|
1482
|
+
const rootNodeId = config.rootNodeId || "root";
|
|
1483
|
+
const rootNodeMeta = graph.nodes[rootNodeId];
|
|
1484
|
+
const rootNodeDirName = rootNodeMeta?.dirName || rootNodeId;
|
|
1485
|
+
const rootNodeInfo = await this.md.readNodeInfo(projectRoot, dirName, rootNodeDirName, isArchived);
|
|
1486
|
+
const goal = rootNodeInfo.requirement || "";
|
|
1487
|
+
// 7. 生成 manifest
|
|
1488
|
+
const manifest = {
|
|
1489
|
+
version: "1.0",
|
|
1490
|
+
exportedAt: new Date().toISOString(),
|
|
1491
|
+
xtepVersion,
|
|
1492
|
+
workspace: {
|
|
1493
|
+
originalId: workspaceId,
|
|
1494
|
+
name: config.name,
|
|
1495
|
+
goal, // 从根节点 requirement 读取
|
|
1496
|
+
scenario: config.scenario,
|
|
1497
|
+
},
|
|
1498
|
+
stats: {
|
|
1499
|
+
nodeCount: Object.keys(graph.nodes).length,
|
|
1500
|
+
memoCount: Object.keys(graph.memos || {}).length,
|
|
1501
|
+
},
|
|
1502
|
+
warnings,
|
|
1503
|
+
};
|
|
1504
|
+
// 8. 创建 zip 并打包
|
|
1505
|
+
const buffer = await this.createTwspArchive(workspaceDir, dirName, manifest, cleanedConfig, cleanedGraph);
|
|
1506
|
+
// 9. 生成文件名
|
|
1507
|
+
const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
1508
|
+
const safeName = config.name.replace(/[/\\:*?"<>|]/g, "_");
|
|
1509
|
+
const filename = `${safeName}_${date}.twsp`;
|
|
1510
|
+
return { buffer, filename, warnings };
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* 创建 .twsp 归档文件
|
|
1514
|
+
*/
|
|
1515
|
+
async createTwspArchive(workspaceDir, dirName, manifest, cleanedConfig, cleanedGraph) {
|
|
1516
|
+
return new Promise((resolve, reject) => {
|
|
1517
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
1518
|
+
const chunks = [];
|
|
1519
|
+
archive.on("data", (chunk) => chunks.push(chunk));
|
|
1520
|
+
archive.on("end", () => resolve(Buffer.concat(chunks)));
|
|
1521
|
+
archive.on("error", (err) => reject(err));
|
|
1522
|
+
// 添加 manifest.json
|
|
1523
|
+
archive.append(JSON.stringify(manifest, null, 2), { name: "manifest.json" });
|
|
1524
|
+
// 添加清洗后的 workspace.json
|
|
1525
|
+
archive.append(JSON.stringify(cleanedConfig, null, 2), { name: `${dirName}/workspace.json` });
|
|
1526
|
+
// 添加清洗后的 graph.json
|
|
1527
|
+
archive.append(JSON.stringify(cleanedGraph, null, 2), { name: `${dirName}/graph.json` });
|
|
1528
|
+
// 添加 Workspace.md(原样复制)
|
|
1529
|
+
const workspaceMdPath = path.join(workspaceDir, "Workspace.md");
|
|
1530
|
+
if (existsSync(workspaceMdPath)) {
|
|
1531
|
+
archive.file(workspaceMdPath, { name: `${dirName}/Workspace.md` });
|
|
1532
|
+
}
|
|
1533
|
+
// 添加 Log.md(原样复制)
|
|
1534
|
+
const logMdPath = path.join(workspaceDir, "Log.md");
|
|
1535
|
+
if (existsSync(logMdPath)) {
|
|
1536
|
+
archive.file(logMdPath, { name: `${dirName}/Log.md` });
|
|
1537
|
+
}
|
|
1538
|
+
// 添加 Problem.md(原样复制)
|
|
1539
|
+
const problemMdPath = path.join(workspaceDir, "Problem.md");
|
|
1540
|
+
if (existsSync(problemMdPath)) {
|
|
1541
|
+
archive.file(problemMdPath, { name: `${dirName}/Problem.md` });
|
|
1542
|
+
}
|
|
1543
|
+
// 添加 nodes 目录(原样复制)
|
|
1544
|
+
const nodesDir = path.join(workspaceDir, "nodes");
|
|
1545
|
+
if (existsSync(nodesDir)) {
|
|
1546
|
+
archive.directory(nodesDir, `${dirName}/nodes`);
|
|
1547
|
+
}
|
|
1548
|
+
// 添加 memos 目录(原样复制)
|
|
1549
|
+
const memosDir = path.join(workspaceDir, "memos");
|
|
1550
|
+
if (existsSync(memosDir)) {
|
|
1551
|
+
archive.directory(memosDir, `${dirName}/memos`);
|
|
1552
|
+
}
|
|
1553
|
+
archive.finalize();
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* 清洗工作区配置(删除本地信息)
|
|
1558
|
+
*/
|
|
1559
|
+
cleanWorkspaceConfig(config) {
|
|
1560
|
+
const cleaned = { ...config };
|
|
1561
|
+
// 删除派发本地信息
|
|
1562
|
+
if (cleaned.dispatch) {
|
|
1563
|
+
// 重建 dispatch 对象,只保留需要导出的字段
|
|
1564
|
+
// enabledAt 设为 0 表示导出状态(导入时会重置)
|
|
1565
|
+
const cleanedDispatch = {
|
|
1566
|
+
enabled: cleaned.dispatch.enabled,
|
|
1567
|
+
useGit: cleaned.dispatch.useGit,
|
|
1568
|
+
enabledAt: 0, // 导出时重置为 0,导入后需要重新启用
|
|
1569
|
+
limits: cleaned.dispatch.limits,
|
|
1570
|
+
review: cleaned.dispatch.review,
|
|
1571
|
+
};
|
|
1572
|
+
// 不复制 Git 相关的本地信息(originalBranch, processBranch, backupBranches)
|
|
1573
|
+
cleaned.dispatch = cleanedDispatch;
|
|
1574
|
+
}
|
|
1575
|
+
// 删除临时状态
|
|
1576
|
+
delete cleaned.pendingManualChanges;
|
|
1577
|
+
return cleaned;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* 清洗节点图(删除本地信息)
|
|
1581
|
+
*/
|
|
1582
|
+
cleanNodeGraph(graph) {
|
|
1583
|
+
const cleaned = {
|
|
1584
|
+
version: graph.version,
|
|
1585
|
+
currentFocus: null, // 清除聚焦状态
|
|
1586
|
+
nodes: {},
|
|
1587
|
+
memos: graph.memos,
|
|
1588
|
+
};
|
|
1589
|
+
// 不导出 lastWriteCodeVersion
|
|
1590
|
+
// 清洗节点派发信息
|
|
1591
|
+
for (const nodeId in graph.nodes) {
|
|
1592
|
+
const node = graph.nodes[nodeId];
|
|
1593
|
+
const cleanedNode = { ...node };
|
|
1594
|
+
if (cleanedNode.dispatch) {
|
|
1595
|
+
cleanedNode.dispatch = {
|
|
1596
|
+
status: cleanedNode.dispatch.status,
|
|
1597
|
+
// 清除执行标记
|
|
1598
|
+
};
|
|
1599
|
+
// 删除 startMarker, endMarker, attempts
|
|
1600
|
+
delete cleanedNode.dispatch.startMarker;
|
|
1601
|
+
delete cleanedNode.dispatch.endMarker;
|
|
1602
|
+
delete cleanedNode.dispatch.attempts;
|
|
1603
|
+
}
|
|
1604
|
+
cleaned.nodes[nodeId] = cleanedNode;
|
|
1605
|
+
}
|
|
1606
|
+
return cleaned;
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* 检查工作区导出的警告信息(公开方法,用于预检查)
|
|
1610
|
+
*/
|
|
1611
|
+
async checkExportWarnings(workspaceId) {
|
|
1612
|
+
const { projectRoot, dirName, isArchived } = await this.resolveWorkspaceInfo(workspaceId);
|
|
1613
|
+
const graph = await this.json.readGraph(projectRoot, dirName, isArchived);
|
|
1614
|
+
return this.checkExternalReferences(projectRoot, dirName, graph, isArchived);
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* 检查外部引用并生成警告
|
|
1618
|
+
* 读取每个节点的 Info.md,检查 docs 字段中的外部文件引用
|
|
1619
|
+
*/
|
|
1620
|
+
async checkExternalReferences(projectRoot, wsDirName, graph, isArchived) {
|
|
1621
|
+
const warnings = [];
|
|
1622
|
+
for (const nodeId in graph.nodes) {
|
|
1623
|
+
const node = graph.nodes[nodeId];
|
|
1624
|
+
try {
|
|
1625
|
+
// 读取节点 Info.md 获取 docs 字段
|
|
1626
|
+
const nodeInfo = await this.md.readNodeInfo(projectRoot, wsDirName, node.dirName, isArchived);
|
|
1627
|
+
// 检查 docs 中的外部引用
|
|
1628
|
+
if (nodeInfo.docs && nodeInfo.docs.length > 0) {
|
|
1629
|
+
for (const doc of nodeInfo.docs) {
|
|
1630
|
+
// 检查是否为外部文件引用(绝对路径或 file:// 协议)
|
|
1631
|
+
// memo:// 是内部引用,不算外部
|
|
1632
|
+
if (doc.path.startsWith("/") || doc.path.startsWith("file://")) {
|
|
1633
|
+
warnings.push(`节点 "${nodeInfo.title}" 包含外部文件引用: ${doc.path}`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
catch {
|
|
1639
|
+
// 读取失败时跳过(可能是节点目录不存在)
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return warnings;
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* 获取当前版本号
|
|
1646
|
+
*/
|
|
1647
|
+
getCurrentVersion() {
|
|
1648
|
+
try {
|
|
1649
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1650
|
+
const __dirname = path.dirname(__filename);
|
|
1651
|
+
const requireFn = createRequire(import.meta.url);
|
|
1652
|
+
const pkg = requireFn(path.join(__dirname, "..", "..", "package.json"));
|
|
1653
|
+
return pkg.version;
|
|
1654
|
+
}
|
|
1655
|
+
catch {
|
|
1656
|
+
return "unknown";
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
// ========== 工作区导入 ==========
|
|
1660
|
+
/**
|
|
1661
|
+
* 从 .twsp 文件导入工作区
|
|
1662
|
+
* @param twspPath .twsp 文件路径
|
|
1663
|
+
* @param targetDir 目标目录(默认 ~/.xtep-workspace/import/)
|
|
1664
|
+
* @returns 导入结果
|
|
1665
|
+
*/
|
|
1666
|
+
async importFromTwsp(twspPath, targetDir) {
|
|
1667
|
+
// 1. 确定目标目录(默认 ~/{localDirName}/import/)
|
|
1668
|
+
const localDirName = this.fs.getDirName();
|
|
1669
|
+
// 展开 ~ 为用户主目录
|
|
1670
|
+
const expandedTargetDir = targetDir?.startsWith("~")
|
|
1671
|
+
? targetDir.replace("~", os.homedir())
|
|
1672
|
+
: targetDir;
|
|
1673
|
+
const finalTargetDir = expandedTargetDir || path.join(os.homedir(), localDirName, "import");
|
|
1674
|
+
// 2. 创建临时解压目录
|
|
1675
|
+
const extractDir = path.join(os.tmpdir(), `twsp-extract-${Date.now()}`);
|
|
1676
|
+
await fs.mkdir(extractDir, { recursive: true });
|
|
1677
|
+
try {
|
|
1678
|
+
// 3. 解压 .twsp 文件(使用 adm-zip 以正确处理中文路径)
|
|
1679
|
+
const zip = new AdmZip(twspPath);
|
|
1680
|
+
zip.extractAllTo(extractDir, true);
|
|
1681
|
+
// 4. 读取并验证 manifest.json
|
|
1682
|
+
const manifestPath = path.join(extractDir, "manifest.json");
|
|
1683
|
+
if (!existsSync(manifestPath)) {
|
|
1684
|
+
throw new XtepError("INVALID_PATH", "无效的 .twsp 文件:缺少 manifest.json");
|
|
1685
|
+
}
|
|
1686
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, "utf-8"));
|
|
1687
|
+
// 5. 查找工作区目录(manifest.json 同级的第一个目录)
|
|
1688
|
+
const entries = await fs.readdir(extractDir, { withFileTypes: true });
|
|
1689
|
+
const workspaceDirEntry = entries.find(e => e.isDirectory());
|
|
1690
|
+
if (!workspaceDirEntry) {
|
|
1691
|
+
throw new XtepError("INVALID_PATH", "无效的 .twsp 文件:缺少工作区目录");
|
|
1692
|
+
}
|
|
1693
|
+
const extractedWorkspaceDir = path.join(extractDir, workspaceDirEntry.name);
|
|
1694
|
+
// 6. 验证 workspace.json 存在
|
|
1695
|
+
const wsConfigPath = path.join(extractedWorkspaceDir, "workspace.json");
|
|
1696
|
+
if (!existsSync(wsConfigPath)) {
|
|
1697
|
+
throw new XtepError("INVALID_PATH", "无效的 .twsp 文件:缺少 workspace.json");
|
|
1698
|
+
}
|
|
1699
|
+
// 7. 生成新的工作区 ID
|
|
1700
|
+
const newWorkspaceId = generateWorkspaceId();
|
|
1701
|
+
// 8. 确定目标目录名(处理重名)
|
|
1702
|
+
const baseName = manifest.workspace.name;
|
|
1703
|
+
const shortId = newWorkspaceId.replace("ws-", "");
|
|
1704
|
+
let finalDirName = `${baseName}_${shortId}`;
|
|
1705
|
+
// 确保目标目录存在
|
|
1706
|
+
await fs.mkdir(finalTargetDir, { recursive: true });
|
|
1707
|
+
const targetWorkspaceDir = path.join(finalTargetDir, localDirName, finalDirName);
|
|
1708
|
+
// 检查目标目录是否存在(重名处理)
|
|
1709
|
+
if (existsSync(targetWorkspaceDir)) {
|
|
1710
|
+
// 重名,添加时间戳后缀
|
|
1711
|
+
const timestamp = Date.now().toString(36);
|
|
1712
|
+
finalDirName = `${baseName}_${timestamp}_${shortId}`;
|
|
1713
|
+
}
|
|
1714
|
+
const finalWorkspacePath = path.join(finalTargetDir, localDirName, finalDirName);
|
|
1715
|
+
// 9. 确保父目录存在并复制文件
|
|
1716
|
+
await fs.mkdir(path.dirname(finalWorkspacePath), { recursive: true });
|
|
1717
|
+
await fs.cp(extractedWorkspaceDir, finalWorkspacePath, { recursive: true });
|
|
1718
|
+
// 10. 更新 workspace.json 中的 ID 和 dirName
|
|
1719
|
+
const wsConfig = JSON.parse(await fs.readFile(path.join(finalWorkspacePath, "workspace.json"), "utf-8"));
|
|
1720
|
+
wsConfig.id = newWorkspaceId;
|
|
1721
|
+
wsConfig.dirName = finalDirName;
|
|
1722
|
+
wsConfig.updatedAt = now();
|
|
1723
|
+
await fs.writeFile(path.join(finalWorkspacePath, "workspace.json"), JSON.stringify(wsConfig, null, 2));
|
|
1724
|
+
// 11. 注册到索引(使用 smartImport 逻辑)
|
|
1725
|
+
const index = await this.json.readIndex();
|
|
1726
|
+
// 构建工作区条目
|
|
1727
|
+
const currentTime = now();
|
|
1728
|
+
index.workspaces.push({
|
|
1729
|
+
id: newWorkspaceId,
|
|
1730
|
+
name: manifest.workspace.name,
|
|
1731
|
+
dirName: finalDirName,
|
|
1732
|
+
projectRoot: finalTargetDir,
|
|
1733
|
+
status: "active",
|
|
1734
|
+
createdAt: currentTime,
|
|
1735
|
+
updatedAt: currentTime,
|
|
1736
|
+
});
|
|
1737
|
+
await this.json.writeIndex(index);
|
|
1738
|
+
// 12. 追加日志
|
|
1739
|
+
await this.md.appendLog(finalTargetDir, finalDirName, {
|
|
1740
|
+
time: currentTime,
|
|
1741
|
+
operator: "system",
|
|
1742
|
+
event: `工作区从 .twsp 文件导入(原 ID: ${manifest.workspace.originalId})`,
|
|
1743
|
+
});
|
|
1744
|
+
// 13. 发送事件通知
|
|
1745
|
+
eventService.emitWorkspaceUpdate(newWorkspaceId);
|
|
1746
|
+
return {
|
|
1747
|
+
workspaceId: newWorkspaceId,
|
|
1748
|
+
name: manifest.workspace.name,
|
|
1749
|
+
path: finalWorkspacePath,
|
|
1750
|
+
warnings: manifest.warnings || [],
|
|
1751
|
+
};
|
|
1752
|
+
}
|
|
1753
|
+
finally {
|
|
1754
|
+
// 14. 清理临时解压目录
|
|
1755
|
+
try {
|
|
1756
|
+
await fs.rm(extractDir, { recursive: true, force: true });
|
|
1757
|
+
}
|
|
1758
|
+
catch {
|
|
1759
|
+
// 清理失败不影响主流程
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
//# sourceMappingURL=WorkspaceService.js.map
|