欢迎光临
我们一直在努力

【码动四季】AtomCode 源码编译与二次开发入门:从 GitHub 拉代码到自定义一个 Skill 引擎

【码动四季】AtomCode 源码编译与二次开发入门:从 GitHub 拉代码到自定义一个 Skill 引擎

摘要:AtomCode 开源了你看到了吗?大多数人只用了它的 CLI 能力,却没碰过源码。本文手把手带你完成 AtomCode 的源码编译、项目结构拆解、以及 3 个二次开发实战——添加自定义规则加载器、扩展 Skill 模板引擎的变量函数、接入本地 Ollama 模型。全程代码可复现。读完你不仅能自己编译 AtomCode,还能给它加功能。

文章目录

    • 1. 为什么需要自己编译 AtomCode
    • 2. 环境搭建与项目结构
      • 2.1 编译环境要求
      • 2.2 拉取源码
      • 2.3 项目结构总览
      • 2.4 首次编译
    • 3. 二次开发实战一:自定义规则加载器
      • 3.1 需求场景
      • 3.2 定位源码
      • 3.3 实现优先级排序
      • 3.4 编译验证
    • 4. 二次开发实战二:为 Skill 模板引擎添加自定义函数
      • 4.1 需求场景
      • 4.2 定位源码
      • 4.3 添加函数支持
      • 4.4 在 Skill 中使用
    • 5. 二次开发实战三:接入本地 Ollama 模型
      • 5.1 需求场景
      • 5.2 定位源码
      • 5.3 添加 Ollama 适配器
      • 5.4 在供应商注册表中注册
      • 5.5 配置和使用
    • 6. 编译与调试技巧
      • 6.1 增量编译
      • 6.2 调试日志
      • 6.3 单元测试
    • 6. 二次开发全流程示意图
    • 7. 常见踩坑
      • 坑 1:Go 版本不匹配
      • 坑 2:Web 面板编译失败
      • 坑 3:国内依赖下载慢
    • 8. 总结与扩展方向
      • 8.1 你学会了什么
      • 8.2 更多可以尝试的方向
      • 8.3 参与的姿势

1. 为什么需要自己编译 AtomCode

大多数人用 AtomCode 只需要一个命令 atomcode 就够了。但有 3 个场景必须走源码编译路线:

场景一:需要修改内置行为

AtomCode 的 Rules 加载顺序是按文件名排序的。如果你的团队希望按规则优先级(而不是按字母序)加载,就得改源码里的 rule_loader.go。

场景二:需要接入内部模型

公司自建了 LLM 推理平台,API 格式和 OpenAI 不完全兼容。AtomCode 的 provider 层需要加一个自定义适配器。

场景三:需要调试疑难问题

Agent 执行到第三步就不动了,日志只显示 “step failed”。这时候看源码里的 agent_executor.go 比猜 log 含义高效得多。

本文基于 AtomCode v4.x 版本,GitHub 仓库地址在文末附注区。


2. 环境搭建与项目结构

2.1 编译环境要求

# 基础要求
Go >= 1.22
Node.js >= 20 (用于前端面板编译)
make
git

# 可选依赖
Docker (用于容器化编译)
gcc (用于 CGO 编译,部分平台需要)

验证环境:

$ go version
go version go1.22.4 darwin/arm64

$ node –version
v20.15.0

$ make –version
GNU Make 3.81

2.2 拉取源码

git clone https://github.com/atomgit/atomcode.git
cd atomcode

仓库体积约 180MB(含依赖和文档),国内用户建议配置 Go proxy:

export GOPROXY=https://goproxy.cn,direct

2.3 项目结构总览

拉完代码后,目录结构如下:

