🐛 fix(ai): 隐藏 Claude CLI 后台控制台窗口 (#749)

## 背景

Windows 下选择 Claude CLI 作为当前 AI Provider 后,执行 SQL 美化、AI 洞察等会调用 Claude CLI
的功能时,会弹出 Claude 控制台窗口并打断界面操作。

Closes #694

## 变更点

- 统一 Claude CLI 认证检查、非流式聊天与流式聊天的命令创建入口
- Windows 子进程启用 `HideWindow` 与 `CREATE_NO_WINDOW`,同时保留已有启动标志
- 通过 build tag 保持 macOS/Linux 原有进程行为
- 新增 Windows 回归测试,覆盖隐藏窗口参数和既有 flags 保留逻辑

## 影响范围

- 仅影响 Claude CLI Provider 在 Windows 上启动后台子进程的窗口行为
- 不修改 AI 配置、请求参数、认证流程或前端交互
- 不新增依赖,不涉及数据迁移;macOS/Linux 行为不变

## 验证

- `go test ./internal/ai/provider -run ClaudeCLI -count=1`
- `go test ./internal/ai/service -count=1`
- `go vet ./internal/ai/provider`
- Linux amd64 与 macOS amd64 包级交叉编译通过
This commit is contained in:
Syngnat
2026-07-27 22:23:54 +08:00
committed by GitHub
4 changed files with 68 additions and 3 deletions

View File

@@ -137,7 +137,7 @@ func CheckClaudeCLILocalAuth(ctx context.Context) error {
defer cancel()
args := append(buildClaudeCLILocalAuthIsolationArgs(), "auth", "status", "--json")
cmd := claudeCommandContext(ctx, command.Path, args...)
cmd := newClaudeCLICommand(ctx, command.Path, args...)
env, err := buildClaudeCLIEnv(ai.ProviderConfig{AuthMode: "local-cli"}, cmd.Environ(), runtime.GOOS, claudeLookPath, fileExists)
if err != nil {
return err
@@ -224,7 +224,7 @@ func (p *ClaudeCLIProvider) Chat(ctx context.Context, req ai.ChatRequest) (*ai.C
if err != nil {
return nil, err
}
cmd := claudeCommandContext(ctx, command.Path, args...)
cmd := newClaudeCLICommand(ctx, command.Path, args...)
if err := p.setEnv(cmd); err != nil {
return nil, err
}
@@ -301,7 +301,7 @@ func (p *ClaudeCLIProvider) ChatStream(ctx context.Context, req ai.ChatRequest,
if err != nil {
return err
}
cmd := claudeCommandContext(ctx, command.Path, args...)
cmd := newClaudeCLICommand(ctx, command.Path, args...)
if err := p.setEnv(cmd); err != nil {
return err
}
@@ -469,6 +469,12 @@ func isClaudeCLITimeout(ctx context.Context, err error) bool {
return errors.Is(ctx.Err(), context.DeadlineExceeded) || errors.Is(err, context.DeadlineExceeded)
}
func newClaudeCLICommand(ctx context.Context, name string, args ...string) *exec.Cmd {
cmd := claudeCommandContext(ctx, name, args...)
configureClaudeCLICommand(cmd)
return cmd
}
func claudeCLIEndpointForLog(config ai.ProviderConfig) string {
baseURL := strings.TrimRight(strings.TrimSpace(config.BaseURL), "/")
if baseURL != "" {

View File

@@ -0,0 +1,7 @@
//go:build !windows
package provider
import "os/exec"
func configureClaudeCLICommand(_ *exec.Cmd) {}

View File

@@ -0,0 +1,21 @@
//go:build windows
package provider
import (
"os/exec"
"syscall"
)
const claudeCLIWindowsCreateNoWindow uint32 = 0x08000000
func configureClaudeCLICommand(cmd *exec.Cmd) {
if cmd == nil {
return
}
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.HideWindow = true
cmd.SysProcAttr.CreationFlags |= claudeCLIWindowsCreateNoWindow
}

View File

@@ -0,0 +1,31 @@
//go:build windows
package provider
import (
"os/exec"
"syscall"
"testing"
)
func TestConfigureClaudeCLICommandHidesWindowsConsole(t *testing.T) {
const existingCreationFlag uint32 = 0x00000004
cmd := &exec.Cmd{
SysProcAttr: &syscall.SysProcAttr{CreationFlags: existingCreationFlag},
}
configureClaudeCLICommand(cmd)
if cmd.SysProcAttr == nil {
t.Fatal("expected Windows process attributes to be configured")
}
if !cmd.SysProcAttr.HideWindow {
t.Fatal("expected Claude CLI window to be hidden")
}
if cmd.SysProcAttr.CreationFlags&claudeCLIWindowsCreateNoWindow == 0 {
t.Fatalf("expected CREATE_NO_WINDOW, got creation flags %#x", cmd.SysProcAttr.CreationFlags)
}
if cmd.SysProcAttr.CreationFlags&existingCreationFlag == 0 {
t.Fatalf("expected existing creation flags to be preserved, got %#x", cmd.SysProcAttr.CreationFlags)
}
}