Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,19 @@ gemini:
container_image: google-gemini/gemini-cli:latest
timeout: 30m

deepseek:
api_key: your-deepseek-api-key-here
base_url: https://api.deepseek.com # Optional, defaults to official API address
model: deepseek-chat # Optional, defaults to deepseek-chat
container_image: codeagent/deepseek-cli:latest
timeout: 30m

docker:
socket: unix:///var/run/docker.sock
network: bridge

# Code provider configuration
code_provider: claude # Options: claude, gemini
code_provider: claude # Options: claude, gemini, deepseek
use_docker: true # Whether to use Docker, false means use local CLI

# AI mention configuration
Expand Down
10 changes: 8 additions & 2 deletions internal/code/code.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (
)

const (
ProviderClaude = "claude"
ProviderGemini = "gemini"
ProviderClaude = "claude"
ProviderGemini = "gemini"
ProviderDeepSeek = "deepseek"
)

type Response struct {
Expand Down Expand Up @@ -48,6 +49,11 @@ func New(workspace *models.Workspace, cfg *config.Config) (Code, error) {
return NewGeminiDocker(workspace, cfg)
}
return NewGeminiLocal(workspace, cfg)
case ProviderDeepSeek:
if cfg.UseDocker {
return NewDeepSeekDocker(workspace, cfg)
}
return NewDeepSeekLocal(workspace, cfg)
default:
return nil, fmt.Errorf("unsupported code provider: %s", provider)
}
Expand Down
164 changes: 164 additions & 0 deletions internal/code/deepseek_docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package code

import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/qiniu/codeagent/internal/config"
"github.com/qiniu/codeagent/pkg/models"
"github.com/qiniu/x/log"
)

// deepSeekDocker Docker 实现
type deepSeekDocker struct {
containerName string
}

// NewDeepSeekDocker 创建 Docker DeepSeek 实现
func NewDeepSeekDocker(workspace *models.Workspace, cfg *config.Config) (Code, error) {
if cfg.DeepSeek.APIKey == "" {
return nil, fmt.Errorf("DEEPSEEK_API_KEY is required")
}

// 解析仓库信息,只获取仓库名,不包含完整URL
repoName := extractRepoName(workspace.Repository)

// Generate unique container name using shared function
containerName := generateContainerName("deepseek", workspace.Org, repoName, workspace)

// 检查是否已经有对应的容器在运行
if isContainerRunning(containerName) {
log.Infof("Found existing container: %s, reusing it", containerName)
return &deepSeekDocker{
containerName: containerName,
}, nil
}

// 确保路径存在
workspacePath, err := filepath.Abs(workspace.Path)
if err != nil {
return nil, fmt.Errorf("failed to get absolute workspace path: %w", err)
}

sessionPath, err := filepath.Abs(workspace.SessionPath)
if err != nil {
return nil, fmt.Errorf("failed to get absolute session path: %w", err)
}

// 检查是否使用了/tmp目录(在macOS上可能导致挂载问题)
if strings.HasPrefix(workspacePath, "/tmp/") {
log.Warnf("Warning: Using /tmp directory may cause mount issues on macOS. Consider using other path instead.")
log.Warnf("Current workspace path: %s", workspacePath)
}

if strings.HasPrefix(sessionPath, "/tmp/") {
log.Warnf("Warning: Using /tmp directory may cause mount issues on macOS. Consider using other path instead.")
log.Warnf("Current session path: %s", sessionPath)
}

// 检查路径是否存在
if _, err := os.Stat(workspacePath); os.IsNotExist(err) {
log.Errorf("Workspace path does not exist: %s", workspacePath)
return nil, fmt.Errorf("workspace path does not exist: %s", workspacePath)
}

if _, err := os.Stat(sessionPath); os.IsNotExist(err) {
log.Errorf("Session path does not exist: %s", sessionPath)
return nil, fmt.Errorf("session path does not exist: %s", sessionPath)
}

// 构建 Docker 命令
args := []string{
"run",
"--rm", // 容器运行完后自动删除
"-d", // 后台运行
"--name", containerName, // 设置容器名称
"-e", "DEEPSEEK_API_KEY=" + cfg.DeepSeek.APIKey,
"-e", "DEEPSEEK_BASE_URL=" + cfg.DeepSeek.BaseURL,
"-e", "DEEPSEEK_MODEL=" + cfg.DeepSeek.Model,
"-v", fmt.Sprintf("%s:/workspace", workspacePath), // 挂载工作空间
"-v", fmt.Sprintf("%s:/tmp/session", sessionPath), // 挂载临时目录
"-w", "/workspace", // 设置工作目录
}

// Mount processed .codeagent directory if available
if workspace.ProcessedCodeAgentPath != "" {
if _, err := os.Stat(workspace.ProcessedCodeAgentPath); err == nil {
args = append(args, "-v", fmt.Sprintf("%s:/workspace/.codeagent", workspace.ProcessedCodeAgentPath))
log.Infof("Mounting processed .codeagent directory: %s -> /workspace/.codeagent", workspace.ProcessedCodeAgentPath)
} else {
log.Warnf("Processed .codeagent directory not found: %s", workspace.ProcessedCodeAgentPath)
}
}

// Add container image
args = append(args, cfg.DeepSeek.ContainerImage)

// 打印调试信息
log.Infof("Docker command: docker %s", strings.Join(args, " "))

cmd := exec.Command("docker", args...)

// 捕获命令输出
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr

if err := cmd.Start(); err != nil {
log.Errorf("Failed to start Docker container: %v", err)
log.Errorf("Docker stderr: %s", stderr.String())
return nil, fmt.Errorf("failed to start Docker container: %w", err)
}

// 等待命令完成
if err := cmd.Wait(); err != nil {
log.Errorf("docker container failed: %v", err)
log.Errorf("docker stdout: %s", stdout.String())
log.Errorf("docker stderr: %s", stderr.String())
return nil, fmt.Errorf("docker container failed: %w", err)
}

log.Infof("docker container started successfully")

return &deepSeekDocker{
containerName: containerName,
}, nil
}

