Skip to content

PicoClaw 代码问题清单(看代码时顺手用claude过了下,如不对请忽略) #116

@trphoenix

Description

@trphoenix

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-344main.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.goproviders/types.go 类型重复

文件pkg/tools/types.gopkg/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 实体(&amp; &lt; 等未解码)
  • 注释标签

建议:考虑使用 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),

缓冲区满后,PublishInboundPublishOutbound 会阻塞调用方(渠道 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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions