散落在各处但贯穿全局的工程"地基":错误处理、ID 生成、事件总线、配置加载、依赖注入、GORM Gen、测试体系。
主文档:01 - 原理与使用
目录
1. 错误处理体系
1.1 自定义错误接口
type StatusError interface {
Code() int
Msg() string
IsAffectStability() bool // 是否影响系统稳定性指标(SRE 用)
}
1.2 错误码分配规则
按业务域分配 9 位数字段:
| 域 | 段 |
|---|---|
| Knowledge | 105_000_000 ~ 105_999_999 |
| Agent | 110_000_000 ~ 110_999_999 |
| Workflow | 120_000_000 ~ 120_999_999 |
| Plugin | 130_000_000 ~ 130_999_999 |
| Conversation | 140_000_000 ~ 140_999_999 |
| OpenAuth | 700_000_000 ~ 700_999_999 |
| ... |
每个域一个 .go 文件(knowledge.go、agent.go、workflow.go...),每个错误用 code.Register(code, msgTpl, opts...) 注册:
// errno/knowledge.go
var ErrKnowledgeNotFound = code.Register(105_001_001, "knowledge {id} not found")
var ErrDocumentTooLarge = code.Register(105_002_005, "document size {size}MB exceeds limit {limit}MB")
1.3 抛出与传递
业务代码:
return errorx.New(errno.ErrKnowledgeNotFound, errorx.KV("id", knowledgeID))
errorx.KV(k, v) 的设计很巧:消息模板里的 {id} 占位运行时填充,既保留结构化(可日志解析),又能渲染成可读消息。
1.4 转 HTTP 响应
源码:backend/api/internal/httputil/error_resp.go
func InternalError(ctx, err) {
var se errorx.StatusError
if errors.As(err, &se) {
// 业务错误:HTTP 200 + body 中带 code
c.JSON(200, BaseResp{Code: se.Code(), Msg: se.Msg()})
return
}
// 未知错误:打 stack,fallback 到通用 500
c.JSON(200, BaseResp{Code: 5000_0000, Msg: "internal server error"})
}
关键约定:HTTP 状态码统一 200
业务错误不通过 HTTP 状态码表达,而是 body 里的 code 字段。这是字节系常见做法,前端只看 body.code,简化客户端逻辑、避免误把业务错当成网络错。
1.5 与 i18n 对齐
回到 文档 03 §2 的内容——错误码的本地化消息前后端各有副本。后端拿到错误码,可选择:
- 用模板渲染中文/英文返回(用了
i18n.GetLocale(ctx)) - 或只返回 code,前端按当前 i18n 语言查表
Coze 实际是两者并行:后端返回时已国际化,前端有副本以应对网络断的情况。
2. ID 生成器
2.1 实现
源码:backend/infra/idgen/impl/idgen/idgen.go
不是雪花,是 Redis 自增辅助的复合 ID:
ID 64 位布局:
┌─────────────────┬──────────────┬────────┬───────────┐
│ seconds (32) │ millis (10) │ cnt (8)│ srvID (14)│
└─────────────────┴──────────────┴────────┴───────────┘
实现要点:
// 大致逻辑
sec, ms := nowSecondsAndMillis()
counter := redis.INCR(fmt.Sprintf("idgen:%d", sec*1000+ms)) % 256
id := (sec << 32) | (ms << 22) | (counter << 14) | serverID
seconds 32 位:可用 ~136 年millis 10 位(0–999)counter 8 位(0–255 / 毫秒,即每毫秒 256 个 ID)serverID 14 位(0–16383 个实例)
2.2 接口
type IDGenerator interface {
GenID(ctx) (int64, error)
GenMultiIDs(ctx, count) ([]int64, error)
}
GenMultiIDs 一次申请多个,建议 ≤200。批量是为了减少 Redis 往返。
2.3 注入
backend/application/base/appinfra/app_infra.go L82:
deps.IDGenSVC, err = idgen.New(deps.CacheCli)
整个应用共享一个 IDGenerator 实例,所有领域生成 ID 都走它,保证全局唯一。
2.4 与雪花的差异
| 雪花 | Coze 方案 | |
|---|---|---|
| 依赖 | 无(纯本地) | Redis(必需) |
| 时钟回拨 | 需特殊处理 | Redis 计数自带保护 |
| 容量 | 4096/ms | 256/ms(单实例) |
| 部署 | 需保证 worker_id 唯一 | serverID 配置即可 |
256/ms 看似少,实际16384 个实例 × 256 = 419 万 IDs/ms——足够。
3. NSQ 事件总线全清单
3.1 接口与多实现
type Producer interface {
Send(ctx, payload) error
}
type ConsumerService interface {
RegisterConsumer(nameServer, topic, group string, h ConsumerHandler) error
}
通过 COZE_MQ_TYPE 环境变量切实现:
| 值 | 实现位置 |
|---|---|
nsq(默认) | infra/eventbus/impl/nsq/ |
kafka | infra/eventbus/impl/kafka/ |
rmq(RocketMQ) | infra/eventbus/impl/rmq/ |
pulsar | infra/eventbus/impl/pulsar/ |
nats | infra/eventbus/impl/nats/ |
应用层不感知具体实现,只用接口。
3.2 三大 Topic
源码:backend/infra/eventbus/impl/eventbus.go
| Topic | Producer 初始化 | 用途 |
|---|---|---|
opencoze_search_app | InitAppEventProducer() (L97) | App / Bot 变更 → Search 域更新索引 |
opencoze_search_resource | InitResourceEventBusProducer() (L86) | Knowledge / Plugin / Workflow 资源变更 → Search 域 |
opencoze_knowledge | InitKnowledgeEventBusProducer() (L107) | 文档解析 / 切片 / 索引的全异步流水线 |
3.3 Consumer 注册位置
| Topic | Consumer 注册位置 |
|---|---|
| opencoze_search_app | backend/application/search/init.go |
| opencoze_search_resource | 同上 |
| opencoze_knowledge | backend/application/knowledge/init.go |
3.4 Consumer 模板
// nsq/consumer.go
func RegisterConsumer(nameServer, topic, group string, h ConsumerHandler) error {
cs, _ := nsq.NewConsumer(topic, group, nsq.NewConfig())
cs.AddHandler(adapter(h))
err := cs.ConnectToNSQLookupd(nameServer)
safego.Go(ctx, func() {
signal.WaitExit() // 收到 SIGINT/SIGTERM 优雅关闭
cs.Stop()
})
return err
}
safego.Go 是 Coze 的内部 goroutine 包装(带 panic recover),贯穿全代码库。
3.5 事件流图
┌─────────────┐ ┌────────────────────────┐ ┌──────────────┐
│ Knowledge │──────►│ opencoze_knowledge │──────►│ KnowledgeApp │
│ HTTP API │ │ IndexDocument │ │ 解析→切片→ │
└─────────────┘ │ IndexSlice │ │ Embedding │
│ DeleteKnowledgeData │ │ →双写索引 │
└────────────────────────┘ └──────────────┘
┌─────────────┐ ┌────────────────────────┐ ┌──────────────┐
│ App / Bot │──────►│ opencoze_search_app │──────►│ SearchApp │
│ Domain │ │ │ │ ES 索引更新 │
└─────────────┘ └────────────────────────┘ └──────────────┘
┌─────────────┐ ┌────────────────────────┐ ┌──────────────┐
│ Plugin / │──────►│ opencoze_search_resource│─────►│ SearchApp │
│ Workflow / │ │ │ │ 全局搜索 │
│ Knowledge │ │ │ │ 索引更新 │
└─────────────┘ └────────────────────────┘ └──────────────┘
4. 配置加载链路
4.1 .env 读取
Makefile 中:
debug:
cp ./docker/.env.debug.example ./docker/.env
代码侧用 os.Getenv 直读(没看到 godotenv 显式调用,可能由 docker compose --env-file 注入)。
关键 env 常量散在 backend/types/consts/:
const (
MySQLDsn = "MYSQL_DSN"
RedisAddr = "REDIS_ADDR"
MQServer = "MQ_SERVER"
OSSEndpoint = "STORAGE_ENDPOINT"
...
)
4.2 业务配置(从 OSS 加载)
源码:backend/bizpkg/config/config.go
type Config struct {
base *baseConfigManager
knowledge *knowledgeConfigManager
model *modelConfigManager
}
func Init(ctx context.Context, db *gorm.DB, oss storage.Storage) (*Config, error) {
// base 与 knowledge 从 MySQL 读
// model 从 OSS 读 JSON 配置
}
模型配置加载
源码:backend/bizpkg/config/modelmgr/modelmgr.go
启动时把 backend/conf/model/ 下的 yaml/json 上传到 OSS(或直接读本地),并扫描 model_meta.json 装载到内存:
启动 → 扫 conf/model/ → 上传 OSS → 同步加载到内存 → 提供 GetModelByID()
4.3 是否支持热更?
当前不支持 etcd 动态配置热更:
- 静态实现在 backend/infra/dynconf/impl/static_config.go
- etcd 在仓库里只用于 Milvus 的元数据,不用于业务配置
- 改了模型/插件配置需要重启 coze-server
商业版可能有动态配置中心,开源版做了占位接口但只有静态实现。
4.4 加载顺序
1. 读 .env 环境变量
2. 初始化 MySQL / Redis / OSS / IDGen
3. config.Init(ctx, db, oss)
├─ base config(MySQL)
├─ knowledge config(MySQL)
└─ model config(OSS / 本地 yaml)
4. 初始化各域 service
5. Hertz Server 启动
任一步失败,启动直接退出,不允许 partial start。
5. 依赖注入实战
5.1 三层组装
源码:backend/application/application.go L84-121
type basicServices struct {
infra *appinfra.AppDependencies
eventbus *eventbus.EventBus
modelMgrSVC modelmgr.Service
connectorSVC connector.Service
userSVC user.Service
// ...
}
type primaryServices struct {
*basicServices
pluginSVC plugin.Service
memorySVC memory.Service
knowledgeSVC knowledge.Service
workflowSVC workflow.Service
}
type complexServices struct {
*primaryServices
singleAgentSVC singleagent.Service
searchSVC search.Service
conversationSVC conversation.Service
}
依赖单向:Basic → Primary → Complex。Complex 服务可调 Primary,但反过来不行,避免循环。
5.2 没用 wire / dig
手写组装。原因:
- Go 工程里 wire 也是手写靠生成,可读性其实不更好
- 三层结构清晰,不需要 IoC 容器
- 显式
Init()函数,启动失败一目了然
5.3 写测试 Mock
每个 domain 有 internal/mock/ 目录,用 go.uber.org/mock/gomock 生成:
//go:generate mockgen \
// -destination ../../internal/mock/domain/knowledge/knowledge_mock.go \
// github.com/coze-dev/coze-studio/backend/domain/knowledge/service Service
测试样例(简化):
func TestKnowledgeService_Retrieve(t *testing.T) {
ctrl := gomock.NewController(t)
mockEmbed := mock.NewMockEmbedder(ctrl)
mockEmbed.EXPECT().Embed(gomock.Any(), gomock.Any()).
Return([][]float32{{0.1, 0.2}}, nil)
svc := NewKnowledgeService(mockEmbed, ...)
res, err := svc.Retrieve(ctx, "query")
assert.NoError(t, err)
}
5.4 mockey:函数级 patch
除了 gomock(接口级),Coze 还用 bytedance/mockey 做函数 monkey patch——可 mock 第三方库的具体函数,不需对方提供接口:
mockey.PatchConvey("test http call", t, func() {
mockey.Mock(http.Post).Return(&http.Response{StatusCode: 200}, nil).Build()
// 调用业务逻辑
})
这是 Go 测试圈较激进的做法,慎用,但调试某些深层依赖时很救命。
6. GORM Gen 自动生成
6.1 生成产物
每个 domain 的 internal/dal/query/gen.go,文件头有:
// Code generated by gorm.io/gen. DO NOT EDIT.
以 singleagent 为例,生成的 Query 结构:
type Query struct {
SingleAgentDraft singleAgentDraft
SingleAgentPublish singleAgentPublish
SingleAgentVersion singleAgentVersion
}
func (q *Query) WithContext(ctx) *Query
func (q *Query) Transaction(fc func(tx *Query) error) error
func (q *Query) ReadDB() *Query
func (q *Query) WriteDB() *Query
6.2 用法
// 链式查询(类型安全)
agents, err := q.WithContext(ctx).SingleAgentDraft.
Where(q.SingleAgentDraft.SpaceID.Eq(spaceID)).
Order(q.SingleAgentDraft.CreatedAt.Desc()).
Limit(20).
Find()
字段、操作都是编译期校验(q.SingleAgentDraft.SpaceID.Eq(...) 而不是字符串拼 SQL)。
6.3 生成命令
未在 Makefile 中暴露,通常:
go generate ./backend/domain/...
每个 domain 的 internal/dal/ 下有 gen.go 触发生成。
6.4 修改流程
1. 改 internal/dal/model/<table>.go(GORM model 定义)
或改 docker/atlas/opencoze_latest_schema.hcl(schema 主源)
2. 同步 schema 到库:make sync_db
3. 重新生成 query:go generate ./backend/...
4. 业务代码用新字段
5. 跑测试:go test ./backend/...
7. 测试体系
7.1 后端单元测试
位置:backend/domain/*/service/*_test.go
go test ./backend/domain/...
使用:
gomock:接口 mockmockey:函数 patchtestify/assert:断言- 表驱动测试(典型 Go style)
7.2 集成测试
文件命名:*_integration_test.go(常见约定),例如 backend/domain/knowledge/service/knowledge_integration_test.go
特点:
- 启动真实 MySQL 连接,DSN 从环境变量
MYSQL_DSN读 - 测完整的 DB → EventBus 链路
- CI 中通过 docker network 把
localhost替换成 service 名(如mysql:3306)
跑法:
MYSQL_DSN="user:pass@tcp(mysql:3306)/coze" go test -tags=integration ./backend/...
(具体 build tag 看每个文件,有的用 //go:build integration)
7.3 测试覆盖率门槛
CLAUDE.md 给出层级要求:
| 包层级 | 最低覆盖率 | 增量覆盖率 |
|---|---|---|
| Level 1(arch / infra) | 80% | 90% |
| Level 2(common / foundation) | 30% | 60% |
| Level 3-4(business / app) | 灵活 | 灵活 |
7.4 前端测试
配置
@coze-arch/vitest-config 提供 preset:
// frontend/apps/coze-studio/vitest.config.ts
import { defineConfig } from '@coze-arch/vitest-config';
export default defineConfig({
preset: 'web', // web / node 两种 preset
});
- preset: web → JSDOM 环境,可测组件
- preset: node → Node 环境,测纯逻辑
跑法
# 全量
rush test
# 单包
cd frontend/packages/foundation/global-store
npm run test
npm run test:cov # 带覆盖率
7.5 e2e 测试
源码扫描:未发现明确的 e2e 目录(没有 cypress/ 或 playwright/)。
可能性:
- 在内部 CI 中,不开源
- 或依靠手工 + Postman 集合
CLAUDE.md 提到 "Separate e2e subspace configuration",暗示有但未提供。
要补 e2e,推荐:
- Playwright(可写 TS,与前端栈对齐)
- 用 OpenAPI(
/v3/chat)做"语义级 e2e"——发问题、断言响应
关键路径速查
系列文档总览
至此一共 8 篇:
| # | 文档 | 重点 |
|---|---|---|
| 01 | 主文档 | 架构总览 |
| 02 | 会话与执行原理 | 会话/记忆/CodeRunner/Connector/OpenAPI/SSE/多模型 |
| 03 | 工程化与扩展 | Adapter 模式 / i18n / 可观测 / Atlas |
| 04 | 生态对比与排错 | vs Dify/n8n/LangGraph、生产部署、11 类排错 |
| 05 | 实战教程合集 | Bot/Workflow/Plugin 从零教程、新增节点 walkthrough |
| 06 | Eino 与工作流引擎深度 | Eino 抽象、节点机制、Loop/Batch、LLM 节点全程走读 |
| 07 | 知识库与插件深度 | RAG 流水线、Hybrid Search、OpenAPI 解析、OAuth 流程 |
| 08 | 横切工程化设施(本篇) | 错误/ID/事件/配置/DI/GORM Gen/测试 |