atomcode/
├── cmd/ # CLI 入口
│ └── atomcode/
│ └── main.go # 主程序入口
├── internal/ # 核心逻辑(不对外暴露)
│ ├── rule/ # 规则引擎
│ │ ├── loader.go # 规则加载器
│ │ ├── matcher.go # 规则匹配器
│ │ └── resolver.go # 冲突解析器
│ ├── skill/ # 技能模块引擎
│ │ ├── engine.go # 技能引擎核心
│ │ ├── template.go # 模板渲染(变量替换)
│ │ └── registry.go # 技能注册表
│ ├── agent/ # Agent 工作流引擎
│ │ ├── executor.go # 步骤执行器
│ │ ├── pipeline.go # 流水线编排
│ │ └── context.go # 上下文管理
│ ├── provider/ # LLM 供应商适配
│ │ ├── openai.go # OpenAI 兼容接口
│ │ ├── deepseek.go # DeepSeek 适配
│ │ └── registry.go # 供应商注册
│ └── config/ # 配置管理
│ ├── config.go # 配置加载
│ └── defaults.go # 默认值
├── pkg/ # 可导出的公共包
│ ├── api/ # HTTP API 定义
│ └── types/ # 公共类型
├── web/ # Web 面板(可选)
│ ├── src/ # React 前端
│ └── dist/ # 编译产物
├── go.mod
├── go.sum
└── Makefile

核心目录说明:

目录作用二次开发频率
internal/rule/ 规则加载和匹配逻辑 ⭐⭐⭐⭐⭐
internal/skill/ 技能模板引擎 ⭐⭐⭐⭐
internal/agent/ 工作流执行器 ⭐⭐⭐
internal/provider/ 模型供应商适配 ⭐⭐⭐⭐
internal/config/ 配置加载 ⭐⭐

#mermaid-svg-VrHwkWsXlqhcVQl1{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VrHwkWsXlqhcVQl1 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VrHwkWsXlqhcVQl1 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VrHwkWsXlqhcVQl1 .error-icon{fill:#552222;}#mermaid-svg-VrHwkWsXlqhcVQl1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VrHwkWsXlqhcVQl1 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VrHwkWsXlqhcVQl1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VrHwkWsXlqhcVQl1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VrHwkWsXlqhcVQl1 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VrHwkWsXlqhcVQl1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VrHwkWsXlqhcVQl1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VrHwkWsXlqhcVQl1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VrHwkWsXlqhcVQl1 .marker.cross{stroke:#333333;}#mermaid-svg-VrHwkWsXlqhcVQl1 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VrHwkWsXlqhcVQl1 p{margin:0;}#mermaid-svg-VrHwkWsXlqhcVQl1 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-VrHwkWsXlqhcVQl1 .cluster-label text{fill:#333;}#mermaid-svg-VrHwkWsXlqhcVQl1 .cluster-label span{color:#333;}#mermaid-svg-VrHwkWsXlqhcVQl1 .cluster-label span p{background-color:transparent;}#mermaid-svg-VrHwkWsXlqhcVQl1 .label text,#mermaid-svg-VrHwkWsXlqhcVQl1 span{fill:#333;color:#333;}#mermaid-svg-VrHwkWsXlqhcVQl1 .node rect,#mermaid-svg-VrHwkWsXlqhcVQl1 .node circle,#mermaid-svg-VrHwkWsXlqhcVQl1 .node ellipse,#mermaid-svg-VrHwkWsXlqhcVQl1 .node polygon,#mermaid-svg-VrHwkWsXlqhcVQl1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VrHwkWsXlqhcVQl1 .rough-node .label text,#mermaid-svg-VrHwkWsXlqhcVQl1 .node .label text,#mermaid-svg-VrHwkWsXlqhcVQl1 .image-shape .label,#mermaid-svg-VrHwkWsXlqhcVQl1 .icon-shape .label{text-anchor:middle;}#mermaid-svg-VrHwkWsXlqhcVQl1 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VrHwkWsXlqhcVQl1 .rough-node .label,#mermaid-svg-VrHwkWsXlqhcVQl1 .node .label,#mermaid-svg-VrHwkWsXlqhcVQl1 .image-shape .label,#mermaid-svg-VrHwkWsXlqhcVQl1 .icon-shape .label{text-align:center;}#mermaid-svg-VrHwkWsXlqhcVQl1 .node.clickable{cursor:pointer;}#mermaid-svg-VrHwkWsXlqhcVQl1 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VrHwkWsXlqhcVQl1 .arrowheadPath{fill:#333333;}#mermaid-svg-VrHwkWsXlqhcVQl1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VrHwkWsXlqhcVQl1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VrHwkWsXlqhcVQl1 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VrHwkWsXlqhcVQl1 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VrHwkWsXlqhcVQl1 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VrHwkWsXlqhcVQl1 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VrHwkWsXlqhcVQl1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VrHwkWsXlqhcVQl1 .cluster text{fill:#333;}#mermaid-svg-VrHwkWsXlqhcVQl1 .cluster span{color:#333;}#mermaid-svg-VrHwkWsXlqhcVQl1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VrHwkWsXlqhcVQl1 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VrHwkWsXlqhcVQl1 rect.text{fill:none;stroke-width:0;}#mermaid-svg-VrHwkWsXlqhcVQl1 .icon-shape,#mermaid-svg-VrHwkWsXlqhcVQl1 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VrHwkWsXlqhcVQl1 .icon-shape p,#mermaid-svg-VrHwkWsXlqhcVQl1 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VrHwkWsXlqhcVQl1 .icon-shape .label rect,#mermaid-svg-VrHwkWsXlqhcVQl1 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VrHwkWsXlqhcVQl1 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VrHwkWsXlqhcVQl1 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VrHwkWsXlqhcVQl1 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

