【码动四季】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 参与的姿势
| 刚入门 | 编译 + 读代码注释 | 2 小时 |
| 能写简单功能 | 修改配置加载、添加日志 | 半天 |
| 熟悉 Go 项目 | 添加新 provider、扩展模板函数 | 1-2 天 |
| 想深度贡献 | 规则冲突检测、Agent 步骤扩展 | 1 周 |




