PicoClaw 代码问题清单
文档信息
| 项目 |
内容 |
| 文档日期 |
2026-02-13 |
| 评审范围 |
全部源码(~40 个 Go 文件) |
| 总体质量评分 |
6.5/10 |
P0 - 必须修复
BUG-001 skillsCmd() 函数重复定义
文件:cmd/picoclaw/main.go
问题:skills 子命令的处理逻辑在 main() 的 switch-case(第 117-164 行)中已经完整实现,但在第 1267-1312 行又定义了一个独立的 skillsCmd() 函数,内容几乎一模一样。该函数永远不会被调用。
影响:死代码,增加维护混乱度。
修复建议:删除 skillsCmd() 函数(第 1267-1312 行),或将 switch-case 中的逻辑提取到 skillsCmd() 中统一调用。
// main.go:101 - switch 中已处理 skills
case "skills":
// ... 完整处理逻辑 (117-164行)
// main.go:1267 - 重复的函数定义(死代码)
func skillsCmd() {
// ... 几乎相同的逻辑
}
BUG-002 CronService 任务重复执行竞态
文件:pkg/cron/service.go:131-175
问题:checkJobs() 中先加锁收集到期任务、清空 NextRunAtMS 并保存、再解锁后执行。但如果 onJob 回调执行缓慢(如 LLM 调用 30s+),下一次 1 秒 tick 的 checkJobs() 进入时,虽然 NextRunAtMS 已被清空,但 executeJob() 中重新计算并设置了新的 NextRunAtMS,期间存在短暂竞态窗口。
影响:极端场景下可能导致定时任务重复触发。
修复建议:添加"执行中"标记,在任务执行期间跳过调度:
type CronJob struct {
// ...
Executing bool `json:"-"` // 不持久化,仅运行时标记
}
BUG-003 Session Key 文件名包含非法字符
文件:pkg/session/manager.go:149
问题:
sessionPath := filepath.Join(sm.storage, session.Key+".json")
Session Key 格式为 channel:chatID(如 telegram:123456),直接用作文件名。: 在 Windows 上是非法文件名字符,在 macOS 上也不推荐使用。
影响:Windows 平台上会话持久化失败。
修复建议:对 Key 进行 sanitize,将 : 替换为 _ 或使用 URL encoding:
safeName := strings.ReplaceAll(session.Key, ":", "_")
sessionPath := filepath.Join(sm.storage, safeName+".json")
BUG-004 HTTPProvider 无超时设置
文件:pkg/providers/http_provider.go:30-31
问题:
client := &http.Client{
Timeout: 0, // 无超时!
}
Timeout: 0 表示无限等待。如果 LLM API 服务端无响应(网络故障、服务宕机),AgentLoop.runLLMIteration() 会永久阻塞,整个 Agent 挂死。
影响:Gateway 模式下 Agent 挂死,所有渠道消息无法处理。
修复建议:
client := &http.Client{
Timeout: 120 * time.Second, // 2 分钟超时
}
P1 - 建议修复
BUG-005 createWorkspaceTemplates() 重复遍历
文件:cmd/picoclaw/main.go:338-344 和 main.go:382-388
问题:templates map 被遍历了两次,写入完全相同的文件。第二次遍历是冗余的。
// 第一次(338行)
for filename, content := range templates {
// ... 写入文件
}
// ... 中间创建 memory/skills 目录 ...
// 第二次(382行)- 完全多余
for filename, content := range templates {
// ... 相同的写入逻辑
}
影响:性能浪费(虽然有文件存在检查所以不会覆盖),代码混乱。
修复建议:删除第二次遍历(第 382-388 行)。
BUG-006 tools/types.go 与 providers/types.go 类型重复
文件:pkg/tools/types.go 和 pkg/providers/types.go
问题:两个包各自定义了几乎完全相同的类型:
| 类型 |
tools/types.go |
providers/types.go |
Message |
有 |
有 |
ToolCall |
有 |
有 |
FunctionCall |
有 |
有 |
LLMResponse |
有 |
有 |
UsageInfo |
有 |
有 |
LLMProvider |
有 |
有 |
ToolDefinition |
有 |
有 |
ToolFunctionDefinition |
有 |
有 |
影响:
- 维护时改了一处忘改另一处,导致隐蔽 bug
- 代码膨胀,违反 DRY 原则
tools/types.go 中的类型实际未被使用(tools 包内部使用的是 providers.Message 等)
修复建议:删除 pkg/tools/types.go,统一使用 pkg/providers/types.go 中的定义。
BUG-007 SlackConfig.AllowFrom 类型不一致
文件:pkg/config/config.go:130-134
问题:
type SlackConfig struct {
// ...
AllowFrom []string `json:"allow_from"` // 使用 []string
}
其他 7 个渠道(Telegram/Discord/QQ/钉钉/飞书/WhatsApp/MaixCam)都使用 FlexibleStringSlice,唯独 Slack 使用原生 []string。
影响:Slack 配置中 allow_from 不支持数字格式(如 [123456]),与其他渠道行为不一致。
修复建议:
type SlackConfig struct {
// ...
AllowFrom FlexibleStringSlice `json:"allow_from"`
}
BUG-008 Token 估算对中文严重不准
文件:pkg/agent/loop.go:630-635
问题:
func (al *AgentLoop) estimateTokens(messages []providers.Message) int {
total := 0
for _, m := range messages {
total += len(m.Content) / 4 // 4 chars per token
}
return total
}
len(m.Content) 返回的是字节数而非字符数。对于 UTF-8 编码的中文,每个汉字 3 字节,而在大多数 tokenizer 中每个汉字约 1-2 token。所以:
- 中文 "你好":
len() = 6 字节,估算 = 1.5 token,实际 ≈ 2 token(偏差小)
- 但更关键的是,中文文本的 bytes/token 比值约 3-6,不是 4
影响:摘要触发时机不准确。对中文内容可能过晚触发摘要,导致上下文溢出被 LLM 截断。
修复建议:
func (al *AgentLoop) estimateTokens(messages []providers.Message) int {
total := 0
for _, m := range messages {
// 使用 rune 计数(字符数)而非字节数
// 英文约 4 字符/token,中文约 1.5 字符/token
// 取折中值 3 字符/token
total += utf8.RuneCountInString(m.Content) / 3
}
return total
}
P2 - 优化建议
OPT-001 main.go 过于臃肿
文件:cmd/picoclaw/main.go(1514 行)
问题:一个文件包含了 CLI 入口、所有子命令实现、工作空间模板、辅助函数等。
建议拆分:
cmd/picoclaw/
├── main.go # 仅 main() + printHelp()
├── agent.go # agentCmd(), interactiveMode(), simpleInteractiveMode()
├── gateway.go # gatewayCmd(), setupCronTool()
├── auth.go # authCmd() 及所有 auth 子命令
├── cron.go # cronCmd() 及所有 cron 子命令
├── skills.go # skillsCmd() 及所有 skills 子命令
├── migrate.go # migrateCmd()
├── onboard.go # onboard(), createWorkspaceTemplates()
└── helpers.go # loadConfig(), getConfigPath(), copyDirectory()
OPT-002 未使用 CLI 框架
问题:所有命令行参数解析都是手写的 os.Args 遍历,散落在多个函数中。
风险:
- 无自动帮助文档生成
- 参数验证不完整(如
--every 传入非数字不会报错)
- 不支持
--help 标志(仅 migrate 子命令支持)
建议:考虑引入 spf13/cobra(Go 社区标准 CLI 框架),但需权衡二进制体积增加。
OPT-003 日志风格不统一
问题:代码中混用了三种日志方式:
| 方式 |
使用位置 |
logger.InfoCF(...) |
agent, channels, tools |
log.Printf(...) |
cron/service.go |
fmt.Printf(...) |
main.go 所有子命令 |
建议:统一使用 pkg/logger 包。对用户输出使用 fmt,对内部日志统一使用 logger,移除对标准库 log 的直接使用。
OPT-004 BaseChannel 运行状态非线程安全
文件:pkg/channels/base.go:23-26
问题:
type BaseChannel struct {
// ...
running bool // 无锁保护
}
running 字段在 setRunning() 中写入、在 IsRunning() 中读取,但无任何同步原语。虽然目前只在单 goroutine 中修改,但作为被多 goroutine 共享的接口实现,这是隐患。
建议:使用 atomic.Bool 替代。
OPT-005 WebFetchTool HTML 提取过于简陋
文件:pkg/tools/web.go:275-298
问题:extractText() 使用正则表达式移除 HTML 标签:
re := regexp.MustCompile(`<[^>]+>`)
result = re.ReplaceAllLiteralString(result, "")
这种方式无法处理:
- 嵌套标签属性中的
>
- CDATA 区段
- HTML 实体(
& < 等未解码)
- 注释标签
建议:考虑使用 golang.org/x/net/html 进行正规的 DOM 遍历和文本提取,或引入轻量级 HTML-to-text 库。但需权衡二进制体积。
OPT-006 SubagentManager 无工具能力
文件:pkg/tools/subagent.go:73-82
问题:子代理只做单次 LLM 调用,不传入任何工具定义:
messages := []providers.Message{
{Role: "system", Content: "You are a subagent..."},
{Role: "user", Content: task.Task},
}
response, err := sm.provider.Chat(ctx, messages, nil, ...) // tools = nil
影响:子代理无法执行文件操作、Web 搜索等工具,只能做纯文本回答,能力大打折扣。
建议:为子代理提供工具子集(如 web_search、web_fetch、read_file),并实现简化版的迭代循环。
OPT-007 消息总线无背压机制
文件:pkg/bus/bus.go:16-17
问题:
inbound: make(chan InboundMessage, 100),
outbound: make(chan OutboundMessage, 100),
缓冲区满后,PublishInbound 和 PublishOutbound 会阻塞调用方(渠道 goroutine 或 Agent goroutine),但无任何告警或丢弃策略。
影响:如果 Agent 处理速度跟不上消息到达速度,所有渠道发布者会被阻塞。
建议:添加非阻塞发布选项或溢出告警:
func (mb *MessageBus) PublishInbound(msg InboundMessage) bool {
select {
case mb.inbound <- msg:
return true
default:
logger.WarnC("bus", "Inbound buffer full, message dropped")
return false
}
}
OPT-008 缺少优雅关闭的超时控制
文件:cmd/picoclaw/main.go:727-734
问题:
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
<-sigChan
fmt.Println("\nShutting down...")
cancel()
heartbeatService.Stop()
cronService.Stop()
agentLoop.Stop()
channelManager.StopAll(ctx) // ctx 已被 cancel
关闭流程没有超时保护。如果某个 Channel 的 Stop() 卡住(如 WebSocket 断开超时),整个进程会挂起无法退出。
建议:
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownCancel()
channelManager.StopAll(shutdownCtx)
统计汇总
| 等级 |
数量 |
说明 |
| P0 必须修复 |
4 |
含 1 个死代码、1 个竞态、1 个跨平台 bug、1 个挂死风险 |
| P1 建议修复 |
4 |
含 2 个重复代码、1 个类型不一致、1 个估算偏差 |
| P2 优化建议 |
8 |
架构优化、安全增强、健壮性提升 |
| 合计 |
16 |
|
文档生成日期:2026-02-13
PicoClaw 代码问题清单
文档信息
P0 - 必须修复
BUG-001
skillsCmd()函数重复定义文件:
cmd/picoclaw/main.go问题:
skills子命令的处理逻辑在main()的 switch-case(第 117-164 行)中已经完整实现,但在第 1267-1312 行又定义了一个独立的skillsCmd()函数,内容几乎一模一样。该函数永远不会被调用。影响:死代码,增加维护混乱度。
修复建议:删除
skillsCmd()函数(第 1267-1312 行),或将 switch-case 中的逻辑提取到skillsCmd()中统一调用。BUG-002 CronService 任务重复执行竞态
文件:
pkg/cron/service.go:131-175问题:
checkJobs()中先加锁收集到期任务、清空NextRunAtMS并保存、再解锁后执行。但如果onJob回调执行缓慢(如 LLM 调用 30s+),下一次 1 秒 tick 的checkJobs()进入时,虽然NextRunAtMS已被清空,但executeJob()中重新计算并设置了新的NextRunAtMS,期间存在短暂竞态窗口。影响:极端场景下可能导致定时任务重复触发。
修复建议:添加"执行中"标记,在任务执行期间跳过调度:
BUG-003 Session Key 文件名包含非法字符
文件:
pkg/session/manager.go:149问题:
Session Key 格式为
channel:chatID(如telegram:123456),直接用作文件名。:在 Windows 上是非法文件名字符,在 macOS 上也不推荐使用。影响:Windows 平台上会话持久化失败。
修复建议:对 Key 进行 sanitize,将
:替换为_或使用 URL encoding:BUG-004 HTTPProvider 无超时设置
文件:
pkg/providers/http_provider.go:30-31问题:
Timeout: 0表示无限等待。如果 LLM API 服务端无响应(网络故障、服务宕机),AgentLoop.runLLMIteration()会永久阻塞,整个 Agent 挂死。影响:Gateway 模式下 Agent 挂死,所有渠道消息无法处理。
修复建议:
P1 - 建议修复
BUG-005
createWorkspaceTemplates()重复遍历文件:
cmd/picoclaw/main.go:338-344和main.go:382-388问题:
templatesmap 被遍历了两次,写入完全相同的文件。第二次遍历是冗余的。影响:性能浪费(虽然有文件存在检查所以不会覆盖),代码混乱。
修复建议:删除第二次遍历(第 382-388 行)。
BUG-006
tools/types.go与providers/types.go类型重复文件:
pkg/tools/types.go和pkg/providers/types.go问题:两个包各自定义了几乎完全相同的类型:
MessageToolCallFunctionCallLLMResponseUsageInfoLLMProviderToolDefinitionToolFunctionDefinition影响:
tools/types.go中的类型实际未被使用(tools 包内部使用的是providers.Message等)修复建议:删除
pkg/tools/types.go,统一使用pkg/providers/types.go中的定义。BUG-007 SlackConfig.AllowFrom 类型不一致
文件:
pkg/config/config.go:130-134问题:
其他 7 个渠道(Telegram/Discord/QQ/钉钉/飞书/WhatsApp/MaixCam)都使用
FlexibleStringSlice,唯独 Slack 使用原生[]string。影响:Slack 配置中
allow_from不支持数字格式(如[123456]),与其他渠道行为不一致。修复建议:
BUG-008 Token 估算对中文严重不准
文件:
pkg/agent/loop.go:630-635问题:
len(m.Content)返回的是字节数而非字符数。对于 UTF-8 编码的中文,每个汉字 3 字节,而在大多数 tokenizer 中每个汉字约 1-2 token。所以:len()= 6 字节,估算 = 1.5 token,实际 ≈ 2 token(偏差小)影响:摘要触发时机不准确。对中文内容可能过晚触发摘要,导致上下文溢出被 LLM 截断。
修复建议:
P2 - 优化建议
OPT-001 main.go 过于臃肿
文件:
cmd/picoclaw/main.go(1514 行)问题:一个文件包含了 CLI 入口、所有子命令实现、工作空间模板、辅助函数等。
建议拆分:
OPT-002 未使用 CLI 框架
问题:所有命令行参数解析都是手写的
os.Args遍历,散落在多个函数中。风险:
--every传入非数字不会报错)--help标志(仅 migrate 子命令支持)建议:考虑引入
spf13/cobra(Go 社区标准 CLI 框架),但需权衡二进制体积增加。OPT-003 日志风格不统一
问题:代码中混用了三种日志方式:
logger.InfoCF(...)log.Printf(...)fmt.Printf(...)建议:统一使用
pkg/logger包。对用户输出使用fmt,对内部日志统一使用logger,移除对标准库log的直接使用。OPT-004 BaseChannel 运行状态非线程安全
文件:
pkg/channels/base.go:23-26问题:
running字段在setRunning()中写入、在IsRunning()中读取,但无任何同步原语。虽然目前只在单 goroutine 中修改,但作为被多 goroutine 共享的接口实现,这是隐患。建议:使用
atomic.Bool替代。OPT-005 WebFetchTool HTML 提取过于简陋
文件:
pkg/tools/web.go:275-298问题:
extractText()使用正则表达式移除 HTML 标签:这种方式无法处理:
>&<等未解码)建议:考虑使用
golang.org/x/net/html进行正规的 DOM 遍历和文本提取,或引入轻量级 HTML-to-text 库。但需权衡二进制体积。OPT-006 SubagentManager 无工具能力
文件:
pkg/tools/subagent.go:73-82问题:子代理只做单次 LLM 调用,不传入任何工具定义:
影响:子代理无法执行文件操作、Web 搜索等工具,只能做纯文本回答,能力大打折扣。
建议:为子代理提供工具子集(如 web_search、web_fetch、read_file),并实现简化版的迭代循环。
OPT-007 消息总线无背压机制
文件:
pkg/bus/bus.go:16-17问题:
缓冲区满后,
PublishInbound和PublishOutbound会阻塞调用方(渠道 goroutine 或 Agent goroutine),但无任何告警或丢弃策略。影响:如果 Agent 处理速度跟不上消息到达速度,所有渠道发布者会被阻塞。
建议:添加非阻塞发布选项或溢出告警:
OPT-008 缺少优雅关闭的超时控制
文件:
cmd/picoclaw/main.go:727-734问题:
关闭流程没有超时保护。如果某个 Channel 的
Stop()卡住(如 WebSocket 断开超时),整个进程会挂起无法退出。建议:
统计汇总
文档生成日期:2026-02-13