use_skill

执行 Agent

代码辅助

用户输入

cmd/main.go

命令类型

skill/engine.go

agent/executor.go

provider 调用

rule/loader.go 加载上下文规则

skill/template.go 渲染模板

provider 生成

输出结果

步骤1: skill调用

步骤2: 命令执行

步骤3: 结果汇总

2.4 首次编译

# 开发模式编译(含调试符号)
make build-dev

# 生产模式编译(优化+strip)
make build

# 仅编译 CLI(不含 Web 面板,更快)
make build-cli

编译成功后,二进制文件在 bin/atomcode:

$ ./bin/atomcode version
AtomCode v4.x (commit: a1b2c3d4, built: 2026-05-20T10:00:00Z)

踩坑提示:如果遇到 CGO_ENABLED=0 导致的构建失败,检查是否使用了需要 CGO 的依赖。AtomCode 核心不依赖 CGO,但某些平台特定的扩展需要。遇到问题先跑 make build-cli CGO_ENABLED=0。


3. 二次开发实战一:自定义规则加载器

3.1 需求场景

AtomCode 默认的规则加载顺序是按文件名排序。团队有 20 条规则,希望按文件中定义的 priority 字段排序,而不是文件名。

3.2 定位源码

规则加载的核心在 internal/rule/loader.go:

// internal/rule/loader.go (简化)
package rule

import (
"os"
"sort"
"path/filepath"
)

// LoadRules 加载指定目录下的所有规则文件
func LoadRules(dir string) ([]*Rule, error) {
files, err := filepath.Glob(filepath.Join(dir, "*.md"))
if err != nil {
return nil, err
}

// 默认按文件名排序
sort.Strings(files) // ← 这里是我们需要改的地方

var rules []*Rule
for _, f := range files {
rule, err := ParseRuleFile(f)
if err != nil {
continue // 跳过解析失败的文件
}
rules = append(rules, rule)
}
return rules, nil
}

3.3 实现优先级排序

在规则文件中约定用 frontmatter 定义优先级:

—
priority: 10
scope: global
—

# API 命名规范

– RESTful 资源命名统一用复数名词

修改 loader.go,添加 frontmatter 解析和优先级排序:

// 在 ParseRuleFile 中增加 frontmatter 解析
func ParseRuleFile(path string) (*Rule, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}

rule := &Rule{
FilePath: path,
FileName: filepath.Base(path),
}

// 解析 frontmatter(— 分隔的 YAML 头)
if bytes.HasPrefix(data, []byte("—")) {
parts := bytes.SplitN(data[3:], []byte("—"), 2)
if len(parts) == 2 {
var meta struct {
Priority int `yaml:"priority"`
Scope string `yaml:"scope"`
}
if err := yaml.Unmarshal(parts[0], &meta); err == nil {
rule.Priority = meta.Priority
rule.Scope = meta.Scope
}
data = parts[1] // 去除 frontmatter 后的纯内容
}
}

rule.Content = string(data)
return rule, nil
}

// 修改 LoadRules,按优先级排序
func LoadRules(dir string) ([]*Rule, error) {
files, err := filepath.Glob(filepath.Join(dir, "*.md"))
if err != nil {
return nil, err
}

var rules []*Rule
for _, f := range files {
rule, err := ParseRuleFile(f)
if err != nil {
continue
}
rules = append(rules, rule)
}

// 按优先级降序排列(高优先级在前)
sort.Slice(rules, func(i, j int) bool {
return rules[i].Priority > rules[j].Priority
})

return rules, nil
}

3.4 编译验证

# 修改后重新编译
make build-cli

# 验证规则加载顺序
./bin/atomcode debug rules –dir .atomcode/rules/

# 输出示例:
# [P:100] api-naming-rule.md ← 优先级高的在前面
# [P:50] security-rule.md
# [P:10] code-style-rule.md
# [P:0] legacy-rule.md ← 未定义优先级的排最后

改动量:约 80 行代码,改动 2 个文件。从阅读源码到验证通过,约 1.5 小时。


4. 二次开发实战二:为 Skill 模板引擎添加自定义函数

4.1 需求场景

Skill 模板支持变量替换(如 ${resourceName}),但缺少字符串处理功能。希望在模板中能用 ${upper(name)} 把变量转大写,用 ${date()} 插入当前日期。

4.2 定位源码

模板引擎在 internal/skill/template.go:

// internal/skill/template.go (简化)
package skill

import (
"strings"
"regexp"
)

var variablePattern = regexp.MustCompile(`\\$\\{([^}]+)\\}`)

// Render 渲染模板,替换所有变量
func Render(template string, vars map[string]string) string {
return variablePattern.ReplaceAllStringFunc(template, func(match string) string {
key := match[2 : len(match)–1] // 去掉 ${ 和 }
if val, ok := vars[key]; ok {
return val
}
return match // 未找到的变量保持原样
})
}

4.3 添加函数支持

扩展模板渲染引擎,支持 ${funcName(args)} 语法:

// 在 template.go 中添加函数注册表
var funcMap = map[string]func(args …string) string{
"upper": func(args …string) string {
if len(args) > 0 {
return strings.ToUpper(args[0])
}
return ""
},
"lower": func(args …string) string {
if len(args) > 0 {
return strings.ToLower(args[0])
}
return ""
},
"date": func(args …string) string {
format := "2006-01-02"
if len(args) > 0 {
format = args[0]
}
return time.Now().Format(format)
},
"default": func(args …string) string {
if len(args) >= 2 && args[0] == "" {
return args[1]
}
if len(args) > 0 {
return args[0]
}
return ""
},
}

// 更新 Render 函数
func Render(template string, vars map[string]string) string {
return variablePattern.ReplaceAllStringFunc(template, func(match string) string {
inner := match[2 : len(match)–1]

// 检查是否是函数调用:funcName(arg1, arg2)
if idx := strings.Index(inner, "("); idx > 0 && strings.HasSuffix(inner, ")") {
funcName := inner[:idx]
argsStr := inner[idx+1 : len(inner)–1]
args := strings.Split(argsStr, ",")
for i := range args {
args[i] = strings.TrimSpace(args[i])
// 参数支持变量引用
if val, ok := vars[args[i]]; ok {
args[i] = val
}
}
if fn, ok := funcMap[funcName]; ok {
return fn(args…)
}
}

// 普通变量替换
if val, ok := vars[inner]; ok {
return val
}
return match
})
}

4.4 在 Skill 中使用

配置好之后,Skill 模板就可以这样写:

# .atomcode/skills/release-notes/SKILL.md

# 版本发布说明生成 Skill

## 模板

# ${upper(productName)} v${version} 发布说明

发布日期:${date(2006-01-02 15:04)}

## 更新内容

${default(changelog, "本次无重大更新")}

调用时传入变量:

# 使用自定义函数
use_skill release-notes vars:productName=atomcode,version=4.2,changelog="修复了规则冲突问题"

改动量:约 60 行代码,改动 1 个文件。从需求到验证,约 1 小时。


5. 二次开发实战三:接入本地 Ollama 模型

5.1 需求场景

