aws-runtime-bridge 1.9.79 → 1.9.81
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/adapter/AcodeSdkAdapter.d.ts +0 -1
- package/dist/adapter/AcodeSdkAdapter.js +11 -1986
- package/dist/adapter/AdapterRegistry.d.ts +0 -1
- package/dist/adapter/AdapterRegistry.js +2 -100
- package/dist/adapter/ClaudeSdkAdapter.d.ts +0 -1
- package/dist/adapter/ClaudeSdkAdapter.js +4 -1300
- package/dist/adapter/CodexSdkAdapter.d.ts +0 -1
- package/dist/adapter/CodexSdkAdapter.js +4 -836
- package/dist/adapter/OpencodeSdkAdapter.d.ts +0 -1
- package/dist/adapter/OpencodeSdkAdapter.js +4 -879
- package/dist/adapter/SdkProviderSpi.d.ts +0 -1
- package/dist/adapter/SdkProviderSpi.js +2 -55
- package/dist/adapter/acode-tool-names.d.ts +0 -1
- package/dist/adapter/acode-tool-names.js +2 -37
- package/dist/adapter/idle-poll-loop.d.ts +0 -1
- package/dist/adapter/idle-poll-loop.js +2 -59
- package/dist/adapter/index.d.ts +0 -1
- package/dist/adapter/index.js +2 -11
- package/dist/adapter/instance-ai-config-resolver.d.ts +0 -1
- package/dist/adapter/instance-ai-config-resolver.js +2 -46
- package/dist/adapter/types.d.ts +0 -1
- package/dist/adapter/types.js +2 -443
- package/dist/adapters/cc-switch/common.d.ts +0 -1
- package/dist/adapters/cc-switch/common.js +2 -61
- package/dist/adapters/cc-switch/index.d.ts +0 -1
- package/dist/adapters/cc-switch/index.js +2 -57
- package/dist/adapters/cc-switch/mcp-acode.d.ts +0 -1
- package/dist/adapters/cc-switch/mcp-acode.js +2 -71
- package/dist/adapters/cc-switch/mcp-claude.d.ts +0 -1
- package/dist/adapters/cc-switch/mcp-claude.js +2 -89
- package/dist/adapters/cc-switch/mcp-claudecode.d.ts +0 -1
- package/dist/adapters/cc-switch/mcp-claudecode.js +2 -77
- package/dist/adapters/cc-switch/mcp-codex.d.ts +0 -1
- package/dist/adapters/cc-switch/mcp-codex.js +11 -249
- package/dist/adapters/cc-switch/mcp-opencode.d.ts +0 -1
- package/dist/adapters/cc-switch/mcp-opencode.js +2 -97
- package/dist/adapters/cc-switch/mcp-placeholder.d.ts +0 -1
- package/dist/adapters/cc-switch/mcp-placeholder.js +2 -19
- package/dist/adapters/cc-switch/skill-acode.d.ts +0 -1
- package/dist/adapters/cc-switch/skill-acode.js +2 -12
- package/dist/adapters/cc-switch/skill-claude.d.ts +0 -1
- package/dist/adapters/cc-switch/skill-claude.js +2 -11
- package/dist/adapters/cc-switch/skill-claudecode.d.ts +0 -1
- package/dist/adapters/cc-switch/skill-claudecode.js +2 -11
- package/dist/adapters/cc-switch/skill-codex.d.ts +0 -1
- package/dist/adapters/cc-switch/skill-codex.js +2 -6
- package/dist/adapters/cc-switch/skill-directory.d.ts +0 -1
- package/dist/adapters/cc-switch/skill-directory.js +3 -137
- package/dist/adapters/cc-switch/skill-opencode.d.ts +0 -1
- package/dist/adapters/cc-switch/skill-opencode.js +2 -11
- package/dist/adapters/cc-switch/skill-placeholder.d.ts +0 -1
- package/dist/adapters/cc-switch/skill-placeholder.js +2 -19
- package/dist/config.d.ts +0 -1
- package/dist/config.js +2 -290
- package/dist/index.d.ts +0 -1
- package/dist/index.js +2 -573
- package/dist/middleware/auth.d.ts +0 -1
- package/dist/middleware/auth.js +2 -98
- package/dist/routes/acode-todos.d.ts +0 -1
- package/dist/routes/acode-todos.js +2 -75
- package/dist/routes/ai-sources.d.ts +0 -1
- package/dist/routes/ai-sources.js +2 -132
- package/dist/routes/dashboard.d.ts +0 -1
- package/dist/routes/dashboard.js +182 -649
- package/dist/routes/events.d.ts +0 -1
- package/dist/routes/events.js +2 -22
- package/dist/routes/file-browser.d.ts +0 -1
- package/dist/routes/file-browser.js +2 -781
- package/dist/routes/file-diff-rollback.d.ts +0 -1
- package/dist/routes/file-diff-rollback.js +2 -301
- package/dist/routes/file-snapshot.d.ts +0 -1
- package/dist/routes/file-snapshot.js +2 -156
- package/dist/routes/git.d.ts +0 -1
- package/dist/routes/git.js +28 -1900
- package/dist/routes/instance.d.ts +0 -1
- package/dist/routes/instance.js +3 -748
- package/dist/routes/mcp.d.ts +0 -1
- package/dist/routes/mcp.js +2 -196
- package/dist/routes/processes.d.ts +0 -1
- package/dist/routes/processes.js +2 -252
- package/dist/routes/properties.d.ts +0 -1
- package/dist/routes/properties.js +2 -72
- package/dist/routes/pty.d.ts +0 -1
- package/dist/routes/pty.js +6 -560
- package/dist/routes/runtime-binding.d.ts +0 -1
- package/dist/routes/runtime-binding.js +2 -263
- package/dist/routes/sessions.d.ts +0 -1
- package/dist/routes/sessions.js +2 -758
- package/dist/routes/skills.d.ts +0 -1
- package/dist/routes/skills.js +2 -187
- package/dist/routes/sub-agents.d.ts +0 -1
- package/dist/routes/sub-agents.js +20 -279
- package/dist/routes/system-metrics.d.ts +0 -1
- package/dist/routes/system-metrics.js +2 -20
- package/dist/routes/task-context.d.ts +0 -1
- package/dist/routes/task-context.js +2 -78
- package/dist/routes/terminal.d.ts +0 -1
- package/dist/routes/terminal.js +54 -2882
- package/dist/routes/yml.d.ts +0 -1
- package/dist/routes/yml.js +2 -223
- package/dist/services/acode-package.d.ts +0 -1
- package/dist/services/acode-package.js +2 -111
- package/dist/services/agent-event-store.d.ts +0 -1
- package/dist/services/agent-event-store.js +2 -81
- package/dist/services/agent-process-manager.d.ts +0 -1
- package/dist/services/agent-process-manager.js +2 -796
- package/dist/services/ai-source-apply.d.ts +0 -1
- package/dist/services/ai-source-apply.js +2 -83
- package/dist/services/auto-register.d.ts +5 -1
- package/dist/services/auto-register.js +2 -1053
- package/dist/services/aws-client-agent-mcp.d.ts +0 -1
- package/dist/services/aws-client-agent-mcp.js +3 -337
- package/dist/services/bridge-package-publish-hooks.d.ts +0 -1
- package/dist/services/bridge-package-publish-hooks.js +2 -115
- package/dist/services/cc-switch-sdk.d.ts +0 -1
- package/dist/services/cc-switch-sdk.js +2 -135
- package/dist/services/cli-commands.d.ts +6 -1
- package/dist/services/cli-commands.js +6 -645
- package/dist/services/event-delta-pusher.d.ts +0 -1
- package/dist/services/event-delta-pusher.js +2 -95
- package/dist/services/fileSnapshotStore.d.ts +0 -1
- package/dist/services/fileSnapshotStore.js +2 -500
- package/dist/services/instance-ai-config-service.d.ts +0 -1
- package/dist/services/instance-ai-config-service.js +2 -174
- package/dist/services/instance-init-service.d.ts +0 -1
- package/dist/services/instance-init-service.js +2 -193
- package/dist/services/instance-service.d.ts +0 -1
- package/dist/services/instance-service.js +2 -237
- package/dist/services/instance-state.d.ts +0 -1
- package/dist/services/instance-state.js +2 -94
- package/dist/services/legacy-path-migration.d.ts +0 -1
- package/dist/services/legacy-path-migration.js +2 -66
- package/dist/services/lifecycle-state.d.ts +0 -1
- package/dist/services/lifecycle-state.js +3 -110
- package/dist/services/mcp-launch-binding-queue.d.ts +0 -1
- package/dist/services/mcp-launch-binding-queue.js +2 -175
- package/dist/services/orphan-monitor.d.ts +0 -1
- package/dist/services/orphan-monitor.js +2 -382
- package/dist/services/panel-auth.d.ts +0 -1
- package/dist/services/panel-auth.js +2 -168
- package/dist/services/process-detector.d.ts +0 -1
- package/dist/services/process-detector.js +18 -1019
- package/dist/services/process-registry.d.ts +0 -1
- package/dist/services/process-registry.js +2 -375
- package/dist/services/ring-buffer.d.ts +0 -1
- package/dist/services/ring-buffer.js +2 -103
- package/dist/services/runtime-binding.d.ts +0 -1
- package/dist/services/runtime-binding.js +6 -363
- package/dist/services/runtime-lifecycle-policy.d.ts +0 -1
- package/dist/services/runtime-lifecycle-policy.js +2 -35
- package/dist/services/session-output.d.ts +0 -1
- package/dist/services/session-output.js +5 -539
- package/dist/services/startup-config-wizard.d.ts +0 -1
- package/dist/services/startup-config-wizard.js +14 -258
- package/dist/services/sub-agent-manager-instance.d.ts +0 -1
- package/dist/services/sub-agent-manager-instance.js +2 -12
- package/dist/services/system-metrics.d.ts +0 -1
- package/dist/services/system-metrics.js +2 -252
- package/dist/services/terminal-persistence.d.ts +0 -1
- package/dist/services/terminal-persistence.js +2 -176
- package/dist/services/tool-installer.d.ts +0 -1
- package/dist/services/tool-installer.js +5 -751
- package/dist/services/tunnel-client.d.ts +0 -1
- package/dist/services/tunnel-client.js +2 -402
- package/dist/services/windows-service-manager.d.ts +3 -3
- package/dist/services/windows-service-manager.js +2 -59
- package/dist/services/workspace-files.d.ts +0 -1
- package/dist/services/workspace-files.js +2 -1275
- package/dist/services/workspace-watch.d.ts +0 -1
- package/dist/services/workspace-watch.js +2 -123
- package/dist/services/workspaceTaskChangeWatcher.d.ts +0 -1
- package/dist/services/workspaceTaskChangeWatcher.js +2 -483
- package/dist/types.d.ts +0 -1
- package/dist/types.js +1 -1
- package/dist/utils/file-utils.d.ts +0 -1
- package/dist/utils/file-utils.js +3 -208
- package/dist/utils/instance-ai-config-debug-logger.d.ts +0 -1
- package/dist/utils/instance-ai-config-debug-logger.js +4 -63
- package/dist/utils/logger.d.ts +0 -1
- package/dist/utils/logger.js +2 -154
- package/dist/utils/mcp-utils.d.ts +0 -1
- package/dist/utils/mcp-utils.js +2 -187
- package/dist/utils/path-utils.d.ts +0 -1
- package/dist/utils/path-utils.js +2 -44
- package/dist/utils/sdk-package-loader.d.ts +0 -1
- package/dist/utils/sdk-package-loader.js +2 -78
- package/dist/utils/validation.d.ts +0 -1
- package/dist/utils/validation.js +2 -53
- package/dist/utils/yaml-utils.d.ts +0 -1
- package/dist/utils/yaml-utils.js +2 -312
- package/node_modules/@cc-switch/sdk/dist/constants.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/constants.js +2 -63
- package/node_modules/@cc-switch/sdk/dist/errors.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/errors.js +2 -8
- package/node_modules/@cc-switch/sdk/dist/index.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/index.js +2 -9
- package/node_modules/@cc-switch/sdk/dist/schemas.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/schemas.js +2 -38
- package/node_modules/@cc-switch/sdk/dist/sdk.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/sdk.js +2 -470
- package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.js +4 -307
- package/node_modules/@cc-switch/sdk/dist/services/mcp-model.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/mcp-model.js +2 -37
- package/node_modules/@cc-switch/sdk/dist/services/mcp-service.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/mcp-service.js +2 -9
- package/node_modules/@cc-switch/sdk/dist/services/skill-model.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/skill-model.js +2 -41
- package/node_modules/@cc-switch/sdk/dist/services/skill-service.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/skill-service.js +2 -9
- package/node_modules/@cc-switch/sdk/dist/state.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/state.js +2 -19
- package/node_modules/@cc-switch/sdk/dist/types.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/types.js +1 -1
- package/node_modules/@cc-switch/sdk/dist/utils/fs.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/utils/fs.js +3 -114
- package/node_modules/@cc-switch/sdk/dist/utils/id.d.ts +0 -1
- package/node_modules/@cc-switch/sdk/dist/utils/id.js +2 -12
- package/node_modules/@cc-switch/sdk/package.json +1 -1
- package/package/acode/dist/ask-user-question-tool.d.ts +0 -1
- package/package/acode/dist/ask-user-question-tool.js +4 -233
- package/package/acode/dist/built-in-file-tools.d.ts +0 -1
- package/package/acode/dist/built-in-file-tools.js +4 -966
- package/package/acode/dist/built-in-symbols-tool.d.ts +0 -1
- package/package/acode/dist/built-in-symbols-tool.js +2 -117
- package/package/acode/dist/builtins/commandArtifacts.d.ts +0 -1
- package/package/acode/dist/builtins/commandArtifacts.js +9 -97
- package/package/acode/dist/builtins/commandOutputEvents.d.ts +0 -1
- package/package/acode/dist/builtins/commandOutputEvents.js +2 -56
- package/package/acode/dist/builtins/shellCommandPreparation.d.ts +0 -1
- package/package/acode/dist/builtins/shellCommandPreparation.js +5 -44
- package/package/acode/dist/builtins/shellCommandResult.d.ts +0 -1
- package/package/acode/dist/builtins/shellCommandResult.js +2 -71
- package/package/acode/dist/builtins/shellCommandRunner.d.ts +0 -1
- package/package/acode/dist/builtins/shellCommandRunner.js +2 -115
- package/package/acode/dist/builtins/shellOutputDecoder.d.ts +0 -1
- package/package/acode/dist/builtins/shellOutputDecoder.js +2 -94
- package/package/acode/dist/cli.d.ts +0 -1
- package/package/acode/dist/cli.js +4 -38
- package/package/acode/dist/commands/commandLoader.d.ts +0 -1
- package/package/acode/dist/commands/commandLoader.js +2 -33
- package/package/acode/dist/commands/commandParser.d.ts +0 -1
- package/package/acode/dist/commands/commandParser.js +3 -77
- package/package/acode/dist/commands/commandRegistry.d.ts +0 -1
- package/package/acode/dist/commands/commandRegistry.js +2 -30
- package/package/acode/dist/compaction.d.ts +0 -1
- package/package/acode/dist/compaction.js +7 -919
- package/package/acode/dist/config.d.ts +0 -1
- package/package/acode/dist/config.js +5 -105
- package/package/acode/dist/fs-utils.d.ts +0 -1
- package/package/acode/dist/fs-utils.js +2 -79
- package/package/acode/dist/hooks/hookBus.d.ts +0 -1
- package/package/acode/dist/hooks/hookBus.js +2 -40
- package/package/acode/dist/index.d.ts +0 -1
- package/package/acode/dist/index.js +2 -19
- package/package/acode/dist/mcpToolNames.d.ts +0 -1
- package/package/acode/dist/mcpToolNames.js +2 -17
- package/package/acode/dist/parsers/ast-cache.d.ts +0 -1
- package/package/acode/dist/parsers/ast-cache.js +2 -76
- package/package/acode/dist/parsers/language-detector.d.ts +0 -1
- package/package/acode/dist/parsers/language-detector.js +2 -44
- package/package/acode/dist/parsers/language-queries.d.ts +0 -1
- package/package/acode/dist/parsers/language-queries.js +12 -602
- package/package/acode/dist/parsers/symbol-extractor.d.ts +0 -1
- package/package/acode/dist/parsers/symbol-extractor.js +3 -164
- package/package/acode/dist/parsers/tree-sitter-loader.d.ts +0 -1
- package/package/acode/dist/parsers/tree-sitter-loader.js +2 -64
- package/package/acode/dist/parsers/types.d.ts +0 -1
- package/package/acode/dist/parsers/types.js +1 -7
- package/package/acode/dist/runtime.d.ts +0 -1
- package/package/acode/dist/runtime.js +86 -3771
- package/package/acode/dist/skill-tool.d.ts +0 -1
- package/package/acode/dist/skill-tool.js +5 -174
- package/package/acode/dist/sub-agent-manager.d.ts +0 -1
- package/package/acode/dist/sub-agent-manager.js +2 -1159
- package/package/acode/dist/sub-agent-store.d.ts +0 -1
- package/package/acode/dist/sub-agent-store.js +4 -236
- package/package/acode/dist/sub-agent-tools.d.ts +0 -1
- package/package/acode/dist/sub-agent-tools.js +15 -369
- package/package/acode/dist/sub-agent-types.d.ts +0 -1
- package/package/acode/dist/sub-agent-types.js +1 -8
- package/package/acode/dist/todo-store.d.ts +0 -1
- package/package/acode/dist/todo-store.js +2 -73
- package/package/acode/dist/todo-tool.d.ts +0 -1
- package/package/acode/dist/todo-tool.js +5 -121
- package/package/acode/dist/types.d.ts +0 -1
- package/package/acode/dist/types.js +1 -1
- package/package/acode/package.json +2 -2
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js +2 -890
- package/package/aws-client-agent-mcp/dist/agent-client.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/agent-client.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/config.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/config.js +2 -243
- package/package/aws-client-agent-mcp/dist/config.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/config.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/constants.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/constants.js +2 -24
- package/package/aws-client-agent-mcp/dist/context-manager.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/context-manager.js +2 -477
- package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/context-manager.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/http-client.js +2 -446
- package/package/aws-client-agent-mcp/dist/http-client.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/http-client.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/index.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/index.js +2 -34
- package/package/aws-client-agent-mcp/dist/logger.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/logger.js +3 -178
- package/package/aws-client-agent-mcp/dist/logger.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/logger.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +38 -2709
- package/package/aws-client-agent-mcp/dist/mcp-server.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js +22 -562
- package/package/aws-client-agent-mcp/dist/mcp-tools.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/memory-store.js +4 -826
- package/package/aws-client-agent-mcp/dist/memory-store.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/memory-store.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.js +31 -294
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.js +2 -288
- package/package/aws-client-agent-mcp/dist/message-buffer.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/messageContent.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/messageContent.js +2 -125
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js +2 -103
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.js +2 -96
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.js +3 -327
- package/package/aws-client-agent-mcp/dist/status-reporter.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.test.js +0 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/types.js +2 -9
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +2 -677
- package/package/aws-client-agent-mcp/dist/websocket-client.test.d.ts +0 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js +0 -1
- package/package/aws-client-agent-mcp/package.json +1 -1
- package/package/cc-switch-sdk/dist/constants.d.ts +0 -1
- package/package/cc-switch-sdk/dist/constants.js +2 -63
- package/package/cc-switch-sdk/dist/errors.d.ts +0 -1
- package/package/cc-switch-sdk/dist/errors.js +2 -8
- package/package/cc-switch-sdk/dist/index.d.ts +0 -1
- package/package/cc-switch-sdk/dist/index.js +2 -9
- package/package/cc-switch-sdk/dist/schemas.d.ts +0 -1
- package/package/cc-switch-sdk/dist/schemas.js +2 -38
- package/package/cc-switch-sdk/dist/sdk.d.ts +0 -1
- package/package/cc-switch-sdk/dist/sdk.js +2 -470
- package/package/cc-switch-sdk/dist/services/ai-config-service.d.ts +0 -1
- package/package/cc-switch-sdk/dist/services/ai-config-service.js +4 -307
- package/package/cc-switch-sdk/dist/services/mcp-model.d.ts +0 -1
- package/package/cc-switch-sdk/dist/services/mcp-model.js +2 -37
- package/package/cc-switch-sdk/dist/services/mcp-service.d.ts +0 -1
- package/package/cc-switch-sdk/dist/services/mcp-service.js +2 -9
- package/package/cc-switch-sdk/dist/services/skill-model.d.ts +0 -1
- package/package/cc-switch-sdk/dist/services/skill-model.js +2 -41
- package/package/cc-switch-sdk/dist/services/skill-service.d.ts +0 -1
- package/package/cc-switch-sdk/dist/services/skill-service.js +2 -9
- package/package/cc-switch-sdk/dist/state.d.ts +0 -1
- package/package/cc-switch-sdk/dist/state.js +2 -19
- package/package/cc-switch-sdk/dist/types.d.ts +0 -1
- package/package/cc-switch-sdk/dist/types.js +1 -1
- package/package/cc-switch-sdk/dist/utils/fs.d.ts +0 -1
- package/package/cc-switch-sdk/dist/utils/fs.js +3 -114
- package/package/cc-switch-sdk/dist/utils/id.d.ts +0 -1
- package/package/cc-switch-sdk/dist/utils/id.js +2 -12
- package/package/cc-switch-sdk/package.json +1 -1
- package/package.json +4 -2
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +0 -1
- package/dist/adapter/AdapterRegistry.d.ts.map +0 -1
- package/dist/adapter/ClaudeSdkAdapter.d.ts.map +0 -1
- package/dist/adapter/CodexSdkAdapter.d.ts.map +0 -1
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +0 -1
- package/dist/adapter/SdkProviderSpi.d.ts.map +0 -1
- package/dist/adapter/acode-tool-names.d.ts.map +0 -1
- package/dist/adapter/idle-poll-loop.d.ts.map +0 -1
- package/dist/adapter/index.d.ts.map +0 -1
- package/dist/adapter/instance-ai-config-resolver.d.ts.map +0 -1
- package/dist/adapter/types.d.ts.map +0 -1
- package/dist/adapters/cc-switch/common.d.ts.map +0 -1
- package/dist/adapters/cc-switch/index.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-acode.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-claude.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-claudecode.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-codex.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-opencode.d.ts.map +0 -1
- package/dist/adapters/cc-switch/mcp-placeholder.d.ts.map +0 -1
- package/dist/adapters/cc-switch/skill-acode.d.ts.map +0 -1
- package/dist/adapters/cc-switch/skill-claude.d.ts.map +0 -1
- package/dist/adapters/cc-switch/skill-claudecode.d.ts.map +0 -1
- package/dist/adapters/cc-switch/skill-codex.d.ts.map +0 -1
- package/dist/adapters/cc-switch/skill-directory.d.ts.map +0 -1
- package/dist/adapters/cc-switch/skill-opencode.d.ts.map +0 -1
- package/dist/adapters/cc-switch/skill-placeholder.d.ts.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/middleware/auth.d.ts.map +0 -1
- package/dist/routes/acode-todos.d.ts.map +0 -1
- package/dist/routes/ai-sources.d.ts.map +0 -1
- package/dist/routes/dashboard.d.ts.map +0 -1
- package/dist/routes/events.d.ts.map +0 -1
- package/dist/routes/file-browser.d.ts.map +0 -1
- package/dist/routes/file-diff-rollback.d.ts.map +0 -1
- package/dist/routes/file-snapshot.d.ts.map +0 -1
- package/dist/routes/git.d.ts.map +0 -1
- package/dist/routes/instance.d.ts.map +0 -1
- package/dist/routes/mcp.d.ts.map +0 -1
- package/dist/routes/processes.d.ts.map +0 -1
- package/dist/routes/properties.d.ts.map +0 -1
- package/dist/routes/pty.d.ts.map +0 -1
- package/dist/routes/runtime-binding.d.ts.map +0 -1
- package/dist/routes/sessions.d.ts.map +0 -1
- package/dist/routes/skills.d.ts.map +0 -1
- package/dist/routes/sub-agents.d.ts.map +0 -1
- package/dist/routes/system-metrics.d.ts.map +0 -1
- package/dist/routes/task-context.d.ts.map +0 -1
- package/dist/routes/terminal.d.ts.map +0 -1
- package/dist/routes/yml.d.ts.map +0 -1
- package/dist/services/acode-package.d.ts.map +0 -1
- package/dist/services/agent-event-store.d.ts.map +0 -1
- package/dist/services/agent-process-manager.d.ts.map +0 -1
- package/dist/services/ai-source-apply.d.ts.map +0 -1
- package/dist/services/auto-register.d.ts.map +0 -1
- package/dist/services/aws-client-agent-mcp.d.ts.map +0 -1
- package/dist/services/bridge-package-publish-hooks.d.ts.map +0 -1
- package/dist/services/cc-switch-sdk.d.ts.map +0 -1
- package/dist/services/cli-commands.d.ts.map +0 -1
- package/dist/services/event-delta-pusher.d.ts.map +0 -1
- package/dist/services/fileSnapshotStore.d.ts.map +0 -1
- package/dist/services/instance-ai-config-service.d.ts.map +0 -1
- package/dist/services/instance-init-service.d.ts.map +0 -1
- package/dist/services/instance-service.d.ts.map +0 -1
- package/dist/services/instance-state.d.ts.map +0 -1
- package/dist/services/legacy-path-migration.d.ts.map +0 -1
- package/dist/services/lifecycle-state.d.ts.map +0 -1
- package/dist/services/mcp-launch-binding-queue.d.ts.map +0 -1
- package/dist/services/orphan-monitor.d.ts.map +0 -1
- package/dist/services/panel-auth.d.ts.map +0 -1
- package/dist/services/process-detector.d.ts.map +0 -1
- package/dist/services/process-registry.d.ts.map +0 -1
- package/dist/services/ring-buffer.d.ts.map +0 -1
- package/dist/services/runtime-binding.d.ts.map +0 -1
- package/dist/services/runtime-lifecycle-policy.d.ts.map +0 -1
- package/dist/services/session-output.d.ts.map +0 -1
- package/dist/services/startup-config-wizard.d.ts.map +0 -1
- package/dist/services/sub-agent-manager-instance.d.ts.map +0 -1
- package/dist/services/system-metrics.d.ts.map +0 -1
- package/dist/services/terminal-persistence.d.ts.map +0 -1
- package/dist/services/tool-installer.d.ts.map +0 -1
- package/dist/services/tunnel-client.d.ts.map +0 -1
- package/dist/services/windows-service-manager.d.ts.map +0 -1
- package/dist/services/workspace-files.d.ts.map +0 -1
- package/dist/services/workspace-watch.d.ts.map +0 -1
- package/dist/services/workspaceTaskChangeWatcher.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/utils/file-utils.d.ts.map +0 -1
- package/dist/utils/instance-ai-config-debug-logger.d.ts.map +0 -1
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/mcp-utils.d.ts.map +0 -1
- package/dist/utils/path-utils.d.ts.map +0 -1
- package/dist/utils/sdk-package-loader.d.ts.map +0 -1
- package/dist/utils/validation.d.ts.map +0 -1
- package/dist/utils/yaml-utils.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/constants.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/errors.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/index.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/schemas.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/sdk.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/ai-config-service.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/mcp-model.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/mcp-service.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/skill-model.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/skill-service.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/state.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/types.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/utils/fs.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/utils/id.d.ts.map +0 -1
- package/package/acode/dist/ask-user-question-tool.d.ts.map +0 -1
- package/package/acode/dist/built-in-file-tools.d.ts.map +0 -1
- package/package/acode/dist/built-in-symbols-tool.d.ts.map +0 -1
- package/package/acode/dist/builtins/commandArtifacts.d.ts.map +0 -1
- package/package/acode/dist/builtins/commandOutputEvents.d.ts.map +0 -1
- package/package/acode/dist/builtins/shellCommandPreparation.d.ts.map +0 -1
- package/package/acode/dist/builtins/shellCommandResult.d.ts.map +0 -1
- package/package/acode/dist/builtins/shellCommandRunner.d.ts.map +0 -1
- package/package/acode/dist/builtins/shellOutputDecoder.d.ts.map +0 -1
- package/package/acode/dist/cli.d.ts.map +0 -1
- package/package/acode/dist/commands/commandLoader.d.ts.map +0 -1
- package/package/acode/dist/commands/commandParser.d.ts.map +0 -1
- package/package/acode/dist/commands/commandRegistry.d.ts.map +0 -1
- package/package/acode/dist/compaction.d.ts.map +0 -1
- package/package/acode/dist/config.d.ts.map +0 -1
- package/package/acode/dist/fs-utils.d.ts.map +0 -1
- package/package/acode/dist/hooks/hookBus.d.ts.map +0 -1
- package/package/acode/dist/index.d.ts.map +0 -1
- package/package/acode/dist/mcpToolNames.d.ts.map +0 -1
- package/package/acode/dist/parsers/ast-cache.d.ts.map +0 -1
- package/package/acode/dist/parsers/language-detector.d.ts.map +0 -1
- package/package/acode/dist/parsers/language-queries.d.ts.map +0 -1
- package/package/acode/dist/parsers/symbol-extractor.d.ts.map +0 -1
- package/package/acode/dist/parsers/tree-sitter-loader.d.ts.map +0 -1
- package/package/acode/dist/parsers/types.d.ts.map +0 -1
- package/package/acode/dist/runtime.d.ts.map +0 -1
- package/package/acode/dist/skill-tool.d.ts.map +0 -1
- package/package/acode/dist/sub-agent-manager.d.ts.map +0 -1
- package/package/acode/dist/sub-agent-store.d.ts.map +0 -1
- package/package/acode/dist/sub-agent-tools.d.ts.map +0 -1
- package/package/acode/dist/sub-agent-types.d.ts.map +0 -1
- package/package/acode/dist/todo-store.d.ts.map +0 -1
- package/package/acode/dist/todo-tool.d.ts.map +0 -1
- package/package/acode/dist/types.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/agent-client.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/agent-client.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/agent-client.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/agent-client.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/config.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/config.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/config.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/config.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/constants.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/constants.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/context-manager.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/context-manager.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/context-manager.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/context-manager.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/http-client.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/http-client.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/http-client.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/http-client.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/index.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/index.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/logger.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/logger.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/logger.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/logger.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/mcp-tools.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/memory-store.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/memory-store.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/memory-store.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/memory-store.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/memory-tools.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/message-buffer.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/messageContent.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/messageContent.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/server-memory-store.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.test.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/types.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js.map +0 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.test.d.ts.map +0 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js.map +0 -1
- package/package/cc-switch-sdk/dist/constants.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/errors.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/index.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/schemas.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/sdk.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/services/ai-config-service.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/services/mcp-model.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/services/mcp-service.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/services/skill-model.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/services/skill-service.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/state.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/types.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/utils/fs.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/utils/id.d.ts.map +0 -1
package/dist/routes/git.js
CHANGED
|
@@ -1,1900 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import axios from 'axios';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
*
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
stat = await fs.stat(workspacePath);
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
const err = error;
|
|
33
|
-
if (err.code === 'ENOENT') {
|
|
34
|
-
throw new Error(`workspace path does not exist: ${workspacePath}`);
|
|
35
|
-
}
|
|
36
|
-
throw new Error(`workspace path is not accessible: ${workspacePath}${err.message ? ` (${err.message})` : ''}`);
|
|
37
|
-
}
|
|
38
|
-
if (!stat.isDirectory()) {
|
|
39
|
-
throw new Error(`workspace path is not a directory: ${workspacePath}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const STABLE_STASH_OBJECT_HASH_PATTERN = /^[0-9a-f]{40}$/i;
|
|
43
|
-
/**
|
|
44
|
-
* 判断 Git diff 汇总行是否应该展示在差异树中。
|
|
45
|
-
* 普通 modified 且文本增删均为 0 通常是 filemode/元数据噪声,避免显示为“无差异内容”。
|
|
46
|
-
*/
|
|
47
|
-
export function shouldIncludeGitDiffSummaryFile(status, additions, deletions, isBinaryDiff) {
|
|
48
|
-
if (status === 'added' || status === 'deleted' || status === 'renamed') {
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
return isBinaryDiff || additions > 0 || deletions > 0;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* 判断 git stash 是否因仓库尚无初始提交而失败。
|
|
55
|
-
* 主流程:兼容 Git 不同版本输出,将底层英文错误归一成可面向用户的诊断。
|
|
56
|
-
*/
|
|
57
|
-
export function isGitStashMissingInitialCommitError(output) {
|
|
58
|
-
const normalized = String(output || '').toLowerCase();
|
|
59
|
-
return normalized.includes('you do not have the initial commit yet')
|
|
60
|
-
|| (normalized.includes('bad revision') && normalized.includes('head'))
|
|
61
|
-
|| (normalized.includes('ambiguous argument') && normalized.includes('head'));
|
|
62
|
-
}
|
|
63
|
-
export function createMissingInitialCommitSnapshotError() {
|
|
64
|
-
return '当前 Git 仓库还没有初始提交,无法创建快照。请先提交一次初始提交后再创建快照。';
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* 判断 git stash push 是否明确表示没有本地变更。
|
|
68
|
-
* 主流程:兼容不同 Git 版本把 no-change 信息输出到 stdout 或 stderr,且可能返回 0 或非 0。
|
|
69
|
-
*/
|
|
70
|
-
export function isGitStashNoChangesOutput(output) {
|
|
71
|
-
const normalized = String(output || '').toLowerCase();
|
|
72
|
-
return normalized.includes('no local changes to save')
|
|
73
|
-
|| normalized.includes('no changes to save')
|
|
74
|
-
|| normalized.includes('没有本地变更需要保存');
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* 生成 AgentsWork 持久化 stash 快照 ref。
|
|
78
|
-
* 主流程:仅接受稳定 stash commit hash,返回项目私有 refs 命名空间下的引用路径。
|
|
79
|
-
*/
|
|
80
|
-
export function createAgentsWorkStashSnapshotRef(stashRef) {
|
|
81
|
-
const normalizedRef = String(stashRef || '').trim();
|
|
82
|
-
if (!STABLE_STASH_OBJECT_HASH_PATTERN.test(normalizedRef)) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
return `refs/agentswork/stash-snapshots/${normalizedRef.toLowerCase()}`;
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* 解析 git stash list --format 输出行。
|
|
89
|
-
* 主流程:优先返回稳定 stash commit hash 作为 ref,同时保留 stash@{n} 供界面展示。
|
|
90
|
-
*/
|
|
91
|
-
export function parseGitStashListLine(line) {
|
|
92
|
-
const rawLine = String(line || '').trim();
|
|
93
|
-
if (!rawLine)
|
|
94
|
-
return null;
|
|
95
|
-
const fields = rawLine.split('\0');
|
|
96
|
-
if (fields.length >= 3) {
|
|
97
|
-
const stableRef = fields[0]?.trim();
|
|
98
|
-
const stashName = fields[1]?.trim();
|
|
99
|
-
const subject = fields[2]?.trim() || stashName;
|
|
100
|
-
const createdAt = fields[3]?.trim() || null;
|
|
101
|
-
if (!stableRef || !stashName)
|
|
102
|
-
return null;
|
|
103
|
-
const parsedSubject = parseGitStashSubject(subject || '');
|
|
104
|
-
return {
|
|
105
|
-
ref: stableRef,
|
|
106
|
-
stashName,
|
|
107
|
-
message: parsedSubject.message,
|
|
108
|
-
branch: parsedSubject.branch,
|
|
109
|
-
createdAt,
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
const match = rawLine.match(/^(stash@\{\d+\})(?::\s*)?(.*)$/);
|
|
113
|
-
if (!match)
|
|
114
|
-
return null;
|
|
115
|
-
const stashName = match[1];
|
|
116
|
-
const parsedSubject = parseGitStashSubject(match[2]?.trim() || stashName);
|
|
117
|
-
return {
|
|
118
|
-
ref: stashName,
|
|
119
|
-
stashName,
|
|
120
|
-
message: parsedSubject.message,
|
|
121
|
-
branch: parsedSubject.branch,
|
|
122
|
-
createdAt: null,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* 从 stash subject 中拆出分支和用户消息。
|
|
127
|
-
*/
|
|
128
|
-
function parseGitStashSubject(subject) {
|
|
129
|
-
const normalized = String(subject || '').trim();
|
|
130
|
-
const branchMatch = normalized.match(/^(?:On|WIP on)\s+([^:]+):\s*(.*)$/);
|
|
131
|
-
if (branchMatch) {
|
|
132
|
-
return {
|
|
133
|
-
branch: branchMatch[1].trim(),
|
|
134
|
-
message: branchMatch[2].trim() || normalized,
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
return { branch: '', message: normalized };
|
|
138
|
-
}
|
|
139
|
-
/**
|
|
140
|
-
* 从 git stash list -n 1 输出解析最近一次 stash。
|
|
141
|
-
* 主流程:提取稳定 stash commit hash,供无变更快照复用最近快照引用。
|
|
142
|
-
*/
|
|
143
|
-
export function parseLatestGitStash(output) {
|
|
144
|
-
const firstLine = String(output || '').split('\n').find(line => line.trim());
|
|
145
|
-
if (!firstLine)
|
|
146
|
-
return null;
|
|
147
|
-
const parsed = parseGitStashListLine(firstLine);
|
|
148
|
-
if (!parsed)
|
|
149
|
-
return null;
|
|
150
|
-
return {
|
|
151
|
-
ref: parsed.ref,
|
|
152
|
-
stashName: parsed.stashName || parsed.ref,
|
|
153
|
-
message: parsed.message || parsed.ref,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* 执行 git 命令并返回输出
|
|
158
|
-
*/
|
|
159
|
-
async function execGitCommand(workspacePath, args) {
|
|
160
|
-
return new Promise((resolve) => {
|
|
161
|
-
let settled = false;
|
|
162
|
-
let timeout = null;
|
|
163
|
-
const finish = (result) => {
|
|
164
|
-
if (settled) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
settled = true;
|
|
168
|
-
if (timeout) {
|
|
169
|
-
clearTimeout(timeout);
|
|
170
|
-
}
|
|
171
|
-
resolve(result);
|
|
172
|
-
};
|
|
173
|
-
const child = spawn('git', args, {
|
|
174
|
-
cwd: workspacePath,
|
|
175
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
176
|
-
});
|
|
177
|
-
let stdout = '';
|
|
178
|
-
let stderr = '';
|
|
179
|
-
child.stdout?.on('data', (chunk) => {
|
|
180
|
-
stdout += String(chunk || '');
|
|
181
|
-
});
|
|
182
|
-
child.stderr?.on('data', (chunk) => {
|
|
183
|
-
stderr += String(chunk || '');
|
|
184
|
-
});
|
|
185
|
-
timeout = setTimeout(() => {
|
|
186
|
-
const timeoutMs = resolveGitCommandTimeoutMs();
|
|
187
|
-
logger.warn(`[runtime-bridge] git command timed out after ${timeoutMs}ms: git ${args.join(' ')} (cwd=${workspacePath})`);
|
|
188
|
-
child.kill('SIGKILL');
|
|
189
|
-
finish({
|
|
190
|
-
stdout,
|
|
191
|
-
stderr: stderr || `git command timed out after ${timeoutMs}ms`,
|
|
192
|
-
exitCode: 124,
|
|
193
|
-
});
|
|
194
|
-
}, resolveGitCommandTimeoutMs());
|
|
195
|
-
child.on('error', (error) => {
|
|
196
|
-
finish({ stdout: '', stderr: error.message, exitCode: 1 });
|
|
197
|
-
});
|
|
198
|
-
child.on('close', (code) => {
|
|
199
|
-
finish({ stdout, stderr, exitCode: code ?? 1 });
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* 持久化 stash 快照引用。
|
|
205
|
-
* 具体功能:为稳定 stash commit hash 创建私有 Git ref;失败时记录警告并保留调用方的兼容回退。
|
|
206
|
-
*/
|
|
207
|
-
export async function persistAgentsWorkStashSnapshotRef(workspacePath, stashRef) {
|
|
208
|
-
const persistentRef = createAgentsWorkStashSnapshotRef(stashRef);
|
|
209
|
-
if (!persistentRef) {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
const result = await execGitCommand(workspacePath, ['update-ref', persistentRef, stashRef]);
|
|
213
|
-
if (result.exitCode !== 0) {
|
|
214
|
-
logger.warn(`[Git] 持久化 stash 快照 ref 失败: ref=${persistentRef}, error=${result.stderr || result.stdout}`);
|
|
215
|
-
return null;
|
|
216
|
-
}
|
|
217
|
-
return persistentRef;
|
|
218
|
-
}
|
|
219
|
-
function normalizeGitPath(targetPath) {
|
|
220
|
-
return String(targetPath || '').replace(/\\/g, '/').replace(/^\.\//, '').trim();
|
|
221
|
-
}
|
|
222
|
-
async function hasGitMetadataDir(targetPath) {
|
|
223
|
-
const gitDir = path.join(targetPath, '.git');
|
|
224
|
-
try {
|
|
225
|
-
const stat = await fs.stat(gitDir);
|
|
226
|
-
return stat.isDirectory() || stat.isFile();
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
async function resolveGitRepositoryContext(workspacePath) {
|
|
233
|
-
const normalizedPath = path.resolve(String(workspacePath).trim());
|
|
234
|
-
await assertGitWorkspacePathAccessible(normalizedPath);
|
|
235
|
-
const isGitRepo = await hasGitMetadataDir(normalizedPath);
|
|
236
|
-
if (isGitRepo) {
|
|
237
|
-
return {
|
|
238
|
-
workspacePath: normalizedPath,
|
|
239
|
-
repositoryRootPath: normalizedPath,
|
|
240
|
-
hasRepository: true,
|
|
241
|
-
isGitRepo: true,
|
|
242
|
-
usingParentGitRepo: false,
|
|
243
|
-
relativeWorkspacePath: ''
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
const repoRootResult = await execGitCommand(normalizedPath, ['rev-parse', '--show-toplevel']);
|
|
247
|
-
if (repoRootResult.exitCode !== 0) {
|
|
248
|
-
return {
|
|
249
|
-
workspacePath: normalizedPath,
|
|
250
|
-
repositoryRootPath: null,
|
|
251
|
-
hasRepository: false,
|
|
252
|
-
isGitRepo: false,
|
|
253
|
-
usingParentGitRepo: false,
|
|
254
|
-
relativeWorkspacePath: ''
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
const repositoryRootPath = path.resolve(repoRootResult.stdout.trim());
|
|
258
|
-
const relativeWorkspacePath = normalizeGitPath(path.relative(repositoryRootPath, normalizedPath));
|
|
259
|
-
return {
|
|
260
|
-
workspacePath: normalizedPath,
|
|
261
|
-
repositoryRootPath,
|
|
262
|
-
hasRepository: true,
|
|
263
|
-
isGitRepo: false,
|
|
264
|
-
usingParentGitRepo: repositoryRootPath !== normalizedPath,
|
|
265
|
-
relativeWorkspacePath
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
function createScopedGitArgs(baseArgs, context) {
|
|
269
|
-
if (!context.relativeWorkspacePath) {
|
|
270
|
-
return [...baseArgs];
|
|
271
|
-
}
|
|
272
|
-
// `git status` does not accept --relative, only a pathspec.
|
|
273
|
-
const supportsRelative = !baseArgs.includes('status');
|
|
274
|
-
return [
|
|
275
|
-
...baseArgs,
|
|
276
|
-
...(supportsRelative ? [`--relative=${context.relativeWorkspacePath}`] : []),
|
|
277
|
-
'--',
|
|
278
|
-
context.relativeWorkspacePath
|
|
279
|
-
];
|
|
280
|
-
}
|
|
281
|
-
function resolveRepositoryRelativeFilePath(filePath, context) {
|
|
282
|
-
const normalizedFilePath = normalizeGitPath(filePath);
|
|
283
|
-
if (!context.relativeWorkspacePath) {
|
|
284
|
-
return normalizedFilePath;
|
|
285
|
-
}
|
|
286
|
-
return normalizeGitPath(path.posix.join(context.relativeWorkspacePath, normalizedFilePath));
|
|
287
|
-
}
|
|
288
|
-
function resolveGitDiffStatus(statusToken) {
|
|
289
|
-
const normalizedToken = String(statusToken || '').trim().toUpperCase();
|
|
290
|
-
if (normalizedToken.startsWith('A')) {
|
|
291
|
-
return 'added';
|
|
292
|
-
}
|
|
293
|
-
if (normalizedToken.startsWith('D')) {
|
|
294
|
-
return 'deleted';
|
|
295
|
-
}
|
|
296
|
-
if (normalizedToken.startsWith('R')) {
|
|
297
|
-
return 'renamed';
|
|
298
|
-
}
|
|
299
|
-
return 'modified';
|
|
300
|
-
}
|
|
301
|
-
function parseRenameDisplayPath(rawPath) {
|
|
302
|
-
const normalizedPath = normalizeGitPath(rawPath);
|
|
303
|
-
if (!normalizedPath.includes('=>')) {
|
|
304
|
-
return normalizedPath;
|
|
305
|
-
}
|
|
306
|
-
const braceMatch = normalizedPath.match(/^(.*)\{([^{}]+)=>([^{}]+)\}(.*)$/);
|
|
307
|
-
if (braceMatch) {
|
|
308
|
-
const prefix = braceMatch[1];
|
|
309
|
-
const renamedTarget = braceMatch[3];
|
|
310
|
-
const suffix = braceMatch[4];
|
|
311
|
-
return normalizeGitPath(`${prefix}${renamedTarget}${suffix}`);
|
|
312
|
-
}
|
|
313
|
-
const arrowIndex = normalizedPath.lastIndexOf('=>');
|
|
314
|
-
if (arrowIndex >= 0) {
|
|
315
|
-
return normalizeGitPath(normalizedPath.slice(arrowIndex + 2));
|
|
316
|
-
}
|
|
317
|
-
return normalizedPath;
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* 解析 git diff 的 name-status 与 numstat 输出,生成前端文件树需要的汇总数据。
|
|
321
|
-
*/
|
|
322
|
-
export function parseGitStatusStagedPaths(output) {
|
|
323
|
-
const stagedPaths = new Set();
|
|
324
|
-
const lines = String(output || '').split('\n').filter(line => line.trim());
|
|
325
|
-
for (const line of lines) {
|
|
326
|
-
const indexStatus = line[0] || ' ';
|
|
327
|
-
if (indexStatus === ' ' || indexStatus === '?') {
|
|
328
|
-
continue;
|
|
329
|
-
}
|
|
330
|
-
const rawPath = line.slice(3).trim();
|
|
331
|
-
const pathParts = rawPath.split(' -> ');
|
|
332
|
-
const displayPath = parseRenameDisplayPath(pathParts[pathParts.length - 1] || rawPath);
|
|
333
|
-
if (displayPath) {
|
|
334
|
-
stagedPaths.add(displayPath);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return stagedPaths;
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* 解析 git diff 的 name-status 与 numstat 输出,生成前端文件树需要的汇总数据。
|
|
341
|
-
*/
|
|
342
|
-
export function parseGitDiffSummary(nameStatusOutput, numstatOutput, stagedPaths = new Set(), forceStaged = false) {
|
|
343
|
-
const statusMap = new Map();
|
|
344
|
-
const nameStatusLines = nameStatusOutput.split('\n').filter(line => line.trim());
|
|
345
|
-
for (const line of nameStatusLines) {
|
|
346
|
-
const parts = line.split('\t');
|
|
347
|
-
if (parts.length < 2) {
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
const status = resolveGitDiffStatus(parts[0]);
|
|
351
|
-
const pathIndex = status === 'renamed' && parts.length >= 3 ? 2 : 1;
|
|
352
|
-
const displayPath = parseRenameDisplayPath(parts[pathIndex]);
|
|
353
|
-
if (!displayPath) {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
statusMap.set(displayPath, status);
|
|
357
|
-
}
|
|
358
|
-
const files = [];
|
|
359
|
-
const lines = numstatOutput.split('\n').filter(line => line.trim());
|
|
360
|
-
for (const line of lines) {
|
|
361
|
-
const parts = line.split('\t');
|
|
362
|
-
if (parts.length < 3)
|
|
363
|
-
continue;
|
|
364
|
-
const isBinaryDiff = parts[0] === '-' || parts[1] === '-';
|
|
365
|
-
const additions = parts[0] === '-' ? 0 : parseInt(parts[0], 10) || 0;
|
|
366
|
-
const deletions = parts[1] === '-' ? 0 : parseInt(parts[1], 10) || 0;
|
|
367
|
-
const filePath = parseRenameDisplayPath(parts.slice(2).join('\t').trim());
|
|
368
|
-
const status = statusMap.get(filePath) || 'modified';
|
|
369
|
-
if (!filePath || !shouldIncludeGitDiffSummaryFile(status, additions, deletions, isBinaryDiff)) {
|
|
370
|
-
continue;
|
|
371
|
-
}
|
|
372
|
-
files.push({ path: filePath, status, additions, deletions, staged: forceStaged || stagedPaths.has(filePath) });
|
|
373
|
-
}
|
|
374
|
-
return files;
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* 从 git status --porcelain 输出中提取未跟踪 (untracked) 路径。
|
|
378
|
-
* `??` 前缀表示未跟踪文件或目录。
|
|
379
|
-
*/
|
|
380
|
-
export function parseGitStatusUntrackedPaths(output) {
|
|
381
|
-
const paths = [];
|
|
382
|
-
const lines = String(output || '').split('\n').filter(line => line.trim());
|
|
383
|
-
for (const line of lines) {
|
|
384
|
-
if (line.startsWith('?? ')) {
|
|
385
|
-
const rawPath = line.slice(3).trim();
|
|
386
|
-
if (rawPath) {
|
|
387
|
-
// 目录路径通常以 / 结尾,去掉以便统一处理
|
|
388
|
-
paths.push(rawPath.replace(/\/$/, ''));
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return paths;
|
|
393
|
-
}
|
|
394
|
-
/**
|
|
395
|
-
* 遍历未跟踪路径,将目录展开为具体文件列表,返回扁平的 GitDiffSummaryFile 数组。
|
|
396
|
-
* 主流程:对每个路径判断是文件还是目录;目录递归遍历,收集所有文件。
|
|
397
|
-
*/
|
|
398
|
-
async function collectUntrackedFiles(repoRootPath, untrackedPaths, existingFilePaths, maxDepth = 10) {
|
|
399
|
-
const files = [];
|
|
400
|
-
async function walk(dirRelativePath, depth) {
|
|
401
|
-
if (depth > maxDepth)
|
|
402
|
-
return;
|
|
403
|
-
const absDir = path.join(repoRootPath, dirRelativePath);
|
|
404
|
-
let entries;
|
|
405
|
-
try {
|
|
406
|
-
entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
407
|
-
}
|
|
408
|
-
catch {
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
for (const entry of entries) {
|
|
412
|
-
// 跳过常见的 VCS 与工具元数据目录,避免扫描噪声
|
|
413
|
-
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
414
|
-
continue;
|
|
415
|
-
const entryRelativePath = dirRelativePath
|
|
416
|
-
? `${dirRelativePath}/${entry.name}`
|
|
417
|
-
: entry.name;
|
|
418
|
-
if (entry.isDirectory()) {
|
|
419
|
-
await walk(entryRelativePath, depth + 1);
|
|
420
|
-
}
|
|
421
|
-
else if (entry.isFile()) {
|
|
422
|
-
const normalizedPath = normalizeGitPath(entryRelativePath);
|
|
423
|
-
if (normalizedPath && !existingFilePaths.has(normalizedPath)) {
|
|
424
|
-
files.push({
|
|
425
|
-
path: normalizedPath,
|
|
426
|
-
status: 'added',
|
|
427
|
-
additions: 0,
|
|
428
|
-
deletions: 0,
|
|
429
|
-
staged: false,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
for (const untrackedPath of untrackedPaths) {
|
|
436
|
-
const normalizedPath = normalizeGitPath(untrackedPath);
|
|
437
|
-
if (!normalizedPath)
|
|
438
|
-
continue;
|
|
439
|
-
const absPath = path.join(repoRootPath, untrackedPath);
|
|
440
|
-
let stat;
|
|
441
|
-
try {
|
|
442
|
-
stat = await fs.stat(absPath);
|
|
443
|
-
}
|
|
444
|
-
catch {
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
if (stat.isFile()) {
|
|
448
|
-
if (!existingFilePaths.has(normalizedPath)) {
|
|
449
|
-
files.push({
|
|
450
|
-
path: normalizedPath,
|
|
451
|
-
status: 'added',
|
|
452
|
-
additions: 0,
|
|
453
|
-
deletions: 0,
|
|
454
|
-
staged: false,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
else if (stat.isDirectory()) {
|
|
459
|
-
await walk(normalizedPath, 0);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return files;
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* 合并工作区与暂存区差异汇总。
|
|
466
|
-
* 主流程:以路径去重,保留工作区展示顺序;同一路径若也存在暂存变更则标记为 staged 并累加统计。
|
|
467
|
-
*/
|
|
468
|
-
export function mergeGitDiffSummaryFiles(unstagedFiles, stagedFiles) {
|
|
469
|
-
const fileMap = new Map();
|
|
470
|
-
for (const file of unstagedFiles) {
|
|
471
|
-
fileMap.set(file.path, { ...file });
|
|
472
|
-
}
|
|
473
|
-
for (const stagedFile of stagedFiles) {
|
|
474
|
-
const existingFile = fileMap.get(stagedFile.path);
|
|
475
|
-
if (!existingFile) {
|
|
476
|
-
fileMap.set(stagedFile.path, { ...stagedFile, staged: true });
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
fileMap.set(stagedFile.path, {
|
|
480
|
-
...existingFile,
|
|
481
|
-
status: existingFile.status === 'modified' ? stagedFile.status : existingFile.status,
|
|
482
|
-
additions: existingFile.additions + stagedFile.additions,
|
|
483
|
-
deletions: existingFile.deletions + stagedFile.deletions,
|
|
484
|
-
staged: true,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
return [...fileMap.values()];
|
|
488
|
-
}
|
|
489
|
-
function normalizeCommitHash(commitHash) {
|
|
490
|
-
return String(commitHash || '').trim();
|
|
491
|
-
}
|
|
492
|
-
export function normalizeGitCommitMessage(message) {
|
|
493
|
-
return String(message || '').trim();
|
|
494
|
-
}
|
|
495
|
-
export function createScopedGitPathspecArgs(context) {
|
|
496
|
-
return ['--', context.relativeWorkspacePath || '.'];
|
|
497
|
-
}
|
|
498
|
-
export function createScopedFilePathspecArgs(filePath, context) {
|
|
499
|
-
return ['--', resolveRepositoryRelativeFilePath(filePath, context)];
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* 解析 git log 的 NUL 分隔输出,避免提交标题中的特殊字符破坏字段切分。
|
|
503
|
-
*/
|
|
504
|
-
function parseGitCommitLog(output) {
|
|
505
|
-
const fields = output.split('\0');
|
|
506
|
-
const commits = [];
|
|
507
|
-
for (let index = 0; index + 4 < fields.length; index += 5) {
|
|
508
|
-
const hash = fields[index]?.trim();
|
|
509
|
-
if (!hash) {
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
commits.push({
|
|
513
|
-
hash,
|
|
514
|
-
shortHash: fields[index + 1]?.trim() || hash.slice(0, 8),
|
|
515
|
-
authorName: fields[index + 2]?.trim() || 'unknown',
|
|
516
|
-
authorDate: fields[index + 3]?.trim() || '',
|
|
517
|
-
subject: fields[index + 4]?.trim() || hash,
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
return commits;
|
|
521
|
-
}
|
|
522
|
-
/**
|
|
523
|
-
* 检查路径是否为 git 仓库
|
|
524
|
-
* POST /runtime/git/check
|
|
525
|
-
*/
|
|
526
|
-
gitRouter.post('/git/check', validateToken, async (req, res) => {
|
|
527
|
-
const { workspacePath } = req.body || {};
|
|
528
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
529
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
try {
|
|
533
|
-
const context = await resolveGitRepositoryContext(String(workspacePath).trim());
|
|
534
|
-
res.json({
|
|
535
|
-
ok: true,
|
|
536
|
-
isGitRepo: context.isGitRepo,
|
|
537
|
-
hasRepository: context.hasRepository,
|
|
538
|
-
usingParentGitRepo: context.usingParentGitRepo,
|
|
539
|
-
workspacePath: context.workspacePath,
|
|
540
|
-
repositoryRootPath: context.repositoryRootPath,
|
|
541
|
-
relativeWorkspacePath: context.relativeWorkspacePath
|
|
542
|
-
});
|
|
543
|
-
return;
|
|
544
|
-
}
|
|
545
|
-
catch (error) {
|
|
546
|
-
const err = error;
|
|
547
|
-
res.status(500).json({ error: err.message || 'check git repo failed' });
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
/**
|
|
551
|
-
* 创建 git stash 快照
|
|
552
|
-
* POST /runtime/git/stash/create
|
|
553
|
-
*/
|
|
554
|
-
gitRouter.post('/git/stash/create', validateToken, async (req, res) => {
|
|
555
|
-
const { workspacePath, message } = req.body || {};
|
|
556
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
557
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
try {
|
|
561
|
-
const normalizedPath = path.resolve(String(workspacePath).trim());
|
|
562
|
-
const defaultMessage = `快照(${new Date().toLocaleString('zh-CN', {
|
|
563
|
-
year: 'numeric',
|
|
564
|
-
month: '2-digit',
|
|
565
|
-
day: '2-digit',
|
|
566
|
-
hour: '2-digit',
|
|
567
|
-
minute: '2-digit'
|
|
568
|
-
})})`;
|
|
569
|
-
const stashMessage = message && String(message).trim()
|
|
570
|
-
? `快照: ${String(message).trim()}`
|
|
571
|
-
: defaultMessage;
|
|
572
|
-
const result = await execGitCommand(normalizedPath, ['stash', 'push', '-m', stashMessage]);
|
|
573
|
-
if (isGitStashNoChangesOutput(`${result.stdout}\n${result.stderr}`)) {
|
|
574
|
-
const latestResult = await execGitCommand(normalizedPath, ['stash', 'list', '-n', '1', '--format=%H%x00%gd%x00%gs%x00%cr']);
|
|
575
|
-
const latestStash = latestResult.exitCode === 0 ? parseLatestGitStash(latestResult.stdout) : null;
|
|
576
|
-
const persistentStashRef = latestStash
|
|
577
|
-
? await persistAgentsWorkStashSnapshotRef(normalizedPath, latestStash.ref)
|
|
578
|
-
: null;
|
|
579
|
-
res.json({
|
|
580
|
-
ok: true,
|
|
581
|
-
stashRef: persistentStashRef ?? latestStash?.ref ?? null,
|
|
582
|
-
stashName: latestStash?.stashName ?? null,
|
|
583
|
-
message: latestStash
|
|
584
|
-
? `没有本地变更需要保存,已复用最近快照 ${latestStash.stashName}`
|
|
585
|
-
: '没有本地变更需要保存',
|
|
586
|
-
noChanges: true,
|
|
587
|
-
reusedLatestStash: Boolean(latestStash),
|
|
588
|
-
workspacePath: normalizedPath
|
|
589
|
-
});
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
if (result.exitCode !== 0) {
|
|
593
|
-
if (isGitStashMissingInitialCommitError(`${result.stdout}\n${result.stderr}`)) {
|
|
594
|
-
res.status(400).json({ error: createMissingInitialCommitSnapshotError() });
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
res.status(400).json({ error: result.stderr || 'git stash push failed' });
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
const listResult = await execGitCommand(normalizedPath, ['stash', 'list', '-n', '1', '--format=%H%x00%gd%x00%gs%x00%cr']);
|
|
601
|
-
const latestStash = listResult.exitCode === 0 ? parseLatestGitStash(listResult.stdout) : null;
|
|
602
|
-
const persistentStashRef = latestStash
|
|
603
|
-
? await persistAgentsWorkStashSnapshotRef(normalizedPath, latestStash.ref)
|
|
604
|
-
: null;
|
|
605
|
-
const stashRef = persistentStashRef ?? latestStash?.ref ?? 'stash@{0}';
|
|
606
|
-
res.json({
|
|
607
|
-
ok: true,
|
|
608
|
-
stashRef,
|
|
609
|
-
stashName: latestStash?.stashName ?? null,
|
|
610
|
-
message: stashMessage,
|
|
611
|
-
workspacePath: normalizedPath
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
catch (error) {
|
|
615
|
-
const err = error;
|
|
616
|
-
res.status(500).json({ error: err.message || 'create stash failed' });
|
|
617
|
-
}
|
|
618
|
-
});
|
|
619
|
-
/**
|
|
620
|
-
* 获取 git stash 列表
|
|
621
|
-
* POST /runtime/git/stash/list
|
|
622
|
-
*/
|
|
623
|
-
gitRouter.post('/git/stash/list', validateToken, async (req, res) => {
|
|
624
|
-
const { workspacePath, limit = 20 } = req.body || {};
|
|
625
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
626
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
try {
|
|
630
|
-
const normalizedPath = path.resolve(String(workspacePath).trim());
|
|
631
|
-
const stashLimit = Math.min(Math.max(1, Number(limit) || 20), 100);
|
|
632
|
-
const result = await execGitCommand(normalizedPath, ['stash', 'list', '-n', String(stashLimit), '--format=%H%x00%gd%x00%gs%x00%cr']);
|
|
633
|
-
if (result.exitCode !== 0) {
|
|
634
|
-
res.status(400).json({ error: result.stderr || 'git stash list failed' });
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
const stashes = result.stdout
|
|
638
|
-
.split('\n')
|
|
639
|
-
.map(parseGitStashListLine)
|
|
640
|
-
.filter((stash) => stash !== null);
|
|
641
|
-
res.json({
|
|
642
|
-
ok: true,
|
|
643
|
-
stashes,
|
|
644
|
-
count: stashes.length,
|
|
645
|
-
workspacePath: normalizedPath
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
catch (error) {
|
|
649
|
-
const err = error;
|
|
650
|
-
res.status(500).json({ error: err.message || 'list stash failed' });
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
/**
|
|
654
|
-
* 应用或弹出 git stash 快照。
|
|
655
|
-
* 主流程:恢复前先清理工作区未提交变更,再执行 apply/pop,并统一处理冲突和引用不存在错误。
|
|
656
|
-
*/
|
|
657
|
-
async function restoreGitStash(req, res, mode) {
|
|
658
|
-
const { workspacePath, ref } = req.body || {};
|
|
659
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
660
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
try {
|
|
664
|
-
const normalizedPath = path.resolve(String(workspacePath).trim());
|
|
665
|
-
const stashRef = ref && String(ref).trim() ? String(ref).trim() : 'stash@{0}';
|
|
666
|
-
await execGitCommand(normalizedPath, ['checkout', '--', '.']);
|
|
667
|
-
const result = await execGitCommand(normalizedPath, ['stash', mode, stashRef]);
|
|
668
|
-
if (result.exitCode !== 0) {
|
|
669
|
-
if (result.stdout.includes('CONFLICT') || result.stderr.includes('CONFLICT')) {
|
|
670
|
-
res.json({
|
|
671
|
-
ok: false,
|
|
672
|
-
hasConflict: true,
|
|
673
|
-
message: '恢复时发生冲突,请手动解决',
|
|
674
|
-
details: result.stdout || result.stderr,
|
|
675
|
-
workspacePath: normalizedPath
|
|
676
|
-
});
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
if (result.stderr.includes('is not a valid reference')) {
|
|
680
|
-
res.status(404).json({ error: `快照 ${stashRef} 不存在` });
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
res.status(400).json({ error: result.stderr || `git stash ${mode} failed` });
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
res.json({
|
|
687
|
-
ok: true,
|
|
688
|
-
stashRef,
|
|
689
|
-
message: mode === 'apply' ? `已应用 ${stashRef}` : `已恢复 ${stashRef}`,
|
|
690
|
-
preservedStash: mode === 'apply',
|
|
691
|
-
workspacePath: normalizedPath
|
|
692
|
-
});
|
|
693
|
-
}
|
|
694
|
-
catch (error) {
|
|
695
|
-
const err = error;
|
|
696
|
-
res.status(500).json({ error: err.message || `${mode} stash failed` });
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
/**
|
|
700
|
-
* 应用 git stash 快照但不删除快照
|
|
701
|
-
* POST /runtime/git/stash/apply
|
|
702
|
-
*/
|
|
703
|
-
gitRouter.post('/git/stash/apply', validateToken, async (req, res) => {
|
|
704
|
-
await restoreGitStash(req, res, 'apply');
|
|
705
|
-
});
|
|
706
|
-
/**
|
|
707
|
-
* 恢复 git stash 快照并删除快照
|
|
708
|
-
* POST /runtime/git/stash/pop
|
|
709
|
-
*/
|
|
710
|
-
gitRouter.post('/git/stash/pop', validateToken, async (req, res) => {
|
|
711
|
-
await restoreGitStash(req, res, 'pop');
|
|
712
|
-
});
|
|
713
|
-
/**
|
|
714
|
-
* 删除指定的 git stash
|
|
715
|
-
* POST /runtime/git/stash/drop
|
|
716
|
-
*/
|
|
717
|
-
gitRouter.post('/git/stash/drop', validateToken, async (req, res) => {
|
|
718
|
-
const { workspacePath, ref } = req.body || {};
|
|
719
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
720
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
try {
|
|
724
|
-
const normalizedPath = path.resolve(String(workspacePath).trim());
|
|
725
|
-
const stashRef = ref && String(ref).trim() ? String(ref).trim() : 'stash@{0}';
|
|
726
|
-
const result = await execGitCommand(normalizedPath, ['stash', 'drop', stashRef]);
|
|
727
|
-
if (result.exitCode !== 0) {
|
|
728
|
-
if (result.stderr.includes('is not a valid reference')) {
|
|
729
|
-
res.status(404).json({ error: `快照 ${stashRef} 不存在` });
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
res.status(400).json({ error: result.stderr || 'git stash drop failed' });
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
res.json({
|
|
736
|
-
ok: true,
|
|
737
|
-
stashRef,
|
|
738
|
-
message: `已删除 ${stashRef}`,
|
|
739
|
-
workspacePath: normalizedPath
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
catch (error) {
|
|
743
|
-
const err = error;
|
|
744
|
-
res.status(500).json({ error: err.message || 'drop stash failed' });
|
|
745
|
-
}
|
|
746
|
-
});
|
|
747
|
-
/**
|
|
748
|
-
* 获取工作区未提交变更概览
|
|
749
|
-
* POST /runtime/git/diff
|
|
750
|
-
*/
|
|
751
|
-
gitRouter.post('/git/diff', validateToken, async (req, res) => {
|
|
752
|
-
const { workspacePath, repositoryPath } = req.body || {};
|
|
753
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
754
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
try {
|
|
758
|
-
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
759
|
-
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
760
|
-
res.json({
|
|
761
|
-
ok: true,
|
|
762
|
-
isGitRepo: context.isGitRepo,
|
|
763
|
-
hasRepository: false,
|
|
764
|
-
usingParentGitRepo: false,
|
|
765
|
-
hasChanges: false,
|
|
766
|
-
files: [],
|
|
767
|
-
workspacePath: context.workspacePath,
|
|
768
|
-
repositoryRootPath: null,
|
|
769
|
-
relativeWorkspacePath: ''
|
|
770
|
-
});
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
const nameStatusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['diff', '--name-status'], context));
|
|
774
|
-
if (nameStatusResult.exitCode !== 0) {
|
|
775
|
-
res.status(400).json({ error: nameStatusResult.stderr || 'git diff failed' });
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
const result = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['diff', '--numstat'], context));
|
|
779
|
-
if (result.exitCode !== 0) {
|
|
780
|
-
res.status(400).json({ error: result.stderr || 'git diff failed' });
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
const stagedNameStatusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['diff', '--cached', '--name-status'], context));
|
|
784
|
-
if (stagedNameStatusResult.exitCode !== 0) {
|
|
785
|
-
res.status(400).json({ error: stagedNameStatusResult.stderr || 'git staged diff failed' });
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
const stagedNumstatResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['diff', '--cached', '--numstat'], context));
|
|
789
|
-
if (stagedNumstatResult.exitCode !== 0) {
|
|
790
|
-
res.status(400).json({ error: stagedNumstatResult.stderr || 'git staged diff failed' });
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
const statusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs(['status', '--porcelain=v1'], context));
|
|
794
|
-
if (statusResult.exitCode !== 0) {
|
|
795
|
-
res.status(400).json({ error: statusResult.stderr || 'git status failed' });
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
const stagedPaths = parseGitStatusStagedPaths(statusResult.stdout);
|
|
799
|
-
const files = mergeGitDiffSummaryFiles(parseGitDiffSummary(nameStatusResult.stdout, result.stdout, stagedPaths), parseGitDiffSummary(stagedNameStatusResult.stdout, stagedNumstatResult.stdout, stagedPaths, true));
|
|
800
|
-
// 补充未跟踪(untracked)文件:git diff 无法感知 untracked,需从 git status 提取并展开目录
|
|
801
|
-
const untrackedPaths = parseGitStatusUntrackedPaths(statusResult.stdout);
|
|
802
|
-
if (untrackedPaths.length > 0) {
|
|
803
|
-
const existingFilePaths = new Set(files.map(f => f.path));
|
|
804
|
-
const untrackedFiles = await collectUntrackedFiles(context.repositoryRootPath, untrackedPaths, existingFilePaths);
|
|
805
|
-
files.push(...untrackedFiles);
|
|
806
|
-
}
|
|
807
|
-
res.json({
|
|
808
|
-
ok: true,
|
|
809
|
-
isGitRepo: context.isGitRepo,
|
|
810
|
-
hasRepository: true,
|
|
811
|
-
usingParentGitRepo: context.usingParentGitRepo,
|
|
812
|
-
hasChanges: files.length > 0,
|
|
813
|
-
files,
|
|
814
|
-
workspacePath: context.workspacePath,
|
|
815
|
-
repositoryRootPath: context.repositoryRootPath,
|
|
816
|
-
relativeWorkspacePath: context.relativeWorkspacePath
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
catch (error) {
|
|
820
|
-
const err = error;
|
|
821
|
-
res.status(500).json({ error: err.message || 'git diff failed' });
|
|
822
|
-
}
|
|
823
|
-
});
|
|
824
|
-
/**
|
|
825
|
-
* 检测仓库是否已配置 git author(user.name / user.email),
|
|
826
|
-
* 未配置时返回 -c 参数回退,避免 git commit 因缺少 author 信息而失败。
|
|
827
|
-
*/
|
|
828
|
-
async function buildGitAuthorFallbackArgs(repoRootPath) {
|
|
829
|
-
const authorArgs = [];
|
|
830
|
-
const nameResult = await execGitCommand(repoRootPath, ['config', 'user.name']);
|
|
831
|
-
if (nameResult.exitCode !== 0 || !nameResult.stdout.trim()) {
|
|
832
|
-
authorArgs.push('-c', 'user.name=AgentsWork');
|
|
833
|
-
}
|
|
834
|
-
const emailResult = await execGitCommand(repoRootPath, ['config', 'user.email']);
|
|
835
|
-
if (emailResult.exitCode !== 0 || !emailResult.stdout.trim()) {
|
|
836
|
-
authorArgs.push('-c', 'user.email=agent@agentswork.com');
|
|
837
|
-
}
|
|
838
|
-
return authorArgs;
|
|
839
|
-
}
|
|
840
|
-
/**
|
|
841
|
-
* 提交工作区未提交变更
|
|
842
|
-
* POST /runtime/git/commit
|
|
843
|
-
*/
|
|
844
|
-
gitRouter.post('/git/commit', validateToken, async (req, res) => {
|
|
845
|
-
const { workspacePath, repositoryPath, message } = req.body || {};
|
|
846
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
847
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
const commitMessage = normalizeGitCommitMessage(message);
|
|
851
|
-
if (!commitMessage) {
|
|
852
|
-
res.status(400).json({ error: 'commit message is required' });
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
try {
|
|
856
|
-
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
857
|
-
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
858
|
-
res.status(400).json({ error: 'workspace is not inside a git repository' });
|
|
859
|
-
return;
|
|
860
|
-
}
|
|
861
|
-
const pathspecArgs = createScopedGitPathspecArgs(context);
|
|
862
|
-
const addResult = await execGitCommand(context.repositoryRootPath, ['add', '--all', ...pathspecArgs]);
|
|
863
|
-
if (addResult.exitCode !== 0) {
|
|
864
|
-
res.status(400).json({ error: addResult.stderr || 'git add failed' });
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
const stagedResult = await execGitCommand(context.repositoryRootPath, ['diff', '--cached', '--quiet', ...pathspecArgs]);
|
|
868
|
-
if (stagedResult.exitCode === 0) {
|
|
869
|
-
res.status(400).json({ error: '没有可提交的变更' });
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
if (stagedResult.exitCode !== 1) {
|
|
873
|
-
res.status(400).json({ error: stagedResult.stderr || 'git staged diff failed' });
|
|
874
|
-
return;
|
|
875
|
-
}
|
|
876
|
-
const authorArgs = await buildGitAuthorFallbackArgs(context.repositoryRootPath);
|
|
877
|
-
const commitResult = await execGitCommand(context.repositoryRootPath, [...authorArgs, 'commit', '-m', commitMessage, ...pathspecArgs]);
|
|
878
|
-
if (commitResult.exitCode !== 0) {
|
|
879
|
-
res.status(400).json({ error: commitResult.stderr || commitResult.stdout || 'git commit failed' });
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
res.json({
|
|
883
|
-
ok: true,
|
|
884
|
-
message: '提交完成',
|
|
885
|
-
output: commitResult.stdout,
|
|
886
|
-
workspacePath: context.workspacePath,
|
|
887
|
-
repositoryRootPath: context.repositoryRootPath,
|
|
888
|
-
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
catch (error) {
|
|
892
|
-
const err = error;
|
|
893
|
-
res.status(500).json({ error: err.message || 'git commit failed' });
|
|
894
|
-
}
|
|
895
|
-
});
|
|
896
|
-
/**
|
|
897
|
-
* 将单个文件加入暂存区
|
|
898
|
-
* POST /runtime/git/add-file
|
|
899
|
-
*/
|
|
900
|
-
gitRouter.post('/git/add-file', validateToken, async (req, res) => {
|
|
901
|
-
const { workspacePath, repositoryPath, filePath } = req.body || {};
|
|
902
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
903
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
904
|
-
return;
|
|
905
|
-
}
|
|
906
|
-
const normalizedFilePath = normalizeGitPath(String(filePath || '').trim());
|
|
907
|
-
if (!normalizedFilePath) {
|
|
908
|
-
res.status(400).json({ error: 'filePath is required' });
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
try {
|
|
912
|
-
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
913
|
-
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
914
|
-
res.status(400).json({ error: 'workspace is not inside a git repository' });
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
const addResult = await execGitCommand(context.repositoryRootPath, ['add', ...createScopedFilePathspecArgs(normalizedFilePath, context)]);
|
|
918
|
-
if (addResult.exitCode !== 0) {
|
|
919
|
-
res.status(400).json({ error: addResult.stderr || 'git add file failed' });
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
res.json({
|
|
923
|
-
ok: true,
|
|
924
|
-
filePath: normalizedFilePath,
|
|
925
|
-
workspacePath: context.workspacePath,
|
|
926
|
-
repositoryRootPath: context.repositoryRootPath,
|
|
927
|
-
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
catch (error) {
|
|
931
|
-
const err = error;
|
|
932
|
-
res.status(500).json({ error: err.message || 'git add file failed' });
|
|
933
|
-
}
|
|
934
|
-
});
|
|
935
|
-
/**
|
|
936
|
-
* 将多个文件批量加入暂存区(单条 git add 命令,避免 index.lock 竞争)
|
|
937
|
-
* POST /runtime/git/add-files
|
|
938
|
-
*/
|
|
939
|
-
gitRouter.post('/git/add-files', validateToken, async (req, res) => {
|
|
940
|
-
const { workspacePath, repositoryPath, filePaths } = req.body || {};
|
|
941
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
942
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
const rawFilePaths = Array.isArray(filePaths) ? filePaths : [];
|
|
946
|
-
const normalizedFilePaths = rawFilePaths
|
|
947
|
-
.map((fp) => normalizeGitPath(String(fp || '').trim()))
|
|
948
|
-
.filter((fp) => fp.length > 0);
|
|
949
|
-
if (normalizedFilePaths.length === 0) {
|
|
950
|
-
res.status(400).json({ error: 'filePaths is required and must be a non-empty array' });
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
try {
|
|
954
|
-
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
955
|
-
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
956
|
-
res.status(400).json({ error: 'workspace is not inside a git repository' });
|
|
957
|
-
return;
|
|
958
|
-
}
|
|
959
|
-
const pathspecArgs = normalizedFilePaths.flatMap((fp) => createScopedFilePathspecArgs(fp, context));
|
|
960
|
-
// 合并为单条 git add -- path1 path2 ...,避免并发 index.lock 冲突
|
|
961
|
-
const addArgs = ['add'];
|
|
962
|
-
const pathSpecs = [];
|
|
963
|
-
for (let i = 0; i < pathspecArgs.length; i++) {
|
|
964
|
-
// createScopedFilePathspecArgs 返回 ['--', relativePath],只收集 path 部分
|
|
965
|
-
if (pathspecArgs[i] !== '--') {
|
|
966
|
-
pathSpecs.push(pathspecArgs[i]);
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
addArgs.push('--', ...pathSpecs);
|
|
970
|
-
const addResult = await execGitCommand(context.repositoryRootPath, addArgs);
|
|
971
|
-
if (addResult.exitCode !== 0) {
|
|
972
|
-
res.status(400).json({ error: addResult.stderr || 'git add files failed' });
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
res.json({
|
|
976
|
-
ok: true,
|
|
977
|
-
filePaths: normalizedFilePaths,
|
|
978
|
-
workspacePath: context.workspacePath,
|
|
979
|
-
repositoryRootPath: context.repositoryRootPath,
|
|
980
|
-
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
catch (error) {
|
|
984
|
-
const err = error;
|
|
985
|
-
res.status(500).json({ error: err.message || 'git add files failed' });
|
|
986
|
-
}
|
|
987
|
-
});
|
|
988
|
-
/**
|
|
989
|
-
* 获取工作区提交列表
|
|
990
|
-
* POST /runtime/git/commits
|
|
991
|
-
*/
|
|
992
|
-
gitRouter.post('/git/commits', validateToken, async (req, res) => {
|
|
993
|
-
const { workspacePath, repositoryPath, limit } = req.body || {};
|
|
994
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
995
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
try {
|
|
999
|
-
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
1000
|
-
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
1001
|
-
res.json({ ok: true, commits: [], workspacePath: context.workspacePath, repositoryRootPath: null });
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
const parsedLimit = Number(limit);
|
|
1005
|
-
const safeLimit = Number.isInteger(parsedLimit) && parsedLimit > 0 && parsedLimit <= 100 ? parsedLimit : 30;
|
|
1006
|
-
const result = await execGitCommand(context.repositoryRootPath, [
|
|
1007
|
-
'log',
|
|
1008
|
-
`-${safeLimit}`,
|
|
1009
|
-
'--date=iso-strict',
|
|
1010
|
-
'--pretty=format:%H%x00%h%x00%an%x00%ad%x00%s%x00',
|
|
1011
|
-
'--',
|
|
1012
|
-
context.relativeWorkspacePath || '.',
|
|
1013
|
-
]);
|
|
1014
|
-
if (result.exitCode !== 0) {
|
|
1015
|
-
res.status(400).json({ error: result.stderr || 'git log failed' });
|
|
1016
|
-
return;
|
|
1017
|
-
}
|
|
1018
|
-
res.json({
|
|
1019
|
-
ok: true,
|
|
1020
|
-
commits: parseGitCommitLog(result.stdout),
|
|
1021
|
-
workspacePath: context.workspacePath,
|
|
1022
|
-
repositoryRootPath: context.repositoryRootPath,
|
|
1023
|
-
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
catch (error) {
|
|
1027
|
-
const err = error;
|
|
1028
|
-
res.status(500).json({ error: err.message || 'git commits failed' });
|
|
1029
|
-
}
|
|
1030
|
-
});
|
|
1031
|
-
/**
|
|
1032
|
-
* 获取指定提交相对父提交的变更概览
|
|
1033
|
-
* POST /runtime/git/commit-diff
|
|
1034
|
-
*/
|
|
1035
|
-
gitRouter.post('/git/commit-diff', validateToken, async (req, res) => {
|
|
1036
|
-
const { workspacePath, repositoryPath, commitHash } = req.body || {};
|
|
1037
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
1038
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
const normalizedCommitHash = normalizeCommitHash(commitHash);
|
|
1042
|
-
if (!normalizedCommitHash) {
|
|
1043
|
-
res.status(400).json({ error: 'commitHash is required' });
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
try {
|
|
1047
|
-
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
1048
|
-
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
1049
|
-
res.status(400).json({ error: 'workspace is not inside a git repository' });
|
|
1050
|
-
return;
|
|
1051
|
-
}
|
|
1052
|
-
const diffBaseArgs = ['diff', `${normalizedCommitHash}^!`];
|
|
1053
|
-
const nameStatusResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs([...diffBaseArgs, '--name-status'], context));
|
|
1054
|
-
if (nameStatusResult.exitCode !== 0) {
|
|
1055
|
-
res.status(400).json({ error: nameStatusResult.stderr || 'git commit diff failed' });
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
const numstatResult = await execGitCommand(context.repositoryRootPath, createScopedGitArgs([...diffBaseArgs, '--numstat'], context));
|
|
1059
|
-
if (numstatResult.exitCode !== 0) {
|
|
1060
|
-
res.status(400).json({ error: numstatResult.stderr || 'git commit diff failed' });
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
const files = parseGitDiffSummary(nameStatusResult.stdout, numstatResult.stdout);
|
|
1064
|
-
res.json({
|
|
1065
|
-
ok: true,
|
|
1066
|
-
commitHash: normalizedCommitHash,
|
|
1067
|
-
isGitRepo: context.isGitRepo,
|
|
1068
|
-
hasRepository: true,
|
|
1069
|
-
usingParentGitRepo: context.usingParentGitRepo,
|
|
1070
|
-
hasChanges: files.length > 0,
|
|
1071
|
-
files,
|
|
1072
|
-
workspacePath: context.workspacePath,
|
|
1073
|
-
repositoryRootPath: context.repositoryRootPath,
|
|
1074
|
-
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
catch (error) {
|
|
1078
|
-
const err = error;
|
|
1079
|
-
res.status(500).json({ error: err.message || 'git commit diff failed' });
|
|
1080
|
-
}
|
|
1081
|
-
});
|
|
1082
|
-
/**
|
|
1083
|
-
* 获取单个文件的详细差异
|
|
1084
|
-
* POST /runtime/git/diff-file
|
|
1085
|
-
*/
|
|
1086
|
-
gitRouter.post('/git/diff-file', validateToken, async (req, res) => {
|
|
1087
|
-
const { workspacePath, filePath, commitHash } = req.body || {};
|
|
1088
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
1089
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
if (!filePath || !String(filePath).trim()) {
|
|
1093
|
-
res.status(400).json({ error: 'filePath is required' });
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
try {
|
|
1097
|
-
// 必须使用 workspacePath 来正确计算 relativeWorkspacePath
|
|
1098
|
-
// 如果使用 repositoryPath(仓库根路径),会丢失工作目录信息,导致文件路径拼接错误
|
|
1099
|
-
const context = await resolveGitRepositoryContext(String(workspacePath).trim());
|
|
1100
|
-
const normalizedFilePath = normalizeGitPath(String(filePath).trim());
|
|
1101
|
-
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
1102
|
-
res.status(400).json({ error: 'workspace is not inside a git repository' });
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
const repositoryRelativeFilePath = resolveRepositoryRelativeFilePath(normalizedFilePath, context);
|
|
1106
|
-
const normalizedCommitHash = normalizeCommitHash(commitHash);
|
|
1107
|
-
const gitArgs = normalizedCommitHash
|
|
1108
|
-
? ['show', '--format=', '--no-ext-diff', normalizedCommitHash, '--', repositoryRelativeFilePath]
|
|
1109
|
-
: ['diff', 'HEAD', '--', repositoryRelativeFilePath];
|
|
1110
|
-
let result = await execGitCommand(context.repositoryRootPath, gitArgs);
|
|
1111
|
-
if (!normalizedCommitHash && result.exitCode !== 0) {
|
|
1112
|
-
result = await execGitCommand(context.repositoryRootPath, ['diff', '--', repositoryRelativeFilePath]);
|
|
1113
|
-
}
|
|
1114
|
-
if (result.exitCode !== 0) {
|
|
1115
|
-
res.status(400).json({ error: result.stderr || 'git diff file failed' });
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
if (!normalizedCommitHash && !result.stdout.trim()) {
|
|
1119
|
-
const stagedResult = await execGitCommand(context.repositoryRootPath, [
|
|
1120
|
-
'diff',
|
|
1121
|
-
'--cached',
|
|
1122
|
-
'--',
|
|
1123
|
-
repositoryRelativeFilePath,
|
|
1124
|
-
]);
|
|
1125
|
-
if (stagedResult.exitCode !== 0) {
|
|
1126
|
-
res.status(400).json({ error: stagedResult.stderr || 'git staged diff file failed' });
|
|
1127
|
-
return;
|
|
1128
|
-
}
|
|
1129
|
-
result = stagedResult;
|
|
1130
|
-
}
|
|
1131
|
-
// 未跟踪文件:git diff 无输出时直接读取文件内容生成新增差异
|
|
1132
|
-
if (!normalizedCommitHash && !result.stdout.trim()) {
|
|
1133
|
-
const absFilePath = path.join(context.repositoryRootPath, repositoryRelativeFilePath);
|
|
1134
|
-
try {
|
|
1135
|
-
const fileContent = await fs.readFile(absFilePath, 'utf-8');
|
|
1136
|
-
if (fileContent) {
|
|
1137
|
-
const diffLines = fileContent.split('\n').map(line => `+${line}`).join('\n');
|
|
1138
|
-
const lineCount = fileContent.split('\n').length;
|
|
1139
|
-
result = {
|
|
1140
|
-
stdout: [
|
|
1141
|
-
`diff --git a/${repositoryRelativeFilePath} b/${repositoryRelativeFilePath}`,
|
|
1142
|
-
'new file mode 100644',
|
|
1143
|
-
`--- /dev/null`,
|
|
1144
|
-
`+++ b/${repositoryRelativeFilePath}`,
|
|
1145
|
-
`@@ -0,0 +1,${lineCount} @@`,
|
|
1146
|
-
diffLines,
|
|
1147
|
-
].join('\n'),
|
|
1148
|
-
stderr: '',
|
|
1149
|
-
exitCode: 0,
|
|
1150
|
-
};
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
catch {
|
|
1154
|
-
// 文件不可读或不存在,保持空差异
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
res.json({
|
|
1158
|
-
ok: true,
|
|
1159
|
-
filePath: normalizedFilePath,
|
|
1160
|
-
repositoryRelativeFilePath,
|
|
1161
|
-
commitHash: normalizedCommitHash || null,
|
|
1162
|
-
diffContent: result.stdout,
|
|
1163
|
-
workspacePath: context.workspacePath,
|
|
1164
|
-
repositoryRootPath: context.repositoryRootPath,
|
|
1165
|
-
usingParentGitRepo: context.usingParentGitRepo
|
|
1166
|
-
});
|
|
1167
|
-
}
|
|
1168
|
-
catch (error) {
|
|
1169
|
-
const err = error;
|
|
1170
|
-
res.status(500).json({ error: err.message || 'git diff file failed' });
|
|
1171
|
-
}
|
|
1172
|
-
});
|
|
1173
|
-
/**
|
|
1174
|
-
* 提交信息生成器的 system prompt。
|
|
1175
|
-
*
|
|
1176
|
-
* 设计要点:
|
|
1177
|
-
* 1. 角色与硬约束(STRICTLY / NEVER / MUST)放在最前,降低被忽略的概率。
|
|
1178
|
-
* 2. few-shot 示例用 `<example>...</example>` 包裹,显式标明"输出只能是 message 行"。
|
|
1179
|
-
* 3. description 使用中文(与项目语言一致),首行 type(scope) 用英文。
|
|
1180
|
-
* 4. 显式禁止 AI 解释自己"在做什么",这正是截图中出现"我们被问到..."问题的根因。
|
|
1181
|
-
*/
|
|
1182
|
-
const COMMIT_MESSAGE_SYSTEM_PROMPT = [
|
|
1183
|
-
'STRICTLY act as a Git commit message generator. NEVER explain, narrate, or describe your task.',
|
|
1184
|
-
'NEVER start your reply with phrases like "我们被问到", "以下是", "好的", "Sure", "Here is", "I will", "I can", "Let me", "根据", "下面".',
|
|
1185
|
-
'NEVER include any meta-language or thinking out loud. Output ONLY the commit message text.',
|
|
1186
|
-
'',
|
|
1187
|
-
'Format (Conventional Commits, 1 line, optional body):',
|
|
1188
|
-
' <type>(<scope>): <description>',
|
|
1189
|
-
'',
|
|
1190
|
-
'Allowed types: feat, fix, refactor, test, docs, style, chore, perf, ci, build, revert.',
|
|
1191
|
-
'First line MUST be <= 72 characters. <description> MUST be in Chinese.',
|
|
1192
|
-
'Add a body (separated by a blank line) ONLY when the change needs more context.',
|
|
1193
|
-
].join('\n');
|
|
1194
|
-
/**
|
|
1195
|
-
* 用户提示词:直接给出 diff,要求"输出"提交信息,不引导模型解释任务。
|
|
1196
|
-
*/
|
|
1197
|
-
function buildCommitMessageUserPrompt(diffContent) {
|
|
1198
|
-
return [
|
|
1199
|
-
'请直接输出上述格式的提交信息,不要任何额外文字。',
|
|
1200
|
-
'以下是暂存区 diff:',
|
|
1201
|
-
'',
|
|
1202
|
-
diffContent,
|
|
1203
|
-
].join('\n');
|
|
1204
|
-
}
|
|
1205
|
-
/**
|
|
1206
|
-
* 识别 conventional commit 消息开头的 type 模式: type(scope)?: description。
|
|
1207
|
-
* 这是 sanitizeCommitMessage 的主策略 — 只要能定位到真正的 type,就直接截断,
|
|
1208
|
-
* 避免 AI 元描述前缀(如"我们被问到..."/"以下是...")污染首行。
|
|
1209
|
-
*/
|
|
1210
|
-
const COMMIT_TYPE_PATTERN = /\b(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(?:\s*\([^)]*\))?\s*:/i;
|
|
1211
|
-
/**
|
|
1212
|
-
* 宽松的 commit type 模式,用于识别不包含 scope 的 commit 消息。
|
|
1213
|
-
* 例如 "fix: 修复登录问题" 或 "feat: 添加新功能"
|
|
1214
|
-
*/
|
|
1215
|
-
const COMMIT_TYPE_LOOSE_PATTERN = /^(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)\s*:/i;
|
|
1216
|
-
/**
|
|
1217
|
-
* 判断文本首行是否看起来像有效的 commit 消息(有实际内容,不是纯元描述)。
|
|
1218
|
-
* 有效特征:包含中文/英文描述、不是纯噪声词汇、长度合理
|
|
1219
|
-
*/
|
|
1220
|
-
function looksLikeValidCommitMessage(text) {
|
|
1221
|
-
const firstLine = text.split('\n')[0]?.trim() ?? '';
|
|
1222
|
-
if (!firstLine || firstLine.length < 3) {
|
|
1223
|
-
return false;
|
|
1224
|
-
}
|
|
1225
|
-
// 检查是否包含有意义的描述(至少 2 个中文字符或 2 个英文字母序列)
|
|
1226
|
-
const chineseChars = (firstLine.match(/[\u4e00-\u9fa5]/g) || []).length;
|
|
1227
|
-
const englishWords = (firstLine.match(/[a-zA-Z]{2,}/g) || []).length;
|
|
1228
|
-
// 无中文且无英文单词时,如果首行只有符号/数字,判定无效
|
|
1229
|
-
if (chineseChars === 0 && englishWords === 0) {
|
|
1230
|
-
return false;
|
|
1231
|
-
}
|
|
1232
|
-
// 排除明显是元描述的前缀(还没被剥离干净的)
|
|
1233
|
-
const metaPrefixes = [
|
|
1234
|
-
/^以下是/,
|
|
1235
|
-
/^上面是/,
|
|
1236
|
-
/^生成的/,
|
|
1237
|
-
/^这是/,
|
|
1238
|
-
/^下面/,
|
|
1239
|
-
/^(?:Here|This|This is|That is)/i,
|
|
1240
|
-
];
|
|
1241
|
-
for (const prefix of metaPrefixes) {
|
|
1242
|
-
if (prefix.test(firstLine)) {
|
|
1243
|
-
return false;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return chineseChars >= 2 || englishWords >= 2;
|
|
1247
|
-
}
|
|
1248
|
-
/**
|
|
1249
|
-
* 已知会污染输出的"AI 元描述"前缀(pattern 列表),作为主策略失败时的兜底。
|
|
1250
|
-
* 当 AI 输出不包含可识别的 commit type(例如只输出元描述)时,按列表依次剥离。
|
|
1251
|
-
*/
|
|
1252
|
-
const COMMIT_NOISE_PREFIX_PATTERNS = [
|
|
1253
|
-
// 中文: "我们被问到..." / "我需要..." / "我将先..." / "我们先..."
|
|
1254
|
-
// 注意: 字符集排除 `.` `:` 全角 `:` 换行,但允许 `,` 和中文 `,`,
|
|
1255
|
-
// 这样 "我们被问到根据 diff 生成提交信息,好的以下是:" 这种带逗号前缀的也能被剥光
|
|
1256
|
-
/^\s*(?:我们|我)\s*(?:被\s*)?(?:问[到过]?|需要|要|将|来|先)[^.:\n]{0,60}?(?:提交信息|commit\s*message|[::,,。])/i,
|
|
1257
|
-
// 中文: "好的, ..." / "好: ..." / "好的以下是:"
|
|
1258
|
-
/^\s*(?:好的|好)\s*[,,::]/i,
|
|
1259
|
-
// 中文: "好的以下是..." / "好以下是..." — 前一条 pattern 剥离后残留
|
|
1260
|
-
/^\s*(?:好的|好)\s*(?:以下是|上面是|提供|生成|给出)[^.:\n]{0,20}?[::]?/i,
|
|
1261
|
-
// 中文: "是:" / "是:" — 前一条 pattern 剥离 "提从信息" 后残留的 "是:"
|
|
1262
|
-
/^\s*是\s*[::]/i,
|
|
1263
|
-
// 英文: "Sure, ..." / "Here is..." / "I will..." / "Let me..." / "Certainly:" / "Of course:"
|
|
1264
|
-
/^\s*(?:Sure|Here(?:'s|\s+is)|We(?:'re| are)|I\s+(?:will|can|'ll)|Let\s+me|Certainly|Of\s+course)[^.:\n]{0,40}?[,,::.]/i,
|
|
1265
|
-
// 中文: "根据您/你提供的 diff, " / "根据上述 diff" / "根据所给 diff"
|
|
1266
|
-
/^\s*根据\s*(?:您|你|您所|你所|所)?\s*(?:提供|上述|给[我您你])?\s*(?:的)?\s*diff\s*[,,::]?/i,
|
|
1267
|
-
// 中文: "以下是xxx提交信息: " / "下面是xxx提交信息," / "以下是符合 conventional commit 规范的提交信息: "
|
|
1268
|
-
/^\s*(?:以下|下面)[^,,::\n]{0,30}?提交信息\s*[,,::]?/i,
|
|
1269
|
-
// 中文: "以下是:" / "下面是:" — 不带"提交信息"的简化版,剥离前一条未命中的残留
|
|
1270
|
-
/^\s*(?:以下|下面)\s*是\s*[,,::]/i,
|
|
1271
|
-
// 中文: "生成xxx提交信息: " / "生成一条 conventional commit 提交信息,"
|
|
1272
|
-
/^\s*生成\s*(?:一条|一个|了)?\s*(?:符合|新的)?\s*(?:conventional\s*commit|git)?\s*提交信息\s*[,,::]?/i,
|
|
1273
|
-
// 开头 markdown 代码块 fence
|
|
1274
|
-
/^\s*```(?:text|markdown|bash|shell)?\s*\n?/i,
|
|
1275
|
-
];
|
|
1276
|
-
/**
|
|
1277
|
-
* 清洗 AI 返回的提交信息,去掉元描述/代码块/超长首行等噪声。
|
|
1278
|
-
*
|
|
1279
|
-
* 主流程:
|
|
1280
|
-
* 1. 主策略: 查找最早的 conventional commit type 模式(type(scope)?:),直接截取
|
|
1281
|
-
* 这一段之前的元描述全部丢弃。位置限定在 80 字符内,避免把 body 中提到的
|
|
1282
|
-
* "feat" 等字眼误当作开头。
|
|
1283
|
-
* 2. 兜底: 主策略未命中时,反复剥离已知元描述前缀 pattern(最多 5 次)
|
|
1284
|
-
* 3. 截断到首个空行,丢弃 body 中混入的元描述
|
|
1285
|
-
* 4. 去掉尾部 markdown 代码块
|
|
1286
|
-
* 5. 首行 > 100 字符视为模型仍在解释,只保留首行
|
|
1287
|
-
* 6. 最终为空时返回 "chore: update staged changes" 兜底
|
|
1288
|
-
*/
|
|
1289
|
-
export function sanitizeCommitMessage(raw) {
|
|
1290
|
-
if (!raw) {
|
|
1291
|
-
logger.info(`[Git/sanitize] 输入为空, 返回 fallback`);
|
|
1292
|
-
return 'chore: update staged changes';
|
|
1293
|
-
}
|
|
1294
|
-
let text = raw.replace(/\r\n/g, '\n').trim();
|
|
1295
|
-
const rawPreview = JSON.stringify(raw.substring(0, 120));
|
|
1296
|
-
// 1. 主策略: 定位 commit type 模式并截断
|
|
1297
|
-
const typeMatch = text.match(COMMIT_TYPE_PATTERN);
|
|
1298
|
-
if (typeMatch && typeMatch.index !== undefined && typeMatch.index <= 80) {
|
|
1299
|
-
text = text.slice(typeMatch.index);
|
|
1300
|
-
}
|
|
1301
|
-
else {
|
|
1302
|
-
// 2. 兜底: 反复剥离元描述前缀
|
|
1303
|
-
for (let i = 0; i < 5; i += 1) {
|
|
1304
|
-
const before = text;
|
|
1305
|
-
for (const pattern of COMMIT_NOISE_PREFIX_PATTERNS) {
|
|
1306
|
-
text = text.replace(pattern, '');
|
|
1307
|
-
}
|
|
1308
|
-
text = text.replace(/^[\s*\-`,,']+/, '').trim();
|
|
1309
|
-
if (text === before)
|
|
1310
|
-
break;
|
|
1311
|
-
}
|
|
1312
|
-
// 2.5 兜底强化: 剥离后检查是否还有有效内容
|
|
1313
|
-
const firstLineAfterStrip = text.split('\n')[0]?.trim() ?? '';
|
|
1314
|
-
// 条件 A: 文本中包含 commit type(严格模式),保留
|
|
1315
|
-
// 条件 B: 文本中包含 commit type(宽松模式,如 "fix: xxx" 无 scope),保留
|
|
1316
|
-
// 条件 C: 文本首行看起来像有效的 commit 消息且有实际描述内容,保留
|
|
1317
|
-
// 条件 D: 首行很长(> 100 字符)可能是模型在解释,保留首行
|
|
1318
|
-
const hasStrictType = !!text.match(COMMIT_TYPE_PATTERN);
|
|
1319
|
-
const hasLooseType = !!text.match(COMMIT_TYPE_LOOSE_PATTERN);
|
|
1320
|
-
const looksValid = looksLikeValidCommitMessage(text);
|
|
1321
|
-
const firstLineTooLong = firstLineAfterStrip.length > 100;
|
|
1322
|
-
if (!hasStrictType && !hasLooseType && !looksValid && !firstLineTooLong) {
|
|
1323
|
-
logger.info(`[Git/sanitize] 剥离后无有效内容, 返回 fallback. raw=${rawPreview}, afterStrip=${JSON.stringify(text.substring(0, 100))}`);
|
|
1324
|
-
return 'chore: update staged changes';
|
|
1325
|
-
}
|
|
1326
|
-
// 如果剥离后有有效内容但没有 strict commit type,尝试提取首行
|
|
1327
|
-
if (!hasStrictType && (hasLooseType || looksValid || firstLineTooLong)) {
|
|
1328
|
-
text = firstLineAfterStrip || text;
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
// 3. 截断到首个空行
|
|
1332
|
-
const firstBlankIndex = text.search(/\n\s*\n/);
|
|
1333
|
-
if (firstBlankIndex > 0) {
|
|
1334
|
-
text = text.slice(0, firstBlankIndex);
|
|
1335
|
-
}
|
|
1336
|
-
// 4. 去掉尾部 markdown 代码块 fence
|
|
1337
|
-
text = text.replace(/\n+\s*```\s*$/g, '').trim();
|
|
1338
|
-
if (!text) {
|
|
1339
|
-
logger.info(`[Git/sanitize] 截断后为空, 返回 fallback. raw=${rawPreview}`);
|
|
1340
|
-
return 'chore: update staged changes';
|
|
1341
|
-
}
|
|
1342
|
-
// 5. 首行超过 100 字符极可能是模型仍在解释,只保留首行
|
|
1343
|
-
const firstLine = text.split('\n')[0]?.trim() ?? '';
|
|
1344
|
-
if (firstLine.length > 100) {
|
|
1345
|
-
logger.info(`[Git/sanitize] 首行超过 100 字符, 仅保留首行. raw=${rawPreview}, firstLine=${JSON.stringify(firstLine.substring(0, 100))}`);
|
|
1346
|
-
return firstLine;
|
|
1347
|
-
}
|
|
1348
|
-
logger.info(`[Git/sanitize] 结果: ${JSON.stringify(text.substring(0, 120))} (from raw=${rawPreview})`);
|
|
1349
|
-
return text;
|
|
1350
|
-
}
|
|
1351
|
-
/**
|
|
1352
|
-
* 从 OpenAI-compatible SSE chunk 文本中提取增量文本。
|
|
1353
|
-
* 处理:空行 / 缺少 data 字段 / [DONE] 哨兵 / 非 JSON 行的容错。
|
|
1354
|
-
*/
|
|
1355
|
-
export function extractOpenAiStreamDelta(chunkText) {
|
|
1356
|
-
const lines = chunkText.split(/\r?\n/);
|
|
1357
|
-
let buffer = '';
|
|
1358
|
-
for (const rawLine of lines) {
|
|
1359
|
-
const line = rawLine.trim();
|
|
1360
|
-
if (!line.startsWith('data:'))
|
|
1361
|
-
continue;
|
|
1362
|
-
const data = line.slice(5).trim();
|
|
1363
|
-
if (!data || data === '[DONE]')
|
|
1364
|
-
continue;
|
|
1365
|
-
try {
|
|
1366
|
-
const parsed = JSON.parse(data);
|
|
1367
|
-
const delta = parsed?.choices?.[0]?.delta?.content;
|
|
1368
|
-
if (typeof delta === 'string' && delta.length > 0) {
|
|
1369
|
-
buffer += delta;
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
catch {
|
|
1373
|
-
// 忽略单行解析失败,继续读后续 chunk
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
return buffer;
|
|
1377
|
-
}
|
|
1378
|
-
/**
|
|
1379
|
-
* 调试级: 记录 AI stream 的前 N 个 chunk 的完整 JSON 结构,用于诊断
|
|
1380
|
-
* 模型返回格式与标准 OpenAI 格式不同导致 delta 无法提取的问题。
|
|
1381
|
-
*/
|
|
1382
|
-
export function debugAiStreamChunk(chunkText, index) {
|
|
1383
|
-
if (index > 5)
|
|
1384
|
-
return; // 只记前 5 个
|
|
1385
|
-
const lines = chunkText.split(/\r?\n/);
|
|
1386
|
-
for (const rawLine of lines) {
|
|
1387
|
-
const line = rawLine.trim();
|
|
1388
|
-
if (!line.startsWith('data:'))
|
|
1389
|
-
continue;
|
|
1390
|
-
const data = line.slice(5).trim();
|
|
1391
|
-
if (!data || data === '[DONE]')
|
|
1392
|
-
continue;
|
|
1393
|
-
try {
|
|
1394
|
-
const parsed = JSON.parse(data);
|
|
1395
|
-
const keys = Object.keys(parsed);
|
|
1396
|
-
const choices = parsed?.choices;
|
|
1397
|
-
const firstChoice = Array.isArray(choices) ? choices[0] : null;
|
|
1398
|
-
const deltaKeys = firstChoice?.delta ? Object.keys(firstChoice.delta) : null;
|
|
1399
|
-
const deltaContent = firstChoice?.delta?.content;
|
|
1400
|
-
logger.info(`[Git/extract] chunk#${index} structure: topKeys=${JSON.stringify(keys)}, ` +
|
|
1401
|
-
`choicesLen=${Array.isArray(choices) ? choices.length : typeof choices}, ` +
|
|
1402
|
-
`deltaKeys=${JSON.stringify(deltaKeys)}, content=${JSON.stringify(deltaContent?.substring(0, 50))}, ` +
|
|
1403
|
-
`finishReason=${JSON.stringify(firstChoice?.finish_reason)}`);
|
|
1404
|
-
}
|
|
1405
|
-
catch { /* ignore */ }
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
/**
|
|
1409
|
-
* 调用 OpenAI-compatible API(非流式)生成 Git 提交信息。
|
|
1410
|
-
*
|
|
1411
|
-
* 主流程:组装 prompt → 调 chat/completions → 清洗回复 → 返回。
|
|
1412
|
-
* 保留此接口为同步路径的兜底(测试 / 旧调用方),新场景请优先使用 `streamAiForCommitMessage`。
|
|
1413
|
-
*/
|
|
1414
|
-
export async function callAiForCommitMessage(aiBaseUrl, aiApiKey, aiModel, diffContent) {
|
|
1415
|
-
const url = `${aiBaseUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
1416
|
-
const response = await axios.post(url, {
|
|
1417
|
-
model: aiModel,
|
|
1418
|
-
stream: false,
|
|
1419
|
-
messages: [
|
|
1420
|
-
{ role: 'system', content: COMMIT_MESSAGE_SYSTEM_PROMPT },
|
|
1421
|
-
{ role: 'user', content: buildCommitMessageUserPrompt(diffContent) },
|
|
1422
|
-
],
|
|
1423
|
-
max_tokens: 200,
|
|
1424
|
-
temperature: 0.2,
|
|
1425
|
-
}, {
|
|
1426
|
-
headers: {
|
|
1427
|
-
'Content-Type': 'application/json',
|
|
1428
|
-
'Authorization': `Bearer ${aiApiKey}`,
|
|
1429
|
-
},
|
|
1430
|
-
timeout: 30000,
|
|
1431
|
-
});
|
|
1432
|
-
const data = response.data;
|
|
1433
|
-
const content = data?.choices?.[0]?.message?.content?.trim() || '';
|
|
1434
|
-
return sanitizeCommitMessage(content);
|
|
1435
|
-
}
|
|
1436
|
-
/**
|
|
1437
|
-
* 流式调用 OpenAI-compatible API 生成 Git 提交信息。
|
|
1438
|
-
*
|
|
1439
|
-
* 主流程:
|
|
1440
|
-
* 1. 发起 stream:true 的 chat/completions 请求
|
|
1441
|
-
* 2. 逐 chunk 解析 SSE,把 delta 文本回调给调用方(`onDelta`)
|
|
1442
|
-
* 3. 全部读取完成后,把累积内容交给 `sanitizeCommitMessage` 清洗
|
|
1443
|
-
* 4. 返回清洗后的最终提交信息
|
|
1444
|
-
*
|
|
1445
|
-
* 入参:
|
|
1446
|
-
* - onDelta: 每次收到增量时调用,用于把"打字机"效果推给上层
|
|
1447
|
-
* - abortSignal: 调用方可中止请求(用户取消时使用)
|
|
1448
|
-
*/
|
|
1449
|
-
export async function streamAiForCommitMessage(aiBaseUrl, aiApiKey, aiModel, diffContent, onDelta, abortSignal) {
|
|
1450
|
-
const url = `${aiBaseUrl.replace(/\/+$/, '')}/chat/completions`;
|
|
1451
|
-
const response = await axios.post(url, {
|
|
1452
|
-
model: aiModel,
|
|
1453
|
-
stream: true,
|
|
1454
|
-
messages: [
|
|
1455
|
-
{ role: 'system', content: COMMIT_MESSAGE_SYSTEM_PROMPT },
|
|
1456
|
-
{ role: 'user', content: buildCommitMessageUserPrompt(diffContent) },
|
|
1457
|
-
],
|
|
1458
|
-
max_tokens: 200,
|
|
1459
|
-
temperature: 0.2,
|
|
1460
|
-
}, {
|
|
1461
|
-
headers: {
|
|
1462
|
-
'Content-Type': 'application/json',
|
|
1463
|
-
'Authorization': `Bearer ${aiApiKey}`,
|
|
1464
|
-
'Accept': 'text/event-stream',
|
|
1465
|
-
},
|
|
1466
|
-
timeout: Number(process.env.AWS_AI_STREAM_TIMEOUT_MS) || 60000,
|
|
1467
|
-
responseType: 'stream',
|
|
1468
|
-
signal: abortSignal,
|
|
1469
|
-
});
|
|
1470
|
-
const stream = response.data;
|
|
1471
|
-
let buffer = '';
|
|
1472
|
-
let accumulated = '';
|
|
1473
|
-
let pendingData = '';
|
|
1474
|
-
let onDataChunkCount = 0;
|
|
1475
|
-
let onDataDeltaCount = 0;
|
|
1476
|
-
logger.info(`[Git/streamAi] AI 流开始: aiModel=${aiModel}`);
|
|
1477
|
-
return await new Promise((resolve, reject) => {
|
|
1478
|
-
const finish = (value) => {
|
|
1479
|
-
stream.removeAllListeners('data');
|
|
1480
|
-
stream.removeAllListeners('end');
|
|
1481
|
-
stream.removeAllListeners('error');
|
|
1482
|
-
stream.removeAllListeners('aborted');
|
|
1483
|
-
logger.info(`[Git/streamAi] AI 流结束: totalChunks=${onDataChunkCount}, totalDeltas=${onDataDeltaCount}, bufferLength=${buffer.length}, sanitized=${JSON.stringify(value).substring(0, 200)}`);
|
|
1484
|
-
resolve(value);
|
|
1485
|
-
};
|
|
1486
|
-
stream.on('data', (chunk) => {
|
|
1487
|
-
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
1488
|
-
onDataChunkCount++;
|
|
1489
|
-
pendingData += text;
|
|
1490
|
-
if (onDataChunkCount <= 3) {
|
|
1491
|
-
logger.info(`[Git/streamAi] chunk#${onDataChunkCount}: length=${text.length}, preview=${JSON.stringify(text.substring(0, 80))}`);
|
|
1492
|
-
}
|
|
1493
|
-
// 调试: 记录前 5 个 chunk 的完整 JSON 结构
|
|
1494
|
-
debugAiStreamChunk(text, onDataChunkCount);
|
|
1495
|
-
// SSE 事件以 \n\n 分隔,逐事件解析
|
|
1496
|
-
const events = pendingData.split(/\n\n/);
|
|
1497
|
-
pendingData = events.pop() ?? '';
|
|
1498
|
-
for (const eventText of events) {
|
|
1499
|
-
const delta = extractOpenAiStreamDelta(eventText);
|
|
1500
|
-
if (delta) {
|
|
1501
|
-
onDataDeltaCount++;
|
|
1502
|
-
buffer += delta;
|
|
1503
|
-
accumulated = sanitizeCommitMessage(buffer);
|
|
1504
|
-
onDelta(delta, accumulated);
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
});
|
|
1508
|
-
stream.on('end', () => {
|
|
1509
|
-
// 处理尾部可能残留的最后一个事件
|
|
1510
|
-
if (pendingData.trim()) {
|
|
1511
|
-
const delta = extractOpenAiStreamDelta(pendingData);
|
|
1512
|
-
if (delta) {
|
|
1513
|
-
logger.info(`[Git/streamAi] 尾部残留 delta: length=${delta.length}`);
|
|
1514
|
-
buffer += delta;
|
|
1515
|
-
}
|
|
1516
|
-
else {
|
|
1517
|
-
logger.info(`[Git/streamAi] 尾部残留无 delta, pendingData=${JSON.stringify(pendingData.substring(0, 100))}`);
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
finish(sanitizeCommitMessage(buffer));
|
|
1521
|
-
});
|
|
1522
|
-
stream.on('error', (err) => {
|
|
1523
|
-
logger.error(`[Git/streamAi] 流错误: ${err.message}`);
|
|
1524
|
-
reject(err);
|
|
1525
|
-
});
|
|
1526
|
-
stream.on('aborted', () => {
|
|
1527
|
-
logger.info(`[Git/streamAi] 流被中止, bufferLength=${buffer.length}`);
|
|
1528
|
-
finish(sanitizeCommitMessage(buffer));
|
|
1529
|
-
});
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
/**
|
|
1533
|
-
* 把 git diff --cached 内容聚合成单段文本,限制长度防止超 token。
|
|
1534
|
-
* 供同步和流式两个生成 commit 消息的路由共用。
|
|
1535
|
-
*
|
|
1536
|
-
* 主流程:
|
|
1537
|
-
* 1. 对每个 stagedFile 调用 git diff --cached 收集单文件 diff
|
|
1538
|
-
* 2. 若逐文件均无 diff(可能因路径规整失败),回落到全量 git diff --cached
|
|
1539
|
-
* 3. 拼接并截断(> 15000 字符追加截断标记)
|
|
1540
|
-
*
|
|
1541
|
-
* 返回: { truncatedDiff: string } 表示收集成功,空字符串表示暂存区无变更。
|
|
1542
|
-
*/
|
|
1543
|
-
async function collectStagedDiffForAi(repositoryPath, workspacePath, stagedFiles) {
|
|
1544
|
-
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
1545
|
-
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
1546
|
-
logger.warn(`[Git/collectDiff] 不在 Git 仓库中: workspacePath=${workspacePath}`);
|
|
1547
|
-
return { truncatedDiff: '', error: { status: 400, message: 'workspace is not inside a git repository' } };
|
|
1548
|
-
}
|
|
1549
|
-
const filePaths = stagedFiles
|
|
1550
|
-
.map(f => f.path)
|
|
1551
|
-
.filter(Boolean);
|
|
1552
|
-
logger.info(`[Git/collectDiff] 开始收集 ${filePaths.length} 个暂存文件差异: ${filePaths.join(', ').substring(0, 200)}`);
|
|
1553
|
-
const diffParts = [];
|
|
1554
|
-
for (const filePath of filePaths) {
|
|
1555
|
-
const repositoryRelativePath = resolveRepositoryRelativeFilePath(filePath, context);
|
|
1556
|
-
const result = await execGitCommand(context.repositoryRootPath, [
|
|
1557
|
-
'diff', '--cached', '--', repositoryRelativePath,
|
|
1558
|
-
]);
|
|
1559
|
-
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
1560
|
-
diffParts.push(`--- ${filePath} ---\n${result.stdout.trim()}`);
|
|
1561
|
-
}
|
|
1562
|
-
else {
|
|
1563
|
-
logger.info(`[Git/collectDiff] 文件无暂存差异或获取失败: ${filePath}, exitCode=${result.exitCode}, stdout.length=${result.stdout.length}`);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
if (diffParts.length === 0) {
|
|
1567
|
-
// Fallback: 尝试获取所有暂存区变更
|
|
1568
|
-
logger.info(`[Git/collectDiff] 逐文件 diff 为空, 回退到全量 diff --cached`);
|
|
1569
|
-
const result = await execGitCommand(context.repositoryRootPath, [
|
|
1570
|
-
'diff', '--cached',
|
|
1571
|
-
]);
|
|
1572
|
-
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
1573
|
-
diffParts.push(result.stdout.trim());
|
|
1574
|
-
logger.info(`[Git/collectDiff] 全量 diff 获取成功: length=${result.stdout.trim().length}`);
|
|
1575
|
-
}
|
|
1576
|
-
else {
|
|
1577
|
-
logger.info(`[Git/collectDiff] 全量 diff 也为空, exitCode=${result.exitCode}`);
|
|
1578
|
-
}
|
|
1579
|
-
}
|
|
1580
|
-
if (diffParts.length === 0) {
|
|
1581
|
-
logger.info(`[Git/collectDiff] 暂存区没有差异内容 (diffParts 为空)`);
|
|
1582
|
-
return { truncatedDiff: '' };
|
|
1583
|
-
}
|
|
1584
|
-
const diffContent = diffParts.join('\n\n');
|
|
1585
|
-
const maxDiffLength = 15000;
|
|
1586
|
-
const truncatedDiff = diffContent.length > maxDiffLength
|
|
1587
|
-
? `${diffContent.slice(0, maxDiffLength)}\n...(diff truncated due to length)`
|
|
1588
|
-
: diffContent;
|
|
1589
|
-
logger.info(`[Git/collectDiff] 收集完成: diffContent.length=${diffContent.length}, truncated=${truncatedDiff.length}`);
|
|
1590
|
-
return { truncatedDiff };
|
|
1591
|
-
}
|
|
1592
|
-
/**
|
|
1593
|
-
* 写入一条 SSE 事件到响应流,失败时吞掉(连接已关闭时不再报错)。
|
|
1594
|
-
*
|
|
1595
|
-
* SSE 要求每条事件数据实时推送到客户端,不能依赖 Node.js 内部流缓冲。
|
|
1596
|
-
* post-flush 保障:
|
|
1597
|
-
* - res.flushHeaders() 已预先发送响应头,后续 write 直接走 socket
|
|
1598
|
-
* - socket.setNoDelay(true) 已关闭 Nagle 算法,每次 write 立即经内核发出
|
|
1599
|
-
* - 额外的 res.write('') + res.flushHeaders() 兜底 flush,
|
|
1600
|
-
* 兼容部分 Node.js 版本/补丁对 _send 的缓冲行为
|
|
1601
|
-
*/
|
|
1602
|
-
function writeSseEvent(res, event, data) {
|
|
1603
|
-
try {
|
|
1604
|
-
res.write(`event: ${event}\n`);
|
|
1605
|
-
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
1606
|
-
// 不调用 res.flushHeaders(): 首次调用已在 SSE 路由开头完成 (L2062),
|
|
1607
|
-
// 后续调用为 no-op。实时性依赖 socket.setNoDelay(true) + 内核 TCP_NODELAY。
|
|
1608
|
-
}
|
|
1609
|
-
catch {
|
|
1610
|
-
// 连接已断开,忽略
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
/**
|
|
1614
|
-
* 根据暂存区文件列表和 AI 源生成提交信息(同步路径,保留作为旧客户端兜底)。
|
|
1615
|
-
*
|
|
1616
|
-
* 主干流程:
|
|
1617
|
-
* 1. 收集暂存区 diff 内容(由 collectStagedDiffForAi 完成)
|
|
1618
|
-
* 2. 调用非流式 AI 接口生成完整提交信息
|
|
1619
|
-
* 3. 返回清洗后的文本
|
|
1620
|
-
*
|
|
1621
|
-
* POST /runtime/git/generate-commit-message
|
|
1622
|
-
*/
|
|
1623
|
-
gitRouter.post('/git/generate-commit-message', validateToken, async (req, res) => {
|
|
1624
|
-
const { workspacePath, repositoryPath, stagedFiles, aiBaseUrl, aiApiKey, aiModel } = req.body || {};
|
|
1625
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
1626
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
if (!Array.isArray(stagedFiles) || stagedFiles.length === 0) {
|
|
1630
|
-
res.status(400).json({ error: 'stagedFiles is required and must be a non-empty array' });
|
|
1631
|
-
return;
|
|
1632
|
-
}
|
|
1633
|
-
if (!aiBaseUrl || !String(aiBaseUrl).trim() || !aiApiKey || !String(aiApiKey).trim() || !aiModel || !String(aiModel).trim()) {
|
|
1634
|
-
res.status(400).json({ error: 'aiBaseUrl, aiApiKey and aiModel are required' });
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
try {
|
|
1638
|
-
const { truncatedDiff, error } = await collectStagedDiffForAi(String(repositoryPath || ''), String(workspacePath), stagedFiles);
|
|
1639
|
-
if (error) {
|
|
1640
|
-
res.status(error.status).json({ error: error.message });
|
|
1641
|
-
return;
|
|
1642
|
-
}
|
|
1643
|
-
if (!truncatedDiff) {
|
|
1644
|
-
res.json({ message: '暂存区没有变更内容' });
|
|
1645
|
-
return;
|
|
1646
|
-
}
|
|
1647
|
-
const generatedMessage = await callAiForCommitMessage(aiBaseUrl, aiApiKey, aiModel, truncatedDiff);
|
|
1648
|
-
res.json({ message: generatedMessage });
|
|
1649
|
-
}
|
|
1650
|
-
catch (err) {
|
|
1651
|
-
const error = err;
|
|
1652
|
-
logger.error(`[Git] generate commit message failed: ${error.message}`);
|
|
1653
|
-
res.status(500).json({ error: error.message || 'generate commit message failed' });
|
|
1654
|
-
}
|
|
1655
|
-
});
|
|
1656
|
-
/**
|
|
1657
|
-
* 流式 SSE 版本:把 AI 生成过程以 delta 事件持续推给前端,实现打字机效果。
|
|
1658
|
-
*
|
|
1659
|
-
* 主干流程:
|
|
1660
|
-
* 1. 收集暂存区 diff(共用 collectStagedDiffForAi)
|
|
1661
|
-
* 2. 设置 SSE 响应头(res.writeHead + chunked transfer)
|
|
1662
|
-
* 3. 调 streamAiForCommitMessage,每收到一段 delta 立刻 write 一条 SSE 事件
|
|
1663
|
-
* delta 事件 data 包含 contentDelta(原始增量)与 message(经过 sanitize 的累计内容)
|
|
1664
|
-
* 4. AI 完成后写一条 done 事件,data 包含最终 message
|
|
1665
|
-
* 5. 任何异常写一条 error 事件,前端据此退出"生成中"状态
|
|
1666
|
-
*
|
|
1667
|
-
* POST /runtime/git/generate-commit-message-stream
|
|
1668
|
-
*/
|
|
1669
|
-
gitRouter.post('/git/generate-commit-message-stream', validateToken, async (req, res) => {
|
|
1670
|
-
const { workspacePath, repositoryPath, stagedFiles, aiBaseUrl, aiApiKey, aiModel } = req.body || {};
|
|
1671
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
1672
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
if (!Array.isArray(stagedFiles) || stagedFiles.length === 0) {
|
|
1676
|
-
res.status(400).json({ error: 'stagedFiles is required and must be a non-empty array' });
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
if (!aiBaseUrl || !String(aiBaseUrl).trim() || !aiApiKey || !String(aiApiKey).trim() || !aiModel || !String(aiModel).trim()) {
|
|
1680
|
-
res.status(400).json({ error: 'aiBaseUrl, aiApiKey and aiModel are required' });
|
|
1681
|
-
return;
|
|
1682
|
-
}
|
|
1683
|
-
logger.info(`[Git/SSE] 收到 generate-commit-message-stream 请求: workspacePath=${String(workspacePath || '').substring(0, 40)}, ` +
|
|
1684
|
-
`stagedFiles=${stagedFiles?.length ?? 0}, aiModel=${aiModel}, aiBaseUrl=${aiBaseUrl}`);
|
|
1685
|
-
// 设置 SSE 响应头(必须在 write 任何数据前完成)
|
|
1686
|
-
res.status(200);
|
|
1687
|
-
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
|
1688
|
-
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
1689
|
-
res.setHeader('Connection', 'keep-alive');
|
|
1690
|
-
res.setHeader('X-Accel-Buffering', 'no');
|
|
1691
|
-
// 禁用 Nagle 算法: SSE 每条数据都很小(< 1460 bytes),默认 TCP_NODELAY=false
|
|
1692
|
-
// 会导致内核延迟发送,将多个小数据包合并后再发出,造成前端 SSE 事件接收延迟甚至超时。
|
|
1693
|
-
// 设置 true 后每次 write 立即经内核发送,不等待合并,确保事件实时到达。
|
|
1694
|
-
req.socket?.setNoDelay(true);
|
|
1695
|
-
res.flushHeaders?.();
|
|
1696
|
-
// 客户端中断时主动中止上游 AI 请求
|
|
1697
|
-
let aborted = false;
|
|
1698
|
-
let abortController = null;
|
|
1699
|
-
let responseFinished = false;
|
|
1700
|
-
const sseEventCount = { delta: 0 };
|
|
1701
|
-
const requestPeer = `${req.ip}:${req.socket?.remotePort ?? '?'}`;
|
|
1702
|
-
req.on('close', () => {
|
|
1703
|
-
if (!aborted) {
|
|
1704
|
-
aborted = true;
|
|
1705
|
-
// 即使连接断开,也不主动 abort AI 请求 — 让 AI 调用自然完成,
|
|
1706
|
-
// 之后同步 API 兜底时可能命中缓存(某些 AI provider 有结果缓存)。
|
|
1707
|
-
// 但若 abortController 已在后续创建,仍可安全终止。
|
|
1708
|
-
logger.info(`[Git/SSE] 客户端断开连接: workspacePath=${String(workspacePath || '').substring(0, 40)}, deltaEventsSent=${sseEventCount.delta}, peer=${requestPeer}`);
|
|
1709
|
-
}
|
|
1710
|
-
});
|
|
1711
|
-
// 发送一条 init SSE 事件, 确保响应体已有数据输出。
|
|
1712
|
-
// 目的: 让 Java 后端确认连接活跃,
|
|
1713
|
-
// 避免 HTTP 客户端在收到 SSE 响应头后、collectDiff/AI 输出前因
|
|
1714
|
-
// 无数据到达而关闭连接。
|
|
1715
|
-
writeSseEvent(res, 'init', { status: 'generating' });
|
|
1716
|
-
try {
|
|
1717
|
-
const { truncatedDiff, error } = await collectStagedDiffForAi(String(repositoryPath || ''), String(workspacePath), stagedFiles);
|
|
1718
|
-
if (error) {
|
|
1719
|
-
logger.warn(`[Git/SSE] collectStagedDiffForAi 返回错误: ${error.message}`);
|
|
1720
|
-
writeSseEvent(res, 'error', { message: error.message });
|
|
1721
|
-
res.end();
|
|
1722
|
-
return;
|
|
1723
|
-
}
|
|
1724
|
-
if (!truncatedDiff) {
|
|
1725
|
-
logger.info(`[Git/SSE] 暂存区没有变更内容, 直接返回 done`);
|
|
1726
|
-
writeSseEvent(res, 'delta', { contentDelta: '', message: '暂存区没有变更内容' });
|
|
1727
|
-
writeSseEvent(res, 'done', { message: '暂存区没有变更内容' });
|
|
1728
|
-
res.end();
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
logger.info(`[Git/SSE] 收集到 diff: truncatedDiff.length=${truncatedDiff.length}`);
|
|
1732
|
-
if (aborted) {
|
|
1733
|
-
// init 事件已在 collectDiff 前发出, 但客户端在此期间断开了连接。
|
|
1734
|
-
// 不跳过早退: 因为 writeSseEvent 的 try/catch 会吞掉已断开连接的写入错误,
|
|
1735
|
-
// 继续执行 streamAiForCommitMessage 不会造成额外负面影响。
|
|
1736
|
-
// 让 AI 调用完成并尝试发送 done 事件, 这样如果客户端重连或中间代理恢复,
|
|
1737
|
-
// 仍有可能收到完整的提交信息。
|
|
1738
|
-
logger.info(`[Git/SSE] 客户端在 collectDiff 期间断开, 但仍尝试 AI 调用`);
|
|
1739
|
-
}
|
|
1740
|
-
logger.info(`[Git/SSE] 开始调用 AI streamAiForCommitMessage: aiModel=${aiModel}`);
|
|
1741
|
-
// 在 AI 调用前才创建 AbortController, 避免 req.on('close') 在 collectDiff 期间
|
|
1742
|
-
// 提前 abort 尚未开始的 AI 请求
|
|
1743
|
-
abortController = new AbortController();
|
|
1744
|
-
let lastEmitted = '';
|
|
1745
|
-
const finalMessage = await streamAiForCommitMessage(aiBaseUrl, aiApiKey, aiModel, truncatedDiff, (delta, accumulated) => {
|
|
1746
|
-
sseEventCount.delta++;
|
|
1747
|
-
// 仅在 sanitize 后的内容相对上次发送有变化时再推送,避免重复渲染
|
|
1748
|
-
if (accumulated !== lastEmitted) {
|
|
1749
|
-
lastEmitted = accumulated;
|
|
1750
|
-
writeSseEvent(res, 'delta', { contentDelta: delta, message: accumulated });
|
|
1751
|
-
}
|
|
1752
|
-
else {
|
|
1753
|
-
writeSseEvent(res, 'delta', { contentDelta: delta, message: accumulated });
|
|
1754
|
-
}
|
|
1755
|
-
}, abortController.signal);
|
|
1756
|
-
logger.info(`[Git/SSE] AI streamAiForCommitMessage 完成: finalMessage=${JSON.stringify(finalMessage).substring(0, 200)}, deltaEventsSent=${sseEventCount.delta}`);
|
|
1757
|
-
if (!aborted) {
|
|
1758
|
-
writeSseEvent(res, 'done', { message: finalMessage });
|
|
1759
|
-
}
|
|
1760
|
-
res.end();
|
|
1761
|
-
}
|
|
1762
|
-
catch (err) {
|
|
1763
|
-
const error = err;
|
|
1764
|
-
logger.error(`[Git/SSE] stream commit message failed: ${error.message}${error.stack ? '\n' + error.stack : ''}`);
|
|
1765
|
-
if (!aborted) {
|
|
1766
|
-
writeSseEvent(res, 'error', { message: error.message || 'generate commit message failed' });
|
|
1767
|
-
}
|
|
1768
|
-
res.end();
|
|
1769
|
-
}
|
|
1770
|
-
});
|
|
1771
|
-
/**
|
|
1772
|
-
* 将已暂存的文件撤销暂存(unstage)
|
|
1773
|
-
* POST /runtime/git/unstage-files
|
|
1774
|
-
*/
|
|
1775
|
-
gitRouter.post('/git/unstage-files', validateToken, async (req, res) => {
|
|
1776
|
-
const { workspacePath, repositoryPath, filePaths } = req.body || {};
|
|
1777
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
1778
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
1779
|
-
return;
|
|
1780
|
-
}
|
|
1781
|
-
const rawFilePaths = Array.isArray(filePaths) ? filePaths : [];
|
|
1782
|
-
const normalizedFilePaths = rawFilePaths
|
|
1783
|
-
.map((fp) => normalizeGitPath(String(fp || '').trim()))
|
|
1784
|
-
.filter((fp) => fp.length > 0);
|
|
1785
|
-
if (normalizedFilePaths.length === 0) {
|
|
1786
|
-
res.status(400).json({ error: 'filePaths is required and must be a non-empty array' });
|
|
1787
|
-
return;
|
|
1788
|
-
}
|
|
1789
|
-
try {
|
|
1790
|
-
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
1791
|
-
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
1792
|
-
res.status(400).json({ error: 'workspace is not inside a git repository' });
|
|
1793
|
-
return;
|
|
1794
|
-
}
|
|
1795
|
-
// 使用 git restore --staged 撤销暂存,兼容 Git 2.23+
|
|
1796
|
-
let lastError = '';
|
|
1797
|
-
for (const filePath of normalizedFilePaths) {
|
|
1798
|
-
const pathspecArgs = createScopedFilePathspecArgs(filePath, context);
|
|
1799
|
-
const result = await execGitCommand(context.repositoryRootPath, ['restore', '--staged', ...pathspecArgs]);
|
|
1800
|
-
if (result.exitCode !== 0) {
|
|
1801
|
-
lastError = result.stderr || `git restore --staged failed for ${filePath}`;
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
if (lastError) {
|
|
1805
|
-
res.status(400).json({ error: lastError });
|
|
1806
|
-
return;
|
|
1807
|
-
}
|
|
1808
|
-
res.json({
|
|
1809
|
-
ok: true,
|
|
1810
|
-
filePaths: normalizedFilePaths,
|
|
1811
|
-
workspacePath: context.workspacePath,
|
|
1812
|
-
repositoryRootPath: context.repositoryRootPath,
|
|
1813
|
-
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
1814
|
-
});
|
|
1815
|
-
}
|
|
1816
|
-
catch (error) {
|
|
1817
|
-
const err = error;
|
|
1818
|
-
res.status(500).json({ error: err.message || 'git unstage files failed' });
|
|
1819
|
-
}
|
|
1820
|
-
});
|
|
1821
|
-
/**
|
|
1822
|
-
* 丢弃未暂存文件的修改(还原到 HEAD),慎用,不可恢复。
|
|
1823
|
-
* POST /runtime/git/restore-files
|
|
1824
|
-
*/
|
|
1825
|
-
gitRouter.post('/git/restore-files', validateToken, async (req, res) => {
|
|
1826
|
-
const { workspacePath, repositoryPath, filePaths } = req.body || {};
|
|
1827
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
1828
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1831
|
-
const rawFilePaths = Array.isArray(filePaths) ? filePaths : [];
|
|
1832
|
-
const normalizedFilePaths = rawFilePaths
|
|
1833
|
-
.map((fp) => normalizeGitPath(String(fp || '').trim()))
|
|
1834
|
-
.filter((fp) => fp.length > 0);
|
|
1835
|
-
if (normalizedFilePaths.length === 0) {
|
|
1836
|
-
res.status(400).json({ error: 'filePaths is required and must be a non-empty array' });
|
|
1837
|
-
return;
|
|
1838
|
-
}
|
|
1839
|
-
try {
|
|
1840
|
-
const context = await resolveGitRepositoryContext(String(repositoryPath || workspacePath).trim());
|
|
1841
|
-
if (!context.hasRepository || !context.repositoryRootPath) {
|
|
1842
|
-
res.status(400).json({ error: 'workspace is not inside a git repository' });
|
|
1843
|
-
return;
|
|
1844
|
-
}
|
|
1845
|
-
// git restore <file> 丢弃工作区未暂存修改
|
|
1846
|
-
let lastError = '';
|
|
1847
|
-
for (const filePath of normalizedFilePaths) {
|
|
1848
|
-
const pathspecArgs = createScopedFilePathspecArgs(filePath, context);
|
|
1849
|
-
const result = await execGitCommand(context.repositoryRootPath, ['restore', ...pathspecArgs]);
|
|
1850
|
-
if (result.exitCode !== 0) {
|
|
1851
|
-
lastError = result.stderr || `git restore failed for ${filePath}`;
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
if (lastError) {
|
|
1855
|
-
res.status(400).json({ error: lastError });
|
|
1856
|
-
return;
|
|
1857
|
-
}
|
|
1858
|
-
res.json({
|
|
1859
|
-
ok: true,
|
|
1860
|
-
filePaths: normalizedFilePaths,
|
|
1861
|
-
workspacePath: context.workspacePath,
|
|
1862
|
-
repositoryRootPath: context.repositoryRootPath,
|
|
1863
|
-
relativeWorkspacePath: context.relativeWorkspacePath,
|
|
1864
|
-
});
|
|
1865
|
-
}
|
|
1866
|
-
catch (error) {
|
|
1867
|
-
const err = error;
|
|
1868
|
-
res.status(500).json({ error: err.message || 'git restore files failed' });
|
|
1869
|
-
}
|
|
1870
|
-
});
|
|
1871
|
-
/**
|
|
1872
|
-
* 获取暂存区文件差异内容(纯 diff 文本,不调用 AI)。
|
|
1873
|
-
* 供 Java 后端直接调用 AI 时使用,避免 AI API 密钥经由 bridge 转发。
|
|
1874
|
-
*
|
|
1875
|
-
* POST /runtime/git/diff-content
|
|
1876
|
-
*/
|
|
1877
|
-
gitRouter.post('/git/diff-content', validateToken, async (req, res) => {
|
|
1878
|
-
const { workspacePath, repositoryPath, stagedFiles } = req.body || {};
|
|
1879
|
-
if (!workspacePath || !String(workspacePath).trim()) {
|
|
1880
|
-
res.status(400).json({ error: 'workspacePath is required' });
|
|
1881
|
-
return;
|
|
1882
|
-
}
|
|
1883
|
-
if (!Array.isArray(stagedFiles) || stagedFiles.length === 0) {
|
|
1884
|
-
res.status(400).json({ error: 'stagedFiles is required and must be a non-empty array' });
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
|
-
try {
|
|
1888
|
-
const { truncatedDiff, error } = await collectStagedDiffForAi(String(repositoryPath || ''), String(workspacePath), stagedFiles);
|
|
1889
|
-
if (error) {
|
|
1890
|
-
res.status(error.status).json({ error: error.message });
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1893
|
-
res.json({ ok: true, diff: truncatedDiff || '' });
|
|
1894
|
-
}
|
|
1895
|
-
catch (err) {
|
|
1896
|
-
const error = err;
|
|
1897
|
-
logger.error(`[Git/diff-content] 获取暂存区差异失败: ${error.message}`);
|
|
1898
|
-
res.status(500).json({ error: error.message || 'get diff content failed' });
|
|
1899
|
-
}
|
|
1900
|
-
});
|
|
1
|
+
import{spawn as V}from"node:child_process";import{promises as A}from"node:fs";import k from"node:path";import L from"axios";import{Router as X}from"express";import{validateToken as y}from"../middleware/auth.js";import{logger as h}from"../utils/logger.js";const P=X(),Z=1e4;function O(){const i=Number(process.env.AWS_RUNTIME_GIT_COMMAND_TIMEOUT_MS);return Number.isFinite(i)&&i>=1e3?i:Z}async function Q(i){let t;try{t=await A.stat(i)}catch(e){const a=e;throw a.code==="ENOENT"?new Error(`workspace path does not exist: ${i}`):new Error(`workspace path is not accessible: ${i}${a.message?` (${a.message})`:""}`)}if(!t.isDirectory())throw new Error(`workspace path is not a directory: ${i}`)}const tt=/^[0-9a-f]{40}$/i;function et(i,t,e,a){return i==="added"||i==="deleted"||i==="renamed"?!0:a||t>0||e>0}function rt(i){const t=String(i||"").toLowerCase();return t.includes("you do not have the initial commit yet")||t.includes("bad revision")&&t.includes("head")||t.includes("ambiguous argument")&&t.includes("head")}function st(){return"\u5F53\u524D Git \u4ED3\u5E93\u8FD8\u6CA1\u6709\u521D\u59CB\u63D0\u4EA4\uFF0C\u65E0\u6CD5\u521B\u5EFA\u5FEB\u7167\u3002\u8BF7\u5148\u63D0\u4EA4\u4E00\u6B21\u521D\u59CB\u63D0\u4EA4\u540E\u518D\u521B\u5EFA\u5FEB\u7167\u3002"}function ot(i){const t=String(i||"").toLowerCase();return t.includes("no local changes to save")||t.includes("no changes to save")||t.includes("\u6CA1\u6709\u672C\u5730\u53D8\u66F4\u9700\u8981\u4FDD\u5B58")}function it(i){const t=String(i||"").trim();return tt.test(t)?`refs/agentswork/stash-snapshots/${t.toLowerCase()}`:null}function W(i){const t=String(i||"").trim();if(!t)return null;const e=t.split("\0");if(e.length>=3){const s=e[0]?.trim(),o=e[1]?.trim(),l=e[2]?.trim()||o,c=e[3]?.trim()||null;if(!s||!o)return null;const u=I(l||"");return{ref:s,stashName:o,message:u.message,branch:u.branch,createdAt:c}}const a=t.match(/^(stash@\{\d+\})(?::\s*)?(.*)$/);if(!a)return null;const r=a[1],n=I(a[2]?.trim()||r);return{ref:r,stashName:r,message:n.message,branch:n.branch,createdAt:null}}function I(i){const t=String(i||"").trim(),e=t.match(/^(?:On|WIP on)\s+([^:]+):\s*(.*)$/);return e?{branch:e[1].trim(),message:e[2].trim()||t}:{branch:"",message:t}}function z(i){const t=String(i||"").split(`
|
|
2
|
+
`).find(a=>a.trim());if(!t)return null;const e=W(t);return e?{ref:e.ref,stashName:e.stashName||e.ref,message:e.message||e.ref}:null}async function p(i,t){return new Promise(e=>{let a=!1,r=null;const n=c=>{a||(a=!0,r&&clearTimeout(r),e(c))},s=V("git",t,{cwd:i,stdio:["ignore","pipe","pipe"]});let o="",l="";s.stdout?.on("data",c=>{o+=String(c||"")}),s.stderr?.on("data",c=>{l+=String(c||"")}),r=setTimeout(()=>{const c=O();h.warn(`[runtime-bridge] git command timed out after ${c}ms: git ${t.join(" ")} (cwd=${i})`),s.kill("SIGKILL"),n({stdout:o,stderr:l||`git command timed out after ${c}ms`,exitCode:124})},O()),s.on("error",c=>{n({stdout:"",stderr:c.message,exitCode:1})}),s.on("close",c=>{n({stdout:o,stderr:l,exitCode:c??1})})})}async function _(i,t){const e=it(t);if(!e)return null;const a=await p(i,["update-ref",e,t]);return a.exitCode!==0?(h.warn(`[Git] \u6301\u4E45\u5316 stash \u5FEB\u7167 ref \u5931\u8D25: ref=${e}, error=${a.stderr||a.stdout}`),null):e}function S(i){return String(i||"").replace(/\\/g,"/").replace(/^\.\//,"").trim()}async function at(i){const t=k.join(i,".git");try{const e=await A.stat(t);return e.isDirectory()||e.isFile()}catch{return!1}}async function R(i){const t=k.resolve(String(i).trim());if(await Q(t),await at(t))return{workspacePath:t,repositoryRootPath:t,hasRepository:!0,isGitRepo:!0,usingParentGitRepo:!1,relativeWorkspacePath:""};const a=await p(t,["rev-parse","--show-toplevel"]);if(a.exitCode!==0)return{workspacePath:t,repositoryRootPath:null,hasRepository:!1,isGitRepo:!1,usingParentGitRepo:!1,relativeWorkspacePath:""};const r=k.resolve(a.stdout.trim()),n=S(k.relative(r,t));return{workspacePath:t,repositoryRootPath:r,hasRepository:!0,isGitRepo:!1,usingParentGitRepo:r!==t,relativeWorkspacePath:n}}function C(i,t){if(!t.relativeWorkspacePath)return[...i];const e=!i.includes("status");return[...i,...e?[`--relative=${t.relativeWorkspacePath}`]:[],"--",t.relativeWorkspacePath]}function N(i,t){const e=S(i);return t.relativeWorkspacePath?S(k.posix.join(t.relativeWorkspacePath,e)):e}function nt(i){const t=String(i||"").trim().toUpperCase();return t.startsWith("A")?"added":t.startsWith("D")?"deleted":t.startsWith("R")?"renamed":"modified"}function T(i){const t=S(i);if(!t.includes("=>"))return t;const e=t.match(/^(.*)\{([^{}]+)=>([^{}]+)\}(.*)$/);if(e){const r=e[1],n=e[3],s=e[4];return S(`${r}${n}${s}`)}const a=t.lastIndexOf("=>");return a>=0?S(t.slice(a+2)):t}function ct(i){const t=new Set,e=String(i||"").split(`
|
|
3
|
+
`).filter(a=>a.trim());for(const a of e){const r=a[0]||" ";if(r===" "||r==="?")continue;const n=a.slice(3).trim(),s=n.split(" -> "),o=T(s[s.length-1]||n);o&&t.add(o)}return t}function D(i,t,e=new Set,a=!1){const r=new Map,n=i.split(`
|
|
4
|
+
`).filter(l=>l.trim());for(const l of n){const c=l.split(" ");if(c.length<2)continue;const u=nt(c[0]),f=u==="renamed"&&c.length>=3?2:1,d=T(c[f]);d&&r.set(d,u)}const s=[],o=t.split(`
|
|
5
|
+
`).filter(l=>l.trim());for(const l of o){const c=l.split(" ");if(c.length<3)continue;const u=c[0]==="-"||c[1]==="-",f=c[0]==="-"?0:parseInt(c[0],10)||0,d=c[1]==="-"?0:parseInt(c[1],10)||0,m=T(c.slice(2).join(" ").trim()),g=r.get(m)||"modified";!m||!et(g,f,d,u)||s.push({path:m,status:g,additions:f,deletions:d,staged:a||e.has(m)})}return s}function lt(i){const t=[],e=String(i||"").split(`
|
|
6
|
+
`).filter(a=>a.trim());for(const a of e)if(a.startsWith("?? ")){const r=a.slice(3).trim();r&&t.push(r.replace(/\/$/,""))}return t}async function ft(i,t,e,a=10){const r=[];async function n(s,o){if(o>a)return;const l=k.join(i,s);let c;try{c=await A.readdir(l,{withFileTypes:!0})}catch{return}for(const u of c){if(u.name===".git"||u.name==="node_modules")continue;const f=s?`${s}/${u.name}`:u.name;if(u.isDirectory())await n(f,o+1);else if(u.isFile()){const d=S(f);d&&!e.has(d)&&r.push({path:d,status:"added",additions:0,deletions:0,staged:!1})}}}for(const s of t){const o=S(s);if(!o)continue;const l=k.join(i,s);let c;try{c=await A.stat(l)}catch{continue}c.isFile()?e.has(o)||r.push({path:o,status:"added",additions:0,deletions:0,staged:!1}):c.isDirectory()&&await n(o,0)}return r}function ut(i,t){const e=new Map;for(const a of i)e.set(a.path,{...a});for(const a of t){const r=e.get(a.path);if(!r){e.set(a.path,{...a,staged:!0});continue}e.set(a.path,{...r,status:r.status==="modified"?a.status:r.status,additions:r.additions+a.additions,deletions:r.deletions+a.deletions,staged:!0})}return[...e.values()]}function q(i){return String(i||"").trim()}function dt(i){return String(i||"").trim()}function ht(i){return["--",i.relativeWorkspacePath||"."]}function G(i,t){return["--",N(i,t)]}function pt(i){const t=i.split("\0"),e=[];for(let a=0;a+4<t.length;a+=5){const r=t[a]?.trim();r&&e.push({hash:r,shortHash:t[a+1]?.trim()||r.slice(0,8),authorName:t[a+2]?.trim()||"unknown",authorDate:t[a+3]?.trim()||"",subject:t[a+4]?.trim()||r})}return e}P.post("/git/check",y,async(i,t)=>{const{workspacePath:e}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const a=await R(String(e).trim());t.json({ok:!0,isGitRepo:a.isGitRepo,hasRepository:a.hasRepository,usingParentGitRepo:a.usingParentGitRepo,workspacePath:a.workspacePath,repositoryRootPath:a.repositoryRootPath,relativeWorkspacePath:a.relativeWorkspacePath});return}catch(a){const r=a;t.status(500).json({error:r.message||"check git repo failed"})}}),P.post("/git/stash/create",y,async(i,t)=>{const{workspacePath:e,message:a}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const r=k.resolve(String(e).trim()),n=`\u5FEB\u7167\uFF08${new Date().toLocaleString("zh-CN",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"})}\uFF09`,s=a&&String(a).trim()?`\u5FEB\u7167: ${String(a).trim()}`:n,o=await p(r,["stash","push","-m",s]);if(ot(`${o.stdout}
|
|
7
|
+
${o.stderr}`)){const d=await p(r,["stash","list","-n","1","--format=%H%x00%gd%x00%gs%x00%cr"]),m=d.exitCode===0?z(d.stdout):null,g=m?await _(r,m.ref):null;t.json({ok:!0,stashRef:g??m?.ref??null,stashName:m?.stashName??null,message:m?`\u6CA1\u6709\u672C\u5730\u53D8\u66F4\u9700\u8981\u4FDD\u5B58\uFF0C\u5DF2\u590D\u7528\u6700\u8FD1\u5FEB\u7167 ${m.stashName}`:"\u6CA1\u6709\u672C\u5730\u53D8\u66F4\u9700\u8981\u4FDD\u5B58",noChanges:!0,reusedLatestStash:!!m,workspacePath:r});return}if(o.exitCode!==0){if(rt(`${o.stdout}
|
|
8
|
+
${o.stderr}`)){t.status(400).json({error:st()});return}t.status(400).json({error:o.stderr||"git stash push failed"});return}const l=await p(r,["stash","list","-n","1","--format=%H%x00%gd%x00%gs%x00%cr"]),c=l.exitCode===0?z(l.stdout):null,f=(c?await _(r,c.ref):null)??c?.ref??"stash@{0}";t.json({ok:!0,stashRef:f,stashName:c?.stashName??null,message:s,workspacePath:r})}catch(r){const n=r;t.status(500).json({error:n.message||"create stash failed"})}}),P.post("/git/stash/list",y,async(i,t)=>{const{workspacePath:e,limit:a=20}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const r=k.resolve(String(e).trim()),n=Math.min(Math.max(1,Number(a)||20),100),s=await p(r,["stash","list","-n",String(n),"--format=%H%x00%gd%x00%gs%x00%cr"]);if(s.exitCode!==0){t.status(400).json({error:s.stderr||"git stash list failed"});return}const o=s.stdout.split(`
|
|
9
|
+
`).map(W).filter(l=>l!==null);t.json({ok:!0,stashes:o,count:o.length,workspacePath:r})}catch(r){const n=r;t.status(500).json({error:n.message||"list stash failed"})}});async function H(i,t,e){const{workspacePath:a,ref:r}=i.body||{};if(!a||!String(a).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const n=k.resolve(String(a).trim()),s=r&&String(r).trim()?String(r).trim():"stash@{0}";await p(n,["checkout","--","."]);const o=await p(n,["stash",e,s]);if(o.exitCode!==0){if(o.stdout.includes("CONFLICT")||o.stderr.includes("CONFLICT")){t.json({ok:!1,hasConflict:!0,message:"\u6062\u590D\u65F6\u53D1\u751F\u51B2\u7A81\uFF0C\u8BF7\u624B\u52A8\u89E3\u51B3",details:o.stdout||o.stderr,workspacePath:n});return}if(o.stderr.includes("is not a valid reference")){t.status(404).json({error:`\u5FEB\u7167 ${s} \u4E0D\u5B58\u5728`});return}t.status(400).json({error:o.stderr||`git stash ${e} failed`});return}t.json({ok:!0,stashRef:s,message:e==="apply"?`\u5DF2\u5E94\u7528 ${s}`:`\u5DF2\u6062\u590D ${s}`,preservedStash:e==="apply",workspacePath:n})}catch(n){const s=n;t.status(500).json({error:s.message||`${e} stash failed`})}}P.post("/git/stash/apply",y,async(i,t)=>{await H(i,t,"apply")}),P.post("/git/stash/pop",y,async(i,t)=>{await H(i,t,"pop")}),P.post("/git/stash/drop",y,async(i,t)=>{const{workspacePath:e,ref:a}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const r=k.resolve(String(e).trim()),n=a&&String(a).trim()?String(a).trim():"stash@{0}",s=await p(r,["stash","drop",n]);if(s.exitCode!==0){if(s.stderr.includes("is not a valid reference")){t.status(404).json({error:`\u5FEB\u7167 ${n} \u4E0D\u5B58\u5728`});return}t.status(400).json({error:s.stderr||"git stash drop failed"});return}t.json({ok:!0,stashRef:n,message:`\u5DF2\u5220\u9664 ${n}`,workspacePath:r})}catch(r){const n=r;t.status(500).json({error:n.message||"drop stash failed"})}}),P.post("/git/diff",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const r=await R(String(a||e).trim());if(!r.hasRepository||!r.repositoryRootPath){t.json({ok:!0,isGitRepo:r.isGitRepo,hasRepository:!1,usingParentGitRepo:!1,hasChanges:!1,files:[],workspacePath:r.workspacePath,repositoryRootPath:null,relativeWorkspacePath:""});return}const n=await p(r.repositoryRootPath,C(["diff","--name-status"],r));if(n.exitCode!==0){t.status(400).json({error:n.stderr||"git diff failed"});return}const s=await p(r.repositoryRootPath,C(["diff","--numstat"],r));if(s.exitCode!==0){t.status(400).json({error:s.stderr||"git diff failed"});return}const o=await p(r.repositoryRootPath,C(["diff","--cached","--name-status"],r));if(o.exitCode!==0){t.status(400).json({error:o.stderr||"git staged diff failed"});return}const l=await p(r.repositoryRootPath,C(["diff","--cached","--numstat"],r));if(l.exitCode!==0){t.status(400).json({error:l.stderr||"git staged diff failed"});return}const c=await p(r.repositoryRootPath,C(["status","--porcelain=v1"],r));if(c.exitCode!==0){t.status(400).json({error:c.stderr||"git status failed"});return}const u=ct(c.stdout),f=ut(D(n.stdout,s.stdout,u),D(o.stdout,l.stdout,u,!0)),d=lt(c.stdout);if(d.length>0){const m=new Set(f.map(x=>x.path)),g=await ft(r.repositoryRootPath,d,m);f.push(...g)}t.json({ok:!0,isGitRepo:r.isGitRepo,hasRepository:!0,usingParentGitRepo:r.usingParentGitRepo,hasChanges:f.length>0,files:f,workspacePath:r.workspacePath,repositoryRootPath:r.repositoryRootPath,relativeWorkspacePath:r.relativeWorkspacePath})}catch(r){const n=r;t.status(500).json({error:n.message||"git diff failed"})}});async function mt(i){const t=[],e=await p(i,["config","user.name"]);(e.exitCode!==0||!e.stdout.trim())&&t.push("-c","user.name=AgentsWork");const a=await p(i,["config","user.email"]);return(a.exitCode!==0||!a.stdout.trim())&&t.push("-c","user.email=agent@agentswork.com"),t}P.post("/git/commit",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a,message:r}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const n=dt(r);if(!n){t.status(400).json({error:"commit message is required"});return}try{const s=await R(String(a||e).trim());if(!s.hasRepository||!s.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const o=ht(s),l=await p(s.repositoryRootPath,["add","--all",...o]);if(l.exitCode!==0){t.status(400).json({error:l.stderr||"git add failed"});return}const c=await p(s.repositoryRootPath,["diff","--cached","--quiet",...o]);if(c.exitCode===0){t.status(400).json({error:"\u6CA1\u6709\u53EF\u63D0\u4EA4\u7684\u53D8\u66F4"});return}if(c.exitCode!==1){t.status(400).json({error:c.stderr||"git staged diff failed"});return}const u=await mt(s.repositoryRootPath),f=await p(s.repositoryRootPath,[...u,"commit","-m",n,...o]);if(f.exitCode!==0){t.status(400).json({error:f.stderr||f.stdout||"git commit failed"});return}t.json({ok:!0,message:"\u63D0\u4EA4\u5B8C\u6210",output:f.stdout,workspacePath:s.workspacePath,repositoryRootPath:s.repositoryRootPath,relativeWorkspacePath:s.relativeWorkspacePath})}catch(s){const o=s;t.status(500).json({error:o.message||"git commit failed"})}}),P.post("/git/add-file",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a,filePath:r}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const n=S(String(r||"").trim());if(!n){t.status(400).json({error:"filePath is required"});return}try{const s=await R(String(a||e).trim());if(!s.hasRepository||!s.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const o=await p(s.repositoryRootPath,["add",...G(n,s)]);if(o.exitCode!==0){t.status(400).json({error:o.stderr||"git add file failed"});return}t.json({ok:!0,filePath:n,workspacePath:s.workspacePath,repositoryRootPath:s.repositoryRootPath,relativeWorkspacePath:s.relativeWorkspacePath})}catch(s){const o=s;t.status(500).json({error:o.message||"git add file failed"})}}),P.post("/git/add-files",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a,filePaths:r}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const s=(Array.isArray(r)?r:[]).map(o=>S(String(o||"").trim())).filter(o=>o.length>0);if(s.length===0){t.status(400).json({error:"filePaths is required and must be a non-empty array"});return}try{const o=await R(String(a||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const l=s.flatMap(d=>G(d,o)),c=["add"],u=[];for(let d=0;d<l.length;d++)l[d]!=="--"&&u.push(l[d]);c.push("--",...u);const f=await p(o.repositoryRootPath,c);if(f.exitCode!==0){t.status(400).json({error:f.stderr||"git add files failed"});return}t.json({ok:!0,filePaths:s,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const l=o;t.status(500).json({error:l.message||"git add files failed"})}}),P.post("/git/commits",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a,limit:r}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}try{const n=await R(String(a||e).trim());if(!n.hasRepository||!n.repositoryRootPath){t.json({ok:!0,commits:[],workspacePath:n.workspacePath,repositoryRootPath:null});return}const s=Number(r),o=Number.isInteger(s)&&s>0&&s<=100?s:30,l=await p(n.repositoryRootPath,["log",`-${o}`,"--date=iso-strict","--pretty=format:%H%x00%h%x00%an%x00%ad%x00%s%x00","--",n.relativeWorkspacePath||"."]);if(l.exitCode!==0){t.status(400).json({error:l.stderr||"git log failed"});return}t.json({ok:!0,commits:pt(l.stdout),workspacePath:n.workspacePath,repositoryRootPath:n.repositoryRootPath,relativeWorkspacePath:n.relativeWorkspacePath})}catch(n){const s=n;t.status(500).json({error:s.message||"git commits failed"})}}),P.post("/git/commit-diff",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a,commitHash:r}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const n=q(r);if(!n){t.status(400).json({error:"commitHash is required"});return}try{const s=await R(String(a||e).trim());if(!s.hasRepository||!s.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const o=["diff",`${n}^!`],l=await p(s.repositoryRootPath,C([...o,"--name-status"],s));if(l.exitCode!==0){t.status(400).json({error:l.stderr||"git commit diff failed"});return}const c=await p(s.repositoryRootPath,C([...o,"--numstat"],s));if(c.exitCode!==0){t.status(400).json({error:c.stderr||"git commit diff failed"});return}const u=D(l.stdout,c.stdout);t.json({ok:!0,commitHash:n,isGitRepo:s.isGitRepo,hasRepository:!0,usingParentGitRepo:s.usingParentGitRepo,hasChanges:u.length>0,files:u,workspacePath:s.workspacePath,repositoryRootPath:s.repositoryRootPath,relativeWorkspacePath:s.relativeWorkspacePath})}catch(s){const o=s;t.status(500).json({error:o.message||"git commit diff failed"})}}),P.post("/git/diff-file",y,async(i,t)=>{const{workspacePath:e,filePath:a,commitHash:r}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!a||!String(a).trim()){t.status(400).json({error:"filePath is required"});return}try{const n=await R(String(e).trim()),s=S(String(a).trim());if(!n.hasRepository||!n.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}const o=N(s,n),l=q(r),c=l?["show","--format=","--no-ext-diff",l,"--",o]:["diff","HEAD","--",o];let u=await p(n.repositoryRootPath,c);if(!l&&u.exitCode!==0&&(u=await p(n.repositoryRootPath,["diff","--",o])),u.exitCode!==0){t.status(400).json({error:u.stderr||"git diff file failed"});return}if(!l&&!u.stdout.trim()){const f=await p(n.repositoryRootPath,["diff","--cached","--",o]);if(f.exitCode!==0){t.status(400).json({error:f.stderr||"git staged diff file failed"});return}u=f}if(!l&&!u.stdout.trim()){const f=k.join(n.repositoryRootPath,o);try{const d=await A.readFile(f,"utf-8");if(d){const m=d.split(`
|
|
10
|
+
`).map(x=>`+${x}`).join(`
|
|
11
|
+
`),g=d.split(`
|
|
12
|
+
`).length;u={stdout:[`diff --git a/${o} b/${o}`,"new file mode 100644","--- /dev/null",`+++ b/${o}`,`@@ -0,0 +1,${g} @@`,m].join(`
|
|
13
|
+
`),stderr:"",exitCode:0}}}catch{}}t.json({ok:!0,filePath:s,repositoryRelativeFilePath:o,commitHash:l||null,diffContent:u.stdout,workspacePath:n.workspacePath,repositoryRootPath:n.repositoryRootPath,usingParentGitRepo:n.usingParentGitRepo})}catch(n){const s=n;t.status(500).json({error:s.message||"git diff file failed"})}});const J=["STRICTLY act as a Git commit message generator. NEVER explain, narrate, or describe your task.",'NEVER start your reply with phrases like "\u6211\u4EEC\u88AB\u95EE\u5230", "\u4EE5\u4E0B\u662F", "\u597D\u7684", "Sure", "Here is", "I will", "I can", "Let me", "\u6839\u636E", "\u4E0B\u9762".',"NEVER include any meta-language or thinking out loud. Output ONLY the commit message text.","","Format (Conventional Commits, 1 line, optional body):"," <type>(<scope>): <description>","","Allowed types: feat, fix, refactor, test, docs, style, chore, perf, ci, build, revert.","First line MUST be <= 72 characters. <description> MUST be in Chinese.","Add a body (separated by a blank line) ONLY when the change needs more context."].join(`
|
|
14
|
+
`);function U(i){return["\u8BF7\u76F4\u63A5\u8F93\u51FA\u4E0A\u8FF0\u683C\u5F0F\u7684\u63D0\u4EA4\u4FE1\u606F,\u4E0D\u8981\u4EFB\u4F55\u989D\u5916\u6587\u5B57\u3002","\u4EE5\u4E0B\u662F\u6682\u5B58\u533A diff:","",i].join(`
|
|
15
|
+
`)}const B=/\b(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)(?:\s*\([^)]*\))?\s*:/i,gt=/^(?:feat|fix|chore|docs|style|refactor|perf|test|build|ci|revert)\s*:/i;function yt(i){const t=i.split(`
|
|
16
|
+
`)[0]?.trim()??"";if(!t||t.length<3)return!1;const e=(t.match(/[\u4e00-\u9fa5]/g)||[]).length,a=(t.match(/[a-zA-Z]{2,}/g)||[]).length;if(e===0&&a===0)return!1;const r=[/^以下是/,/^上面是/,/^生成的/,/^这是/,/^下面/,/^(?:Here|This|This is|That is)/i];for(const n of r)if(n.test(t))return!1;return e>=2||a>=2}const Pt=[/^\s*(?:我们|我)\s*(?:被\s*)?(?:问[到过]?|需要|要|将|来|先)[^.:\n]{0,60}?(?:提交信息|commit\s*message|[::,,。])/i,/^\s*(?:好的|好)\s*[,,::]/i,/^\s*(?:好的|好)\s*(?:以下是|上面是|提供|生成|给出)[^.:\n]{0,20}?[::]?/i,/^\s*是\s*[::]/i,/^\s*(?:Sure|Here(?:'s|\s+is)|We(?:'re| are)|I\s+(?:will|can|'ll)|Let\s+me|Certainly|Of\s+course)[^.:\n]{0,40}?[,,::.]/i,/^\s*根据\s*(?:您|你|您所|你所|所)?\s*(?:提供|上述|给[我您你])?\s*(?:的)?\s*diff\s*[,,::]?/i,/^\s*(?:以下|下面)[^,,::\n]{0,30}?提交信息\s*[,,::]?/i,/^\s*(?:以下|下面)\s*是\s*[,,::]/i,/^\s*生成\s*(?:一条|一个|了)?\s*(?:符合|新的)?\s*(?:conventional\s*commit|git)?\s*提交信息\s*[,,::]?/i,/^\s*```(?:text|markdown|bash|shell)?\s*\n?/i];function v(i){if(!i)return h.info("[Git/sanitize] \u8F93\u5165\u4E3A\u7A7A, \u8FD4\u56DE fallback"),"chore: update staged changes";let t=i.replace(/\r\n/g,`
|
|
17
|
+
`).trim();const e=JSON.stringify(i.substring(0,120)),a=t.match(B);if(a&&a.index!==void 0&&a.index<=80)t=t.slice(a.index);else{for(let f=0;f<5;f+=1){const d=t;for(const m of Pt)t=t.replace(m,"");if(t=t.replace(/^[\s*\-`,,']+/,"").trim(),t===d)break}const s=t.split(`
|
|
18
|
+
`)[0]?.trim()??"",o=!!t.match(B),l=!!t.match(gt),c=yt(t),u=s.length>100;if(!o&&!l&&!c&&!u)return h.info(`[Git/sanitize] \u5265\u79BB\u540E\u65E0\u6709\u6548\u5185\u5BB9, \u8FD4\u56DE fallback. raw=${e}, afterStrip=${JSON.stringify(t.substring(0,100))}`),"chore: update staged changes";!o&&(l||c||u)&&(t=s||t)}const r=t.search(/\n\s*\n/);if(r>0&&(t=t.slice(0,r)),t=t.replace(/\n+\s*```\s*$/g,"").trim(),!t)return h.info(`[Git/sanitize] \u622A\u65AD\u540E\u4E3A\u7A7A, \u8FD4\u56DE fallback. raw=${e}`),"chore: update staged changes";const n=t.split(`
|
|
19
|
+
`)[0]?.trim()??"";return n.length>100?(h.info(`[Git/sanitize] \u9996\u884C\u8D85\u8FC7 100 \u5B57\u7B26, \u4EC5\u4FDD\u7559\u9996\u884C. raw=${e}, firstLine=${JSON.stringify(n.substring(0,100))}`),n):(h.info(`[Git/sanitize] \u7ED3\u679C: ${JSON.stringify(t.substring(0,120))} (from raw=${e})`),t)}function K(i){const t=i.split(/\r?\n/);let e="";for(const a of t){const r=a.trim();if(!r.startsWith("data:"))continue;const n=r.slice(5).trim();if(!(!n||n==="[DONE]"))try{const o=JSON.parse(n)?.choices?.[0]?.delta?.content;typeof o=="string"&&o.length>0&&(e+=o)}catch{}}return e}function wt(i,t){if(t>5)return;const e=i.split(/\r?\n/);for(const a of e){const r=a.trim();if(!r.startsWith("data:"))continue;const n=r.slice(5).trim();if(!(!n||n==="[DONE]"))try{const s=JSON.parse(n),o=Object.keys(s),l=s?.choices,c=Array.isArray(l)?l[0]:null,u=c?.delta?Object.keys(c.delta):null,f=c?.delta?.content;h.info(`[Git/extract] chunk#${t} structure: topKeys=${JSON.stringify(o)}, choicesLen=${Array.isArray(l)?l.length:typeof l}, deltaKeys=${JSON.stringify(u)}, content=${JSON.stringify(f?.substring(0,50))}, finishReason=${JSON.stringify(c?.finish_reason)}`)}catch{}}}async function St(i,t,e,a){const r=`${i.replace(/\/+$/,"")}/chat/completions`,o=(await L.post(r,{model:e,stream:!1,messages:[{role:"system",content:J},{role:"user",content:U(a)}],max_tokens:200,temperature:.2},{headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},timeout:3e4})).data?.choices?.[0]?.message?.content?.trim()||"";return v(o)}async function kt(i,t,e,a,r,n){const s=`${i.replace(/\/+$/,"")}/chat/completions`,l=(await L.post(s,{model:e,stream:!0,messages:[{role:"system",content:J},{role:"user",content:U(a)}],max_tokens:200,temperature:.2},{headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`,Accept:"text/event-stream"},timeout:Number(process.env.AWS_AI_STREAM_TIMEOUT_MS)||6e4,responseType:"stream",signal:n})).data;let c="",u="",f="",d=0,m=0;return h.info(`[Git/streamAi] AI \u6D41\u5F00\u59CB: aiModel=${e}`),await new Promise((g,x)=>{const b=w=>{l.removeAllListeners("data"),l.removeAllListeners("end"),l.removeAllListeners("error"),l.removeAllListeners("aborted"),h.info(`[Git/streamAi] AI \u6D41\u7ED3\u675F: totalChunks=${d}, totalDeltas=${m}, bufferLength=${c.length}, sanitized=${JSON.stringify(w).substring(0,200)}`),g(w)};l.on("data",w=>{const j=typeof w=="string"?w:w.toString("utf8");d++,f+=j,d<=3&&h.info(`[Git/streamAi] chunk#${d}: length=${j.length}, preview=${JSON.stringify(j.substring(0,80))}`),wt(j,d);const F=f.split(/\n\n/);f=F.pop()??"";for(const Y of F){const M=K(Y);M&&(m++,c+=M,u=v(c),r(M,u))}}),l.on("end",()=>{if(f.trim()){const w=K(f);w?(h.info(`[Git/streamAi] \u5C3E\u90E8\u6B8B\u7559 delta: length=${w.length}`),c+=w):h.info(`[Git/streamAi] \u5C3E\u90E8\u6B8B\u7559\u65E0 delta, pendingData=${JSON.stringify(f.substring(0,100))}`)}b(v(c))}),l.on("error",w=>{h.error(`[Git/streamAi] \u6D41\u9519\u8BEF: ${w.message}`),x(w)}),l.on("aborted",()=>{h.info(`[Git/streamAi] \u6D41\u88AB\u4E2D\u6B62, bufferLength=${c.length}`),b(v(c))})})}async function E(i,t,e){const a=await R(String(i||t).trim());if(!a.hasRepository||!a.repositoryRootPath)return h.warn(`[Git/collectDiff] \u4E0D\u5728 Git \u4ED3\u5E93\u4E2D: workspacePath=${t}`),{truncatedDiff:"",error:{status:400,message:"workspace is not inside a git repository"}};const r=e.map(c=>c.path).filter(Boolean);h.info(`[Git/collectDiff] \u5F00\u59CB\u6536\u96C6 ${r.length} \u4E2A\u6682\u5B58\u6587\u4EF6\u5DEE\u5F02: ${r.join(", ").substring(0,200)}`);const n=[];for(const c of r){const u=N(c,a),f=await p(a.repositoryRootPath,["diff","--cached","--",u]);f.exitCode===0&&f.stdout.trim()?n.push(`--- ${c} ---
|
|
20
|
+
${f.stdout.trim()}`):h.info(`[Git/collectDiff] \u6587\u4EF6\u65E0\u6682\u5B58\u5DEE\u5F02\u6216\u83B7\u53D6\u5931\u8D25: ${c}, exitCode=${f.exitCode}, stdout.length=${f.stdout.length}`)}if(n.length===0){h.info("[Git/collectDiff] \u9010\u6587\u4EF6 diff \u4E3A\u7A7A, \u56DE\u9000\u5230\u5168\u91CF diff --cached");const c=await p(a.repositoryRootPath,["diff","--cached"]);c.exitCode===0&&c.stdout.trim()?(n.push(c.stdout.trim()),h.info(`[Git/collectDiff] \u5168\u91CF diff \u83B7\u53D6\u6210\u529F: length=${c.stdout.trim().length}`)):h.info(`[Git/collectDiff] \u5168\u91CF diff \u4E5F\u4E3A\u7A7A, exitCode=${c.exitCode}`)}if(n.length===0)return h.info("[Git/collectDiff] \u6682\u5B58\u533A\u6CA1\u6709\u5DEE\u5F02\u5185\u5BB9 (diffParts \u4E3A\u7A7A)"),{truncatedDiff:""};const s=n.join(`
|
|
21
|
+
|
|
22
|
+
`),o=15e3,l=s.length>o?`${s.slice(0,o)}
|
|
23
|
+
...(diff truncated due to length)`:s;return h.info(`[Git/collectDiff] \u6536\u96C6\u5B8C\u6210: diffContent.length=${s.length}, truncated=${l.length}`),{truncatedDiff:l}}function $(i,t,e){try{i.write(`event: ${t}
|
|
24
|
+
`),i.write(`data: ${JSON.stringify(e)}
|
|
25
|
+
|
|
26
|
+
`)}catch{}}P.post("/git/generate-commit-message",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a,stagedFiles:r,aiBaseUrl:n,aiApiKey:s,aiModel:o}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!Array.isArray(r)||r.length===0){t.status(400).json({error:"stagedFiles is required and must be a non-empty array"});return}if(!n||!String(n).trim()||!s||!String(s).trim()||!o||!String(o).trim()){t.status(400).json({error:"aiBaseUrl, aiApiKey and aiModel are required"});return}try{const{truncatedDiff:l,error:c}=await E(String(a||""),String(e),r);if(c){t.status(c.status).json({error:c.message});return}if(!l){t.json({message:"\u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9"});return}const u=await St(n,s,o,l);t.json({message:u})}catch(l){const c=l;h.error(`[Git] generate commit message failed: ${c.message}`),t.status(500).json({error:c.message||"generate commit message failed"})}}),P.post("/git/generate-commit-message-stream",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a,stagedFiles:r,aiBaseUrl:n,aiApiKey:s,aiModel:o}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!Array.isArray(r)||r.length===0){t.status(400).json({error:"stagedFiles is required and must be a non-empty array"});return}if(!n||!String(n).trim()||!s||!String(s).trim()||!o||!String(o).trim()){t.status(400).json({error:"aiBaseUrl, aiApiKey and aiModel are required"});return}h.info(`[Git/SSE] \u6536\u5230 generate-commit-message-stream \u8BF7\u6C42: workspacePath=${String(e||"").substring(0,40)}, stagedFiles=${r?.length??0}, aiModel=${o}, aiBaseUrl=${n}`),t.status(200),t.setHeader("Content-Type","text/event-stream; charset=utf-8"),t.setHeader("Cache-Control","no-cache, no-transform"),t.setHeader("Connection","keep-alive"),t.setHeader("X-Accel-Buffering","no"),i.socket?.setNoDelay(!0),t.flushHeaders?.();let l=!1,c=null,u=!1;const f={delta:0},d=`${i.ip}:${i.socket?.remotePort??"?"}`;i.on("close",()=>{l||(l=!0,h.info(`[Git/SSE] \u5BA2\u6237\u7AEF\u65AD\u5F00\u8FDE\u63A5: workspacePath=${String(e||"").substring(0,40)}, deltaEventsSent=${f.delta}, peer=${d}`))}),$(t,"init",{status:"generating"});try{const{truncatedDiff:m,error:g}=await E(String(a||""),String(e),r);if(g){h.warn(`[Git/SSE] collectStagedDiffForAi \u8FD4\u56DE\u9519\u8BEF: ${g.message}`),$(t,"error",{message:g.message}),t.end();return}if(!m){h.info("[Git/SSE] \u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9, \u76F4\u63A5\u8FD4\u56DE done"),$(t,"delta",{contentDelta:"",message:"\u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9"}),$(t,"done",{message:"\u6682\u5B58\u533A\u6CA1\u6709\u53D8\u66F4\u5185\u5BB9"}),t.end();return}h.info(`[Git/SSE] \u6536\u96C6\u5230 diff: truncatedDiff.length=${m.length}`),l&&h.info("[Git/SSE] \u5BA2\u6237\u7AEF\u5728 collectDiff \u671F\u95F4\u65AD\u5F00, \u4F46\u4ECD\u5C1D\u8BD5 AI \u8C03\u7528"),h.info(`[Git/SSE] \u5F00\u59CB\u8C03\u7528 AI streamAiForCommitMessage: aiModel=${o}`),c=new AbortController;let x="";const b=await kt(n,s,o,m,(w,j)=>{f.delta++,j!==x?(x=j,$(t,"delta",{contentDelta:w,message:j})):$(t,"delta",{contentDelta:w,message:j})},c.signal);h.info(`[Git/SSE] AI streamAiForCommitMessage \u5B8C\u6210: finalMessage=${JSON.stringify(b).substring(0,200)}, deltaEventsSent=${f.delta}`),l||$(t,"done",{message:b}),t.end()}catch(m){const g=m;h.error(`[Git/SSE] stream commit message failed: ${g.message}${g.stack?`
|
|
27
|
+
`+g.stack:""}`),l||$(t,"error",{message:g.message||"generate commit message failed"}),t.end()}}),P.post("/git/unstage-files",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a,filePaths:r}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const s=(Array.isArray(r)?r:[]).map(o=>S(String(o||"").trim())).filter(o=>o.length>0);if(s.length===0){t.status(400).json({error:"filePaths is required and must be a non-empty array"});return}try{const o=await R(String(a||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}let l="";for(const c of s){const u=G(c,o),f=await p(o.repositoryRootPath,["restore","--staged",...u]);f.exitCode!==0&&(l=f.stderr||`git restore --staged failed for ${c}`)}if(l){t.status(400).json({error:l});return}t.json({ok:!0,filePaths:s,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const l=o;t.status(500).json({error:l.message||"git unstage files failed"})}}),P.post("/git/restore-files",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a,filePaths:r}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}const s=(Array.isArray(r)?r:[]).map(o=>S(String(o||"").trim())).filter(o=>o.length>0);if(s.length===0){t.status(400).json({error:"filePaths is required and must be a non-empty array"});return}try{const o=await R(String(a||e).trim());if(!o.hasRepository||!o.repositoryRootPath){t.status(400).json({error:"workspace is not inside a git repository"});return}let l="";for(const c of s){const u=G(c,o),f=await p(o.repositoryRootPath,["restore",...u]);f.exitCode!==0&&(l=f.stderr||`git restore failed for ${c}`)}if(l){t.status(400).json({error:l});return}t.json({ok:!0,filePaths:s,workspacePath:o.workspacePath,repositoryRootPath:o.repositoryRootPath,relativeWorkspacePath:o.relativeWorkspacePath})}catch(o){const l=o;t.status(500).json({error:l.message||"git restore files failed"})}}),P.post("/git/diff-content",y,async(i,t)=>{const{workspacePath:e,repositoryPath:a,stagedFiles:r}=i.body||{};if(!e||!String(e).trim()){t.status(400).json({error:"workspacePath is required"});return}if(!Array.isArray(r)||r.length===0){t.status(400).json({error:"stagedFiles is required and must be a non-empty array"});return}try{const{truncatedDiff:n,error:s}=await E(String(a||""),String(e),r);if(s){t.status(s.status).json({error:s.message});return}t.json({ok:!0,diff:n||""})}catch(n){const s=n;h.error(`[Git/diff-content] \u83B7\u53D6\u6682\u5B58\u533A\u5DEE\u5F02\u5931\u8D25: ${s.message}`),t.status(500).json({error:s.message||"get diff content failed"})}});export{Q as assertGitWorkspacePathAccessible,St as callAiForCommitMessage,it as createAgentsWorkStashSnapshotRef,st as createMissingInitialCommitSnapshotError,G as createScopedFilePathspecArgs,ht as createScopedGitPathspecArgs,wt as debugAiStreamChunk,K as extractOpenAiStreamDelta,P as gitRouter,rt as isGitStashMissingInitialCommitError,ot as isGitStashNoChangesOutput,ut as mergeGitDiffSummaryFiles,dt as normalizeGitCommitMessage,D as parseGitDiffSummary,W as parseGitStashListLine,ct as parseGitStatusStagedPaths,lt as parseGitStatusUntrackedPaths,z as parseLatestGitStash,_ as persistAgentsWorkStashSnapshotRef,O as resolveGitCommandTimeoutMs,v as sanitizeCommitMessage,et as shouldIncludeGitDiffSummaryFile,kt as streamAiForCommitMessage};
|
|
28
|
+
|