// Prompt 实现 Code 接口
func (d *deepSeekDocker) Prompt(message string) (*Response, error) {
// 使用内置的 deepseek-cli 工具(假设容器内有这个工具)
args := []string{
"exec",
d.containerName,
"deepseek-cli",
"--prompt",
message,
}

cmd := exec.Command("docker", args...)

log.Infof("Executing DeepSeek CLI with docker: %s", strings.Join(args, " "))

stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}

// 启动命令
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to execute deepseek: %w", err)
}

return &Response{Out: stdout}, nil
}

// Close 实现 Code 接口
func (d *deepSeekDocker) Close() error {
stopCmd := exec.Command("docker", "rm", "-f", d.containerName)
return stopCmd.Run()
}
143 changes: 143 additions & 0 deletions internal/code/deepseek_local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package code

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"

"github.com/qiniu/codeagent/internal/config"
"github.com/qiniu/codeagent/pkg/models"
"github.com/qiniu/x/log"
)

// deepSeekLocal 本地 HTTP API 实现
type deepSeekLocal struct {
workspace *models.Workspace
config *config.Config
client *http.Client
}

// NewDeepSeekLocal 创建本地 DeepSeek API 实现
func NewDeepSeekLocal(workspace *models.Workspace, cfg *config.Config) (Code, error) {
if cfg.DeepSeek.APIKey == "" {
return nil, fmt.Errorf("DEEPSEEK_API_KEY is required")
}

client := &http.Client{
Timeout: cfg.DeepSeek.Timeout,
}

return &deepSeekLocal{
workspace: workspace,
config: cfg,
client: client,
}, nil
}

type deepSeekRequest struct {
Model string `json:"model"`
Messages []deepSeekMessage `json:"messages"`
Stream bool `json:"stream"`
}

type deepSeekMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}

type deepSeekResponse struct {
Choices []struct {
Message deepSeekMessage `json:"message"`
} `json:"choices"`
}

// Prompt 实现 Code 接口 - HTTP API 版本
func (d *deepSeekLocal) Prompt(message string) (*Response, error) {
// 构建请求
reqBody := deepSeekRequest{
Model: d.config.DeepSeek.Model,
Messages: []deepSeekMessage{
{
Role: "user",
Content: message,
},
},
Stream: false,
}

jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}

// 设置超时
timeout := d.config.DeepSeek.Timeout
if timeout == 0 {
timeout = 5 * time.Minute
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

// 创建 HTTP 请求
req, err := http.NewRequestWithContext(ctx, "POST", d.config.DeepSeek.BaseURL+"/chat/completions", bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}

// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+d.config.DeepSeek.APIKey)

log.Infof("Executing DeepSeek API call to %s with model %s", d.config.DeepSeek.BaseURL, d.config.DeepSeek.Model)

// 发送请求
resp, err := d.client.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
log.Warnf("DeepSeek API call timed out after %s", timeout)
return nil, fmt.Errorf("deepseek API call timed out: %w", err)
}
return nil, fmt.Errorf("failed to call DeepSeek API: %w", err)
}
defer resp.Body.Close()

// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}

// 检查响应状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("DeepSeek API returned status %d: %s", resp.StatusCode, string(body))
}

// 解析响应
var deepSeekResp deepSeekResponse
if err := json.Unmarshal(body, &deepSeekResp); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}

if len(deepSeekResp.Choices) == 0 {
return nil, fmt.Errorf("no choices in DeepSeek response")
}

// 获取生成的内容
content := deepSeekResp.Choices[0].Message.Content

log.Infof("DeepSeek API call completed successfully")
return &Response{
Out: strings.NewReader(content),
}, nil
}

// Close 实现 Code 接口
func (d *deepSeekLocal) Close() error {
// HTTP 客户端不需要特殊的清理
return nil
}
Loading
Loading