公司有内部 Ollama 服务,部署了 DeepSeek 蒸馏模型。希望 AtomCode 直接调用本地 Ollama,不经过公网。

5.2 定位源码

供应商适配在 internal/provider/ 目录下。OpenAI 兼容接口在 openai.go:

// internal/provider/openai.go (简化)
package provider

import (
"bytes"
"encoding/json"
"net/http"
)

type OpenAIProvider struct {
apiKey string
baseURL string
model string
}

func NewOpenAIProvider(config ProviderConfig) *OpenAIProvider {
return &OpenAIProvider{
apiKey: config.APIKey,
baseURL: config.BaseURL,
model: config.Model,
}
}

func (p *OpenAIProvider) Chat(messages []Message) (*Response, error) {
// 调用 OpenAI 兼容 API
body := map[string]interface{}{
"model": p.model,
"messages": messages,
}

jsonData, _ := json.Marshal(body)
req, _ := http.NewRequest("POST", p.baseURL+"/chat/completions", bytes.NewBuffer(jsonData))
req.Header.Set("Authorization", "Bearer "+p.apiKey)
req.Header.Set("Content-Type", "application/json")

// … 发送请求和解析响应
}

5.3 添加 Ollama 适配器

Ollama 的 API 和 OpenAI 有差异——不需要 API Key,请求格式略有不同。创建一个新文件 ollama.go:

// internal/provider/ollama.go
package provider

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)

type OllamaProvider struct {
baseURL string
model string
}

func NewOllamaProvider(config ProviderConfig) *OllamaProvider {
baseURL := config.BaseURL
if baseURL == "" {
baseURL = "http://localhost:11434"
}
return &OllamaProvider{
baseURL: baseURL,
model: config.Model,
}
}

// Ollama 的聊天请求格式
type OllamaChatRequest struct {
Model string `json:"model"`
Messages []OllamaMessage `json:"messages"`
Stream bool `json:"stream"`
}

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

// Ollama 的聊天响应格式
type OllamaChatResponse struct {
Message OllamaMessage `json:"message"`
Done bool `json:"done"`
}

func (p *OllamaProvider) Chat(messages []Message) (*Response, error) {
// 转换消息格式
var ollamaMsgs []OllamaMessage
for _, m := range messages {
ollamaMsgs = append(ollamaMsgs, OllamaMessage{
Role: m.Role,
Content: m.Content,
})
}

reqBody := OllamaChatRequest{
Model: p.model,
Messages: ollamaMsgs,
Stream: false,
}

jsonData, _ := json.Marshal(reqBody)
url := fmt.Sprintf("%s/api/chat", p.baseURL)

resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("ollama request failed: %w", err)
}
defer resp.Body.Close()

var ollamaResp OllamaChatResponse
if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
return nil, fmt.Errorf("ollama response parse failed: %w", err)
}

return &Response{
Content: ollamaResp.Message.Content,
}, nil
}

// 注册到供应商工厂
func init() {
RegisterProvider("ollama", func(config ProviderConfig) Provider {
return NewOllamaProvider(config)
})
}

5.4 在供应商注册表中注册

打开 internal/provider/registry.go,确保 init() 函数导入了新适配器:

// internal/provider/registry.go
package provider

import (
_ "github.com/atomgit/atomcode/internal/provider/ollama" // 导入触发 init
)

// GetProvider 根据名称返回供应商实例
func GetProvider(name string, config ProviderConfig) (Provider, error) {
factory, ok := providers[strings.ToLower(name)]
if !ok {
return nil, fmt.Errorf("unknown provider: %s", name)
}
return factory(config), nil
}

5.5 配置和使用

在 .atomcode/config.yaml 中配置:

provider: ollama
ollama:
base_url: http://192.168.1.100:11434
model: deepseek–coder–v2:16b

