Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/picoclaw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func main() {

workspace := cfg.WorkspacePath()
installer := skills.NewSkillInstaller(workspace)
// 获取全局配置目录和内置 skills 目录
// get global config directory and builtin skills directory
globalDir := filepath.Dir(getConfigPath())
globalSkillsDir := filepath.Join(globalDir, "skills")
builtinSkillsDir := filepath.Join(globalDir, "picoclaw", "skills")
Expand Down
46 changes: 23 additions & 23 deletions pkg/channels/qq.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,31 @@ func (c *QQChannel) Start(ctx context.Context) error {

logger.InfoC("qq", "Starting QQ bot (WebSocket mode)")

// 创建 token source
// create token source
credentials := &token.QQBotCredentials{
AppID: c.config.AppID,
AppSecret: c.config.AppSecret,
}
c.tokenSource = token.NewQQBotTokenSource(credentials)

// 创建子 context
// create child context
c.ctx, c.cancel = context.WithCancel(ctx)

// 启动自动刷新 token 协程
// start auto-refresh token goroutine
if err := token.StartRefreshAccessToken(c.ctx, c.tokenSource); err != nil {
return fmt.Errorf("failed to start token refresh: %w", err)
}

// 初始化 OpenAPI 客户端
// initialize OpenAPI client
c.api = botgo.NewOpenAPI(c.config.AppID, c.tokenSource).WithTimeout(5 * time.Second)

// 注册事件处理器
// register event handlers
intent := event.RegisterHandlers(
c.handleC2CMessage(),
c.handleGroupATMessage(),
)

// 获取 WebSocket 接入点
// get WebSocket endpoint
wsInfo, err := c.api.WS(c.ctx, nil, "")
if err != nil {
return fmt.Errorf("failed to get websocket info: %w", err)
Expand All @@ -81,10 +81,10 @@ func (c *QQChannel) Start(ctx context.Context) error {
"shards": wsInfo.Shards,
})

// 创建并保存 sessionManager
// create and save sessionManager
c.sessionManager = botgo.NewSessionManager()

// goroutine 中启动 WebSocket 连接,避免阻塞
// start WebSocket connection in goroutine to avoid blocking
go func() {
if err := c.sessionManager.Start(wsInfo, c.tokenSource, &intent); err != nil {
logger.ErrorCF("qq", "WebSocket session error", map[string]any{
Expand Down Expand Up @@ -116,12 +116,12 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
return fmt.Errorf("QQ bot not running")
}

// 构造消息
// construct message
msgToCreate := &dto.MessageToCreate{
Content: msg.Content,
}

// C2C 消息发送
// send C2C message
_, err := c.api.PostC2CMessage(ctx, msg.ChatID, msgToCreate)
if err != nil {
logger.ErrorCF("qq", "Failed to send C2C message", map[string]any{
Expand All @@ -133,15 +133,15 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
return nil
}

// handleC2CMessage 处理 QQ 私聊消息
// handleC2CMessage handles QQ private messages
func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
return func(event *dto.WSPayload, data *dto.WSC2CMessageData) error {
// 去重检查
// deduplication check
if c.isDuplicate(data.ID) {
return nil
}

// 提取用户信息
// extract user info
var senderID string
if data.Author != nil && data.Author.ID != "" {
senderID = data.Author.ID
Expand All @@ -150,7 +150,7 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
return nil
}

// 提取消息内容
// extract message content
content := data.Content
if content == "" {
logger.DebugC("qq", "Received empty message, ignoring")
Expand All @@ -162,7 +162,7 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
"length": len(content),
})

// 转发到消息总线
// forward to message bus
metadata := map[string]string{
"message_id": data.ID,
"peer_kind": "direct",
Expand All @@ -175,15 +175,15 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
}
}

// handleGroupATMessage 处理群@消息
// handleGroupATMessage handles group @messages
func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
return func(event *dto.WSPayload, data *dto.WSGroupATMessageData) error {
// 去重检查
// deduplication check
if c.isDuplicate(data.ID) {
return nil
}

// 提取用户信息
// extract user info
var senderID string
if data.Author != nil && data.Author.ID != "" {
senderID = data.Author.ID
Expand All @@ -192,7 +192,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
return nil
}

// 提取消息内容(去掉 @ 机器人部分)
// extract message content (remove @bot part)
content := data.Content
if content == "" {
logger.DebugC("qq", "Received empty group message, ignoring")
Expand All @@ -205,7 +205,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
"length": len(content),
})

// 转发到消息总线(使用 GroupID 作为 ChatID
// forward to message bus (use GroupID as ChatID)
metadata := map[string]string{
"message_id": data.ID,
"group_id": data.GroupID,
Expand All @@ -219,7 +219,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
}
}

// isDuplicate 检查消息是否重复
// isDuplicate checks if message is duplicate
func (c *QQChannel) isDuplicate(messageID string) bool {
c.mu.Lock()
defer c.mu.Unlock()
Expand All @@ -230,9 +230,9 @@ func (c *QQChannel) isDuplicate(messageID string) bool {

c.processedIDs[messageID] = true

// 简单清理:限制 map 大小
// simple cleanup: limit map size
if len(c.processedIDs) > 10000 {
// 清空一半
// clear half
count := 0
for id := range c.processedIDs {
if count >= 5000 {
Expand Down
6 changes: 3 additions & 3 deletions pkg/channels/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) {
return
}

// 检查白名单,避免为被拒绝的用户下载附件
// check allowlist to avoid downloading attachments for rejected users
if !c.IsAllowed(ev.User) {
logger.DebugCF("slack", "Message rejected by allowlist", map[string]any{
"user_id": ev.User,
Expand Down Expand Up @@ -232,9 +232,9 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) {
content = c.stripBotMention(content)

var mediaPaths []string
localFiles := []string{} // 跟踪需要清理的本地文件
localFiles := []string{} // track local files that need cleanup

// 确保临时文件在函数返回时被清理
// ensure temp files are cleaned up when function returns
defer func() {
for _, file := range localFiles {
if err := os.Remove(file); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions pkg/channels/telegram.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
senderID = fmt.Sprintf("%d|%s", user.ID, user.Username)
}

// 检查白名单,避免为被拒绝的用户下载附件
// check allowlist to avoid downloading attachments for rejected users
if !c.IsAllowed(senderID) {
logger.DebugCF("telegram", "Message rejected by allowlist", map[string]any{
"user_id": senderID,
Expand All @@ -221,9 +221,9 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes

content := ""
mediaPaths := []string{}
localFiles := []string{} // 跟踪需要清理的本地文件
localFiles := []string{} // track local files that need cleanup

// 确保临时文件在函数返回时被清理
// ensure temp files are cleaned up when function returns
defer func() {
for _, file := range localFiles {
if err := os.Remove(file); err != nil {
Expand Down
12 changes: 6 additions & 6 deletions pkg/skills/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ func (info SkillInfo) validate() error {

type SkillsLoader struct {
workspace string
workspaceSkills string // workspace skills (项目级别)
globalSkills string // 全局 skills (~/.picoclaw/skills)
builtinSkills string // 内置 skills
workspaceSkills string // workspace skills (project-level)
globalSkills string // global skills (~/.picoclaw/skills)
builtinSkills string // builtin skills
}

func NewSkillsLoader(workspace string, globalSkills string, builtinSkills string) *SkillsLoader {
Expand Down Expand Up @@ -120,23 +120,23 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo {
}

func (sl *SkillsLoader) LoadSkill(name string) (string, bool) {
// 1. 优先从 workspace skills 加载(项目级别)
// 1. load from workspace skills first (project-level)
if sl.workspaceSkills != "" {
skillFile := filepath.Join(sl.workspaceSkills, name, "SKILL.md")
if content, err := os.ReadFile(skillFile); err == nil {
return sl.stripFrontmatter(string(content)), true
}
}

// 2. 其次从全局 skills 加载 (~/.picoclaw/skills)
// 2. then load from global skills (~/.picoclaw/skills)
if sl.globalSkills != "" {
skillFile := filepath.Join(sl.globalSkills, name, "SKILL.md")
if content, err := os.ReadFile(skillFile); err == nil {
return sl.stripFrontmatter(string(content)), true
}
}

// 3. 最后从内置 skills 加载
// 3. finally load from builtin skills
if sl.builtinSkills != "" {
skillFile := filepath.Join(sl.builtinSkills, name, "SKILL.md")
if content, err := os.ReadFile(skillFile); err == nil {
Expand Down