#mermaid-svg-pQuaJQrMX52pLb2X{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pQuaJQrMX52pLb2X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pQuaJQrMX52pLb2X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pQuaJQrMX52pLb2X .error-icon{fill:#552222;}#mermaid-svg-pQuaJQrMX52pLb2X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pQuaJQrMX52pLb2X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pQuaJQrMX52pLb2X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pQuaJQrMX52pLb2X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pQuaJQrMX52pLb2X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pQuaJQrMX52pLb2X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pQuaJQrMX52pLb2X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pQuaJQrMX52pLb2X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pQuaJQrMX52pLb2X .marker.cross{stroke:#333333;}#mermaid-svg-pQuaJQrMX52pLb2X svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pQuaJQrMX52pLb2X p{margin:0;}#mermaid-svg-pQuaJQrMX52pLb2X .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-pQuaJQrMX52pLb2X .cluster-label text{fill:#333;}#mermaid-svg-pQuaJQrMX52pLb2X .cluster-label span{color:#333;}#mermaid-svg-pQuaJQrMX52pLb2X .cluster-label span p{background-color:transparent;}#mermaid-svg-pQuaJQrMX52pLb2X .label text,#mermaid-svg-pQuaJQrMX52pLb2X span{fill:#333;color:#333;}#mermaid-svg-pQuaJQrMX52pLb2X .node rect,#mermaid-svg-pQuaJQrMX52pLb2X .node circle,#mermaid-svg-pQuaJQrMX52pLb2X .node ellipse,#mermaid-svg-pQuaJQrMX52pLb2X .node polygon,#mermaid-svg-pQuaJQrMX52pLb2X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-pQuaJQrMX52pLb2X .rough-node .label text,#mermaid-svg-pQuaJQrMX52pLb2X .node .label text,#mermaid-svg-pQuaJQrMX52pLb2X .image-shape .label,#mermaid-svg-pQuaJQrMX52pLb2X .icon-shape .label{text-anchor:middle;}#mermaid-svg-pQuaJQrMX52pLb2X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-pQuaJQrMX52pLb2X .rough-node .label,#mermaid-svg-pQuaJQrMX52pLb2X .node .label,#mermaid-svg-pQuaJQrMX52pLb2X .image-shape .label,#mermaid-svg-pQuaJQrMX52pLb2X .icon-shape .label{text-align:center;}#mermaid-svg-pQuaJQrMX52pLb2X .node.clickable{cursor:pointer;}#mermaid-svg-pQuaJQrMX52pLb2X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-pQuaJQrMX52pLb2X .arrowheadPath{fill:#333333;}#mermaid-svg-pQuaJQrMX52pLb2X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-pQuaJQrMX52pLb2X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-pQuaJQrMX52pLb2X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pQuaJQrMX52pLb2X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-pQuaJQrMX52pLb2X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pQuaJQrMX52pLb2X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-pQuaJQrMX52pLb2X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-pQuaJQrMX52pLb2X .cluster text{fill:#333;}#mermaid-svg-pQuaJQrMX52pLb2X .cluster span{color:#333;}#mermaid-svg-pQuaJQrMX52pLb2X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-pQuaJQrMX52pLb2X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-pQuaJQrMX52pLb2X rect.text{fill:none;stroke-width:0;}#mermaid-svg-pQuaJQrMX52pLb2X .icon-shape,#mermaid-svg-pQuaJQrMX52pLb2X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-pQuaJQrMX52pLb2X .icon-shape p,#mermaid-svg-pQuaJQrMX52pLb2X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-pQuaJQrMX52pLb2X .icon-shape .label rect,#mermaid-svg-pQuaJQrMX52pLb2X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-pQuaJQrMX52pLb2X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-pQuaJQrMX52pLb2X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-pQuaJQrMX52pLb2X :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

openai

deepseek

ollama

自定义

AtomCode CLI

provider 选择

api.openai.com

api.deepseek.com

localhost:11434

企业内部 API

改动量:约 100 行代码,新增 1 个文件,修改 1 个文件。从开发到测试,约 2 小时。


6. 编译与调试技巧

6.1 增量编译

开发时不建议每次都全量编译,用 go run 直接跑:

# 直接运行(不生成二进制)
go run ./cmd/atomcode use_skill csdn-article-writer

# 配合热重载(安装 air)
air — cmd/atomcode use_skill csdn-article-writer

6.2 调试日志

AtomCode 内部有分层日志,编译时开启 debug:

# 编译 debug 版本
make build-dev

# 运行并查看详细日志
ATOMCODE_DEBUG=1 ./bin/atomcode use_skill csdn-article-writer

# 只查看某个模块的日志
ATOMCODE_DEBUG=skill,rule ./bin/atomcode use_skill csdn-article-writer

日志输出示例:

[DEBUG] rule/loader.go:42 加载规则文件: .atomcode/rules/csdn-article-creation-rule.md
[DEBUG] rule/matcher.go:18 匹配规则: scope=csdn, path=knowledge/csdn/test.md → 命中
[DEBUG] skill/engine.go:55 渲染模板: skill=csdn-article-writer, vars=3
[DEBUG] skill/template.go:120 替换变量: ${title} → "AtomCode 源码编译"
[DEBUG] provider/openai.go:88 请求模型: deepseek-v4-flash, tokens=2847

6.3 单元测试

# 运行所有测试
make test

# 只运行某个模块的测试
go test ./internal/rule/...
go test ./internal/skill/...
go test ./internal/provider/...

# 带覆盖率
go test -coverprofile=coverage.out ./internal/...
go tool cover -html=coverage.out -o coverage.html


6. 二次开发全流程示意图

Why:经过前面 3 个实战案例(规则加载器→模板函数→Ollama 适配器),你可能对单个环节的修改已经熟悉了。但完整的二次开发不仅仅是在 IDE 里改代码——从发现需求到提交 PR,中间涉及源码下载、环境搭建、定位修改、编译验证、提交 PR 五个关键阶段。下面这张流程图帮你建立「端到端」的全局视角,避免在某个环节卡住后不知道下一步该做什么。

#mermaid-svg-ZCJqXonn3pDFPBig{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZCJqXonn3pDFPBig .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZCJqXonn3pDFPBig .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZCJqXonn3pDFPBig .error-icon{fill:#552222;}#mermaid-svg-ZCJqXonn3pDFPBig .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZCJqXonn3pDFPBig .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZCJqXonn3pDFPBig .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZCJqXonn3pDFPBig .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZCJqXonn3pDFPBig .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZCJqXonn3pDFPBig .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZCJqXonn3pDFPBig .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZCJqXonn3pDFPBig .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZCJqXonn3pDFPBig .marker.cross{stroke:#333333;}#mermaid-svg-ZCJqXonn3pDFPBig svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZCJqXonn3pDFPBig p{margin:0;}#mermaid-svg-ZCJqXonn3pDFPBig .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZCJqXonn3pDFPBig .cluster-label text{fill:#333;}#mermaid-svg-ZCJqXonn3pDFPBig .cluster-label span{color:#333;}#mermaid-svg-ZCJqXonn3pDFPBig .cluster-label span p{background-color:transparent;}#mermaid-svg-ZCJqXonn3pDFPBig .label text,#mermaid-svg-ZCJqXonn3pDFPBig span{fill:#333;color:#333;}#mermaid-svg-ZCJqXonn3pDFPBig .node rect,#mermaid-svg-ZCJqXonn3pDFPBig .node circle,#mermaid-svg-ZCJqXonn3pDFPBig .node ellipse,#mermaid-svg-ZCJqXonn3pDFPBig .node polygon,#mermaid-svg-ZCJqXonn3pDFPBig .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZCJqXonn3pDFPBig .rough-node .label text,#mermaid-svg-ZCJqXonn3pDFPBig .node .label text,#mermaid-svg-ZCJqXonn3pDFPBig .image-shape .label,#mermaid-svg-ZCJqXonn3pDFPBig .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZCJqXonn3pDFPBig .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZCJqXonn3pDFPBig .rough-node .label,#mermaid-svg-ZCJqXonn3pDFPBig .node .label,#mermaid-svg-ZCJqXonn3pDFPBig .image-shape .label,#mermaid-svg-ZCJqXonn3pDFPBig .icon-shape .label{text-align:center;}#mermaid-svg-ZCJqXonn3pDFPBig .node.clickable{cursor:pointer;}#mermaid-svg-ZCJqXonn3pDFPBig .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZCJqXonn3pDFPBig .arrowheadPath{fill:#333333;}#mermaid-svg-ZCJqXonn3pDFPBig .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZCJqXonn3pDFPBig .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZCJqXonn3pDFPBig .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZCJqXonn3pDFPBig .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZCJqXonn3pDFPBig .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZCJqXonn3pDFPBig .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZCJqXonn3pDFPBig .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZCJqXonn3pDFPBig .cluster text{fill:#333;}#mermaid-svg-ZCJqXonn3pDFPBig .cluster span{color:#333;}#mermaid-svg-ZCJqXonn3pDFPBig div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZCJqXonn3pDFPBig .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZCJqXonn3pDFPBig rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZCJqXonn3pDFPBig .icon-shape,#mermaid-svg-ZCJqXonn3pDFPBig .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZCJqXonn3pDFPBig .icon-shape p,#mermaid-svg-ZCJqXonn3pDFPBig .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZCJqXonn3pDFPBig .icon-shape .label rect,#mermaid-svg-ZCJqXonn3pDFPBig .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZCJqXonn3pDFPBig .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZCJqXonn3pDFPBig .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZCJqXonn3pDFPBig :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

是

否

否

是

否

是

否

是

需修改

通过

发现需求或 Bug

Fork 官方仓库

git clone 到本地

搭建编译环境

Go 版本 ≥ 1.22

make build-cli 验证

安装 Go 1.22+

阅读对应模块源码

找到修改点?

加 Debug 日志重新编译

本地修改代码

go test 单元测试

测试通过?

make build 全量编译

集成验证

功能符合预期?

提交 commit & 推送

发起 Pull Request

Code Review 通过?

按 Review 意见修改

PR 合入主仓库

该流程图覆盖了本文 3 个实战案例的通用步骤。你在做自己的二次开发时,可以对照这个流程检查当前处于哪个阶段——如果「编译验证」反复失败,优先回看第 7 节的踩坑指南;如果不确定改哪个文件,参考第 2.3 节的项目结构总览。


7. 常见踩坑

坑 1:Go 版本不匹配

AtomCode v4.x 要求 Go 1.22+。用低版本编译会报语法错误,比如 range over int 是 1.22 才支持的。

解决:go version 确认版本。用 go.mod 中的 go 1.22 行判断最低要求。

坑 2:Web 面板编译失败

如果不需要 Web 面板,用 make build-cli 跳过前端编译。默认 make build 会尝试编译 React 前端,需要 Node.js 和 npm。

解决:make build-cli 只编译 CLI,不依赖 Node.js。

坑 3:国内依赖下载慢

go mod tidy 时部分依赖从 GitHub 下载慢。

解决:配置 Go proxy:GOPROXY=https://goproxy.cn,direct。如果某个依赖仍然慢,手动 go get -v 单个包看卡在哪里。


8. 总结与扩展方向

8.1 你学会了什么

能力改动量难度耗时
源码编译 AtomCode 0(直接编译) ⭐ 10 分钟
自定义规则加载器 ~80 行 / 2 文件 ⭐⭐ 1.5 小时
Skill 模板函数扩展 ~60 行 / 1 文件 ⭐⭐ 1 小时
接入 Ollama 模型 ~100 行 / 2 文件 ⭐⭐⭐ 2 小时

8.2 更多可以尝试的方向

  • 自定义 Agent 步骤类型:当前 Agent 只支持 skill 和 command 两种步骤,可以扩展支持 http_request、database_query 等
  • 规则冲突自动检测:在 internal/rule/resolver.go 中添加规则冲突检测逻辑,输出冲突报告
  • Token 用量统计:在 provider 调用层增加 token 计数和成本统计,输出到日志
  • 自定义输出格式:修改 cmd/atomcode/main.go 中的输出渲染,支持 JSON/YAML 格式导出

8.3 参与的姿势

你的 Go 水平推荐起点预期投入
刚入门 编译 + 读代码注释 2 小时
能写简单功能 修改配置加载、添加日志 半天
熟悉 Go 项目 添加新 provider、扩展模板函数 1-2 天
想深度贡献 规则冲突检测、Agent 步骤扩展 1 周
赞(0)
未经允许不得转载:171主机测评 » 【码动四季】AtomCode 源码编译与二次开发入门:从 GitHub 拉代码到自定义一个 Skill 引擎
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址