From 15e3c7d08a1ff877e0d21ecc304e01e6fd96f138 Mon Sep 17 00:00:00 2001
From: seth <117851610+Sethispr@users.noreply.github.com>
Date: Fri, 13 Feb 2026 08:05:14 -0800
Subject: [PATCH 01/18] chore: lint readme
MD004 / ul-style Unordered list style [Expected: asterisk; Actual: dash]
MD009 / no-trailing-spaces Trailing spaces [Expected: 0 or 2; Actual: 1]
MD012 / no-multiple-blanks Multiple consecutive blank lines [Expected: 1; Actual: 2]
MD022 / blanks-around-headings Headings should be surrounded by blank lines [Expected: 1; Actual: 0; Below]
MD031 / blanks-around-fences Fenced code blocks should be surrounded by blank lines
MD032 / blanks-around-lists Lists should be surrounded by blank lines
---
README.md | 84 +++++++++++++++++++++++++++----------------------------
1 file changed, 41 insertions(+), 43 deletions(-)
diff --git a/README.md b/README.md
index 2ba70881b..81d155f89 100644
--- a/README.md
+++ b/README.md
@@ -17,14 +17,12 @@
[中文](README.zh.md) | [日本語](README.ja.md) | **English**
-
---
🦐 PicoClaw is an ultra-lightweight personal AI Assistant inspired by [nanobot](https://github.com/HKUDS/nanobot), refactored from the ground up in Go through a self-bootstrapping process, where the AI agent itself drove the entire architectural migration and code optimization.
⚡️ Runs on $10 hardware with <10MB RAM: That's 99% less memory than OpenClaw and 98% cheaper than a Mac mini!
-
-
> [!CAUTION]
> **🚨 SECURITY & OFFICIAL CHANNELS / 安全声明**
>
> * **NO CRYPTO:** PicoClaw has **NO** official token/coin. All claims on `pump.fun` or other trading platforms are **SCAMS**.
> * **OFFICIAL DOMAIN:** The **ONLY** official website is **[picoclaw.io](https://picoclaw.io)**, and company website is **[sipeed.com](https://sipeed.com)**
-> * **Warning:** Many `.ai/.org/.com/.net/...` domains are registered by third parties.
+> * **Warning:** Many `.ai/.org/.com/.net/...` domains are registered by third parties.
>
## 📢 News
+
2026-02-13 🎉 PicoClaw hit 5000 stars in 4days! Thank you for the community! There are so many PRs&issues come in (during Chinese New Year holidays), we are finalizing the Project Roadmap and setting up the Developer Group to accelerate PicoClaw's development.
🚀 Call to Action: Please submit your feature requests in GitHub Discussions. We will review and prioritize them during our upcoming weekly meeting.
-
2026-02-09 🎉 PicoClaw Launched! Built in 1 day to bring AI Agents to $10 hardware with <10MB RAM. 🦐 PicoClaw,Let's Go!
## ✨ Features
@@ -103,9 +100,9 @@
PicoClaw can be deployed on almost any Linux device!
-- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(Ethernet) or W(WiFi6) version, for Minimal Home Assistant
-- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), or $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) for Automated Server Maintenance
-- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) or $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) for Smart Monitoring
+* $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(Ethernet) or W(WiFi6) version, for Minimal Home Assistant
+* $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), or $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) for Automated Server Maintenance
+* $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) or $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) for Smart Monitoring
@@ -220,8 +217,8 @@ picoclaw onboard
**3. Get API Keys**
-- **LLM Provider**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
-- **Web Search** (optional): [Brave Search](https://brave.com/search/api) - Free tier available (2000 requests/month)
+* **LLM Provider**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
+* **Web Search** (optional): [Brave Search](https://brave.com/search/api) - Free tier available (2000 requests/month)
> **Note**: See `config.example.json` for a complete configuration template.
@@ -251,9 +248,9 @@ Talk to your picoclaw through Telegram, Discord, or DingTalk
**1. Create a bot**
-- Open Telegram, search `@BotFather`
-- Send `/newbot`, follow prompts
-- Copy the token
+* Open Telegram, search `@BotFather`
+* Send `/newbot`, follow prompts
+* Copy the token
**2. Configure**
@@ -284,19 +281,19 @@ picoclaw gateway
**1. Create a bot**
-- Go to
-- Create an application → Bot → Add Bot
-- Copy the bot token
+* Go to
+* Create an application → Bot → Add Bot
+* Copy the bot token
**2. Enable intents**
-- In the Bot settings, enable **MESSAGE CONTENT INTENT**
-- (Optional) Enable **SERVER MEMBERS INTENT** if you plan to use allow lists based on member data
+* In the Bot settings, enable **MESSAGE CONTENT INTENT**
+* (Optional) Enable **SERVER MEMBERS INTENT** if you plan to use allow lists based on member data
**3. Get your User ID**
-- Discord Settings → Advanced → enable **Developer Mode**
-- Right-click your avatar → **Copy User ID**
+* Discord Settings → Advanced → enable **Developer Mode**
+* Right-click your avatar → **Copy User ID**
**4. Configure**
@@ -314,10 +311,10 @@ picoclaw gateway
**5. Invite the bot**
-- OAuth2 → URL Generator
-- Scopes: `bot`
-- Bot Permissions: `Send Messages`, `Read Message History`
-- Open the generated invite URL and add the bot to your server
+* OAuth2 → URL Generator
+* Scopes: `bot`
+* Bot Permissions: `Send Messages`, `Read Message History`
+* Open the generated invite URL and add the bot to your server
**6. Run**
@@ -332,8 +329,8 @@ picoclaw gateway
**1. Create a bot**
-- Go to [QQ Open Platform](https://connect.qq.com/)
-- Create an application → Get **AppID** and **AppSecret**
+* Go to [QQ Open Platform](https://connect.qq.com/)
+* Create an application → Get **AppID** and **AppSecret**
**2. Configure**
@@ -365,9 +362,9 @@ picoclaw gateway
**1. Create a bot**
-- Go to [Open Platform](https://open.dingtalk.com/)
-- Create an internal app
-- Copy Client ID and Client Secret
+* Go to [Open Platform](https://open.dingtalk.com/)
+* Create an internal app
+* Copy Client ID and Client Secret
**2. Configure**
@@ -400,8 +397,6 @@ Connect Picoclaw to the Agent Social Network simply by sending a single message
**Read `https://clawdchat.ai/skill.md` and follow the instructions to join [ClawdChat.ai](https://clawdchat.ai)**
-
-
## ⚙️ Configuration
Config file: `~/.picoclaw/config.json`
@@ -464,12 +459,12 @@ When `restrict_to_workspace: true`, the following tools are sandboxed:
Even with `restrict_to_workspace: false`, the `exec` tool blocks these dangerous commands:
-- `rm -rf`, `del /f`, `rmdir /s` — Bulk deletion
-- `format`, `mkfs`, `diskpart` — Disk formatting
-- `dd if=` — Disk imaging
-- Writing to `/dev/sd[a-z]` — Direct disk writes
-- `shutdown`, `reboot`, `poweroff` — System shutdown
-- Fork bomb `:(){ :|:& };:`
+* `rm -rf`, `del /f`, `rmdir /s` — Bulk deletion
+* `format`, `mkfs`, `diskpart` — Disk formatting
+* `dd if=` — Disk imaging
+* Writing to `/dev/sd[a-z]` — Direct disk writes
+* `shutdown`, `reboot`, `poweroff` — System shutdown
+* Fork bomb `:(){ :|:& };:`
#### Error Examples
@@ -488,6 +483,7 @@ Even with `restrict_to_workspace: false`, the `exec` tool blocks these dangerous
If you need the agent to access paths outside the workspace:
**Method 1: Config file**
+
```json
{
"agents": {
@@ -499,6 +495,7 @@ If you need the agent to access paths outside the workspace:
```
**Method 2: Environment variable**
+
```bash
export PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE=false
```
@@ -590,8 +587,9 @@ The subagent has access to tools (message, web_search, etc.) and can communicate
| `interval` | `30` | Check interval in minutes (min: 5) |
**Environment variables:**
-- `PICOCLAW_HEARTBEAT_ENABLED=false` to disable
-- `PICOCLAW_HEARTBEAT_INTERVAL=60` to change interval
+
+* `PICOCLAW_HEARTBEAT_ENABLED=false` to disable
+* `PICOCLAW_HEARTBEAT_INTERVAL=60` to change interval
### Providers
@@ -613,7 +611,7 @@ The subagent has access to tools (message, web_search, etc.) and can communicate
**1. Get API key and base URL**
-- Get [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)
+* Get [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)
**2. Configure**
@@ -724,9 +722,9 @@ picoclaw agent -m "Hello"
PicoClaw supports scheduled reminders and recurring tasks through the `cron` tool:
-- **One-time reminders**: "Remind me in 10 minutes" → triggers once after 10min
-- **Recurring tasks**: "Remind me every 2 hours" → triggers every 2 hours
-- **Cron expressions**: "Remind me at 9am daily" → uses cron expression
+* **One-time reminders**: "Remind me in 10 minutes" → triggers once after 10min
+* **Recurring tasks**: "Remind me every 2 hours" → triggers every 2 hours
+* **Cron expressions**: "Remind me at 9am daily" → uses cron expression
Jobs are stored in `~/.picoclaw/workspace/cron/` and processed automatically.
From 7fa641a2edb41bd69268112d8683ded97a65a767 Mon Sep 17 00:00:00 2001
From: Hoshina
Date: Sat, 14 Feb 2026 16:50:21 +0800
Subject: [PATCH 02/18] feat: add OneBot channel support
---
config/config.example.json | 8 +
pkg/channels/manager.go | 13 +
pkg/channels/onebot.go | 677 +++++++++++++++++++++++++++++++++++++
pkg/config/config.go | 18 +
4 files changed, 716 insertions(+)
create mode 100644 pkg/channels/onebot.go
diff --git a/config/config.example.json b/config/config.example.json
index ee3ac97f7..f7e88357f 100644
--- a/config/config.example.json
+++ b/config/config.example.json
@@ -60,6 +60,14 @@
"webhook_port": 18791,
"webhook_path": "/webhook/line",
"allow_from": []
+ },
+ "onebot": {
+ "enabled": false,
+ "ws_url": "ws://127.0.0.1:3001",
+ "access_token": "",
+ "reconnect_interval": 5,
+ "group_trigger_prefix": [],
+ "allow_from": []
}
},
"providers": {
diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go
index 69e9b2b43..15f8c6037 100644
--- a/pkg/channels/manager.go
+++ b/pkg/channels/manager.go
@@ -163,6 +163,19 @@ func (m *Manager) initChannels() error {
}
}
+ if m.config.Channels.OneBot.Enabled && m.config.Channels.OneBot.WSUrl != "" {
+ logger.DebugC("channels", "Attempting to initialize OneBot channel")
+ onebot, err := NewOneBotChannel(m.config.Channels.OneBot, m.bus)
+ if err != nil {
+ logger.ErrorCF("channels", "Failed to initialize OneBot channel", map[string]interface{}{
+ "error": err.Error(),
+ })
+ } else {
+ m.channels["onebot"] = onebot
+ logger.InfoC("channels", "OneBot channel enabled successfully")
+ }
+ }
+
logger.InfoCF("channels", "Channel initialization completed", map[string]interface{}{
"enabled_channels": len(m.channels),
})
diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go
new file mode 100644
index 000000000..fe6edbeea
--- /dev/null
+++ b/pkg/channels/onebot.go
@@ -0,0 +1,677 @@
+package channels
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gorilla/websocket"
+
+ "github.com/sipeed/picoclaw/pkg/bus"
+ "github.com/sipeed/picoclaw/pkg/config"
+ "github.com/sipeed/picoclaw/pkg/logger"
+)
+
+type OneBotChannel struct {
+ *BaseChannel
+ config config.OneBotConfig
+ conn *websocket.Conn
+ ctx context.Context
+ cancel context.CancelFunc
+ dedup map[string]struct{}
+ dedupRing []string
+ dedupIdx int
+ mu sync.Mutex
+ writeMu sync.Mutex
+ echoCounter int64
+}
+
+
+type oneBotRawEvent struct {
+ PostType string `json:"post_type"`
+ MessageType string `json:"message_type"`
+ SubType string `json:"sub_type"`
+ MessageID json.RawMessage `json:"message_id"`
+ UserID json.RawMessage `json:"user_id"`
+ GroupID json.RawMessage `json:"group_id"`
+ RawMessage string `json:"raw_message"`
+ Message json.RawMessage `json:"message"`
+ Sender json.RawMessage `json:"sender"`
+ SelfID json.RawMessage `json:"self_id"`
+ Time json.RawMessage `json:"time"`
+ MetaEventType string `json:"meta_event_type"`
+ Echo string `json:"echo"`
+ RetCode json.RawMessage `json:"retcode"`
+ Status string `json:"status"`
+}
+
+type oneBotSender struct {
+ UserID json.RawMessage `json:"user_id"`
+ Nickname string `json:"nickname"`
+ Card string `json:"card"`
+}
+
+type oneBotEvent struct {
+ PostType string
+ MessageType string
+ SubType string
+ MessageID string
+ UserID int64
+ GroupID int64
+ Content string
+ RawContent string
+ IsBotMentioned bool
+ Sender oneBotSender
+ SelfID int64
+ Time int64
+ MetaEventType string
+}
+
+type oneBotAPIRequest struct {
+ Action string `json:"action"`
+ Params interface{} `json:"params"`
+ Echo string `json:"echo,omitempty"`
+}
+
+type oneBotSendPrivateMsgParams struct {
+ UserID int64 `json:"user_id"`
+ Message string `json:"message"`
+}
+
+type oneBotSendGroupMsgParams struct {
+ GroupID int64 `json:"group_id"`
+ Message string `json:"message"`
+}
+
+func NewOneBotChannel(cfg config.OneBotConfig, messageBus *bus.MessageBus) (*OneBotChannel, error) {
+ base := NewBaseChannel("onebot", cfg, messageBus, cfg.AllowFrom)
+
+ const dedupSize = 1024
+ return &OneBotChannel{
+ BaseChannel: base,
+ config: cfg,
+ dedup: make(map[string]struct{}, dedupSize),
+ dedupRing: make([]string, dedupSize),
+ dedupIdx: 0,
+ }, nil
+}
+
+func (c *OneBotChannel) Start(ctx context.Context) error {
+ if c.config.WSUrl == "" {
+ return fmt.Errorf("OneBot ws_url not configured")
+ }
+
+ logger.InfoCF("onebot", "Starting OneBot channel", map[string]interface{}{
+ "ws_url": c.config.WSUrl,
+ })
+
+ c.ctx, c.cancel = context.WithCancel(ctx)
+
+ if err := c.connect(); err != nil {
+ return fmt.Errorf("failed to connect to OneBot: %w", err)
+ }
+
+ go c.listen()
+
+ if c.config.ReconnectInterval > 0 {
+ go c.reconnectLoop()
+ }
+
+ c.setRunning(true)
+ logger.InfoC("onebot", "OneBot channel started successfully")
+
+ return nil
+}
+
+func (c *OneBotChannel) connect() error {
+ dialer := websocket.DefaultDialer
+ dialer.HandshakeTimeout = 10 * time.Second
+
+ header := make(map[string][]string)
+ if c.config.AccessToken != "" {
+ header["Authorization"] = []string{"Bearer " + c.config.AccessToken}
+ }
+
+ conn, _, err := dialer.Dial(c.config.WSUrl, header)
+ if err != nil {
+ return err
+ }
+
+ c.mu.Lock()
+ c.conn = conn
+ c.mu.Unlock()
+
+ logger.InfoC("onebot", "WebSocket connected")
+ return nil
+}
+
+func (c *OneBotChannel) reconnectLoop() {
+ interval := time.Duration(c.config.ReconnectInterval) * time.Second
+ if interval < 5*time.Second {
+ interval = 5 * time.Second
+ }
+
+ for {
+ select {
+ case <-c.ctx.Done():
+ return
+ case <-time.After(interval):
+ c.mu.Lock()
+ conn := c.conn
+ c.mu.Unlock()
+
+ if conn == nil {
+ logger.InfoC("onebot", "Attempting to reconnect...")
+ if err := c.connect(); err != nil {
+ logger.ErrorCF("onebot", "Reconnect failed", map[string]interface{}{
+ "error": err.Error(),
+ })
+ } else {
+ go c.listen()
+ }
+ }
+ }
+ }
+}
+
+func (c *OneBotChannel) Stop(ctx context.Context) error {
+ logger.InfoC("onebot", "Stopping OneBot channel")
+ c.setRunning(false)
+
+ if c.cancel != nil {
+ c.cancel()
+ }
+
+ c.mu.Lock()
+ if c.conn != nil {
+ c.conn.Close()
+ c.conn = nil
+ }
+ c.mu.Unlock()
+
+ return nil
+}
+
+func (c *OneBotChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
+ if !c.IsRunning() {
+ return fmt.Errorf("OneBot channel not running")
+ }
+
+ c.mu.Lock()
+ conn := c.conn
+ c.mu.Unlock()
+
+ if conn == nil {
+ return fmt.Errorf("OneBot WebSocket not connected")
+ }
+
+ action, params, err := c.buildSendRequest(msg)
+ if err != nil {
+ return err
+ }
+
+ c.writeMu.Lock()
+ c.echoCounter++
+ echo := fmt.Sprintf("send_%d", c.echoCounter)
+ c.writeMu.Unlock()
+
+ req := oneBotAPIRequest{
+ Action: action,
+ Params: params,
+ Echo: echo,
+ }
+
+ data, err := json.Marshal(req)
+ if err != nil {
+ return fmt.Errorf("failed to marshal OneBot request: %w", err)
+ }
+
+ c.writeMu.Lock()
+ err = conn.WriteMessage(websocket.TextMessage, data)
+ c.writeMu.Unlock()
+
+ if err != nil {
+ logger.ErrorCF("onebot", "Failed to send message", map[string]interface{}{
+ "error": err.Error(),
+ })
+ return err
+ }
+
+ return nil
+}
+
+func (c *OneBotChannel) buildSendRequest(msg bus.OutboundMessage) (string, interface{}, error) {
+ chatID := msg.ChatID
+
+ if len(chatID) > 6 && chatID[:6] == "group:" {
+ groupID, err := strconv.ParseInt(chatID[6:], 10, 64)
+ if err != nil {
+ return "", nil, fmt.Errorf("invalid group ID in chatID: %s", chatID)
+ }
+ return "send_group_msg", oneBotSendGroupMsgParams{
+ GroupID: groupID,
+ Message: msg.Content,
+ }, nil
+ }
+
+ if len(chatID) > 8 && chatID[:8] == "private:" {
+ userID, err := strconv.ParseInt(chatID[8:], 10, 64)
+ if err != nil {
+ return "", nil, fmt.Errorf("invalid user ID in chatID: %s", chatID)
+ }
+ return "send_private_msg", oneBotSendPrivateMsgParams{
+ UserID: userID,
+ Message: msg.Content,
+ }, nil
+ }
+
+ userID, err := strconv.ParseInt(chatID, 10, 64)
+ if err != nil {
+ return "", nil, fmt.Errorf("invalid chatID for OneBot: %s", chatID)
+ }
+
+ return "send_private_msg", oneBotSendPrivateMsgParams{
+ UserID: userID,
+ Message: msg.Content,
+ }, nil
+}
+
+func (c *OneBotChannel) listen() {
+ logger.InfoC("onebot", "WebSocket listener started")
+ for {
+ select {
+ case <-c.ctx.Done():
+ logger.InfoC("onebot", "WebSocket listener stopping (context cancelled)")
+ return
+ default:
+ c.mu.Lock()
+ conn := c.conn
+ c.mu.Unlock()
+
+ if conn == nil {
+ logger.WarnC("onebot", "WebSocket connection is nil, listener exiting")
+ return
+ }
+
+ _, message, err := conn.ReadMessage()
+ if err != nil {
+ logger.ErrorCF("onebot", "WebSocket read error", map[string]interface{}{
+ "error": err.Error(),
+ })
+ c.mu.Lock()
+ if c.conn != nil {
+ c.conn.Close()
+ c.conn = nil
+ }
+ c.mu.Unlock()
+ return
+ }
+
+ logger.DebugCF("onebot", "Raw WebSocket message received", map[string]interface{}{
+ "length": len(message),
+ "payload": string(message),
+ })
+
+ var raw oneBotRawEvent
+ if err := json.Unmarshal(message, &raw); err != nil {
+ logger.WarnCF("onebot", "Failed to unmarshal raw event", map[string]interface{}{
+ "error": err.Error(),
+ "payload": string(message),
+ })
+ continue
+ }
+
+ if raw.Echo != "" || raw.Status != "" {
+ logger.DebugCF("onebot", "Received API response, skipping", map[string]interface{}{
+ "echo": raw.Echo,
+ "status": raw.Status,
+ })
+ continue
+ }
+
+ logger.DebugCF("onebot", "Parsed raw event", map[string]interface{}{
+ "post_type": raw.PostType,
+ "message_type": raw.MessageType,
+ "sub_type": raw.SubType,
+ "meta_event_type": raw.MetaEventType,
+ })
+
+ c.handleRawEvent(&raw)
+ }
+ }
+}
+
+func parseJSONInt64(raw json.RawMessage) (int64, error) {
+ if len(raw) == 0 {
+ return 0, nil
+ }
+
+ var n int64
+ if err := json.Unmarshal(raw, &n); err == nil {
+ return n, nil
+ }
+
+ var s string
+ if err := json.Unmarshal(raw, &s); err == nil {
+ return strconv.ParseInt(s, 10, 64)
+ }
+ return 0, fmt.Errorf("cannot parse as int64: %s", string(raw))
+}
+
+func parseJSONString(raw json.RawMessage) string {
+ if len(raw) == 0 {
+ return ""
+ }
+ var s string
+ if err := json.Unmarshal(raw, &s); err == nil {
+ return s
+ }
+
+ return string(raw)
+}
+
+type parseMessageResult struct {
+ Text string
+ IsBotMentioned bool
+}
+
+func parseMessageContentEx(raw json.RawMessage, selfID int64) parseMessageResult {
+ if len(raw) == 0 {
+ return parseMessageResult{}
+ }
+
+ var s string
+ if err := json.Unmarshal(raw, &s); err == nil {
+ mentioned := false
+ if selfID > 0 {
+ cqAt := fmt.Sprintf("[CQ:at,qq=%d]", selfID)
+ if strings.Contains(s, cqAt) {
+ mentioned = true
+ s = strings.ReplaceAll(s, cqAt, "")
+ s = strings.TrimSpace(s)
+ }
+ }
+ return parseMessageResult{Text: s, IsBotMentioned: mentioned}
+ }
+
+ var segments []map[string]interface{}
+ if err := json.Unmarshal(raw, &segments); err == nil {
+ var text string
+ mentioned := false
+ selfIDStr := strconv.FormatInt(selfID, 10)
+ for _, seg := range segments {
+ segType, _ := seg["type"].(string)
+ data, _ := seg["data"].(map[string]interface{})
+ switch segType {
+ case "text":
+ if data != nil {
+ if t, ok := data["text"].(string); ok {
+ text += t
+ }
+ }
+ case "at":
+ if data != nil && selfID > 0 {
+ qqVal := fmt.Sprintf("%v", data["qq"])
+ if qqVal == selfIDStr || qqVal == "all" {
+ mentioned = true
+ }
+ }
+ }
+ }
+ return parseMessageResult{Text: strings.TrimSpace(text), IsBotMentioned: mentioned}
+ }
+ return parseMessageResult{}
+}
+
+func (c *OneBotChannel) handleRawEvent(raw *oneBotRawEvent) {
+ switch raw.PostType {
+ case "message":
+ evt, err := c.normalizeMessageEvent(raw)
+ if err != nil {
+ logger.WarnCF("onebot", "Failed to normalize message event", map[string]interface{}{
+ "error": err.Error(),
+ })
+ return
+ }
+ c.handleMessage(evt)
+ case "meta_event":
+ c.handleMetaEvent(raw)
+ case "notice":
+ logger.DebugCF("onebot", "Notice event received", map[string]interface{}{
+ "sub_type": raw.SubType,
+ })
+ case "request":
+ logger.DebugCF("onebot", "Request event received", map[string]interface{}{
+ "sub_type": raw.SubType,
+ })
+ case "":
+ logger.DebugCF("onebot", "Event with empty post_type (possibly API response)", map[string]interface{}{
+ "echo": raw.Echo,
+ "status": raw.Status,
+ })
+ default:
+ logger.DebugCF("onebot", "Unknown post_type", map[string]interface{}{
+ "post_type": raw.PostType,
+ })
+ }
+}
+
+func (c *OneBotChannel) normalizeMessageEvent(raw *oneBotRawEvent) (*oneBotEvent, error) {
+ userID, err := parseJSONInt64(raw.UserID)
+ if err != nil {
+ return nil, fmt.Errorf("parse user_id: %w (raw: %s)", err, string(raw.UserID))
+ }
+
+ groupID, _ := parseJSONInt64(raw.GroupID)
+ selfID, _ := parseJSONInt64(raw.SelfID)
+ ts, _ := parseJSONInt64(raw.Time)
+ messageID := parseJSONString(raw.MessageID)
+
+ parsed := parseMessageContentEx(raw.Message, selfID)
+ isBotMentioned := parsed.IsBotMentioned
+
+ content := raw.RawMessage
+ if content == "" {
+ content = parsed.Text
+ } else if selfID > 0 {
+ cqAt := fmt.Sprintf("[CQ:at,qq=%d]", selfID)
+ if strings.Contains(content, cqAt) {
+ isBotMentioned = true
+ content = strings.ReplaceAll(content, cqAt, "")
+ content = strings.TrimSpace(content)
+ }
+ }
+
+ var sender oneBotSender
+ if len(raw.Sender) > 0 {
+ if err := json.Unmarshal(raw.Sender, &sender); err != nil {
+ logger.WarnCF("onebot", "Failed to parse sender", map[string]interface{}{
+ "error": err.Error(),
+ "sender": string(raw.Sender),
+ })
+ }
+ }
+
+ logger.DebugCF("onebot", "Normalized message event", map[string]interface{}{
+ "message_type": raw.MessageType,
+ "user_id": userID,
+ "group_id": groupID,
+ "message_id": messageID,
+ "content_len": len(content),
+ "nickname": sender.Nickname,
+ })
+
+ return &oneBotEvent{
+ PostType: raw.PostType,
+ MessageType: raw.MessageType,
+ SubType: raw.SubType,
+ MessageID: messageID,
+ UserID: userID,
+ GroupID: groupID,
+ Content: content,
+ RawContent: raw.RawMessage,
+ IsBotMentioned: isBotMentioned,
+ Sender: sender,
+ SelfID: selfID,
+ Time: ts,
+ MetaEventType: raw.MetaEventType,
+ }, nil
+}
+
+func (c *OneBotChannel) handleMetaEvent(raw *oneBotRawEvent) {
+ switch raw.MetaEventType {
+ case "lifecycle":
+ logger.InfoCF("onebot", "Lifecycle event", map[string]interface{}{
+ "sub_type": raw.SubType,
+ })
+ case "heartbeat":
+ logger.DebugC("onebot", "Heartbeat received")
+ default:
+ logger.DebugCF("onebot", "Unknown meta_event_type", map[string]interface{}{
+ "meta_event_type": raw.MetaEventType,
+ })
+ }
+}
+
+func (c *OneBotChannel) handleMessage(evt *oneBotEvent) {
+ if c.isDuplicate(evt.MessageID) {
+ logger.DebugCF("onebot", "Duplicate message, skipping", map[string]interface{}{
+ "message_id": evt.MessageID,
+ })
+ return
+ }
+
+ content := evt.Content
+ if content == "" {
+ logger.DebugCF("onebot", "Received empty message, ignoring", map[string]interface{}{
+ "message_id": evt.MessageID,
+ })
+ return
+ }
+
+ senderID := strconv.FormatInt(evt.UserID, 10)
+ var chatID string
+
+ metadata := map[string]string{
+ "message_id": evt.MessageID,
+ }
+
+ switch evt.MessageType {
+ case "private":
+ chatID = "private:" + senderID
+ logger.InfoCF("onebot", "Received private message", map[string]interface{}{
+ "sender": senderID,
+ "message_id": evt.MessageID,
+ "length": len(content),
+ "content": truncate(content, 100),
+ })
+
+ case "group":
+ groupIDStr := strconv.FormatInt(evt.GroupID, 10)
+ chatID = "group:" + groupIDStr
+ metadata["group_id"] = groupIDStr
+
+ senderUserID, _ := parseJSONInt64(evt.Sender.UserID)
+ if senderUserID > 0 {
+ metadata["sender_user_id"] = strconv.FormatInt(senderUserID, 10)
+ }
+
+ if evt.Sender.Card != "" {
+ metadata["sender_name"] = evt.Sender.Card
+ } else if evt.Sender.Nickname != "" {
+ metadata["sender_name"] = evt.Sender.Nickname
+ }
+
+ triggered, strippedContent := c.checkGroupTrigger(content, evt.IsBotMentioned)
+ if !triggered {
+ logger.DebugCF("onebot", "Group message ignored (no trigger)", map[string]interface{}{
+ "sender": senderID,
+ "group": groupIDStr,
+ "is_mentioned": evt.IsBotMentioned,
+ "content": truncate(content, 100),
+ })
+ return
+ }
+ content = strippedContent
+
+ logger.InfoCF("onebot", "Received group message", map[string]interface{}{
+ "sender": senderID,
+ "group": groupIDStr,
+ "message_id": evt.MessageID,
+ "is_mentioned": evt.IsBotMentioned,
+ "length": len(content),
+ "content": truncate(content, 100),
+ })
+
+ default:
+ logger.WarnCF("onebot", "Unknown message type, cannot route", map[string]interface{}{
+ "type": evt.MessageType,
+ "message_id": evt.MessageID,
+ "user_id": evt.UserID,
+ })
+ return
+ }
+
+ if evt.Sender.Nickname != "" {
+ metadata["nickname"] = evt.Sender.Nickname
+ }
+
+ logger.DebugCF("onebot", "Forwarding message to bus", map[string]interface{}{
+ "sender_id": senderID,
+ "chat_id": chatID,
+ "content": truncate(content, 100),
+ })
+
+ c.HandleMessage(senderID, chatID, content, []string{}, metadata)
+}
+
+func (c *OneBotChannel) isDuplicate(messageID string) bool {
+ if messageID == "" || messageID == "0" {
+ return false
+ }
+
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ if _, exists := c.dedup[messageID]; exists {
+ return true
+ }
+
+ if old := c.dedupRing[c.dedupIdx]; old != "" {
+ delete(c.dedup, old)
+ }
+ c.dedupRing[c.dedupIdx] = messageID
+ c.dedup[messageID] = struct{}{}
+ c.dedupIdx = (c.dedupIdx + 1) % len(c.dedupRing)
+
+ return false
+}
+
+func truncate(s string, n int) string {
+ runes := []rune(s)
+ if len(runes) <= n {
+ return s
+ }
+ return string(runes[:n]) + "..."
+}
+
+func (c *OneBotChannel) checkGroupTrigger(content string, isBotMentioned bool) (triggered bool, strippedContent string) {
+ if isBotMentioned {
+ return true, strings.TrimSpace(content)
+ }
+
+ for _, prefix := range c.config.GroupTriggerPrefix {
+ if prefix == "" {
+ continue
+ }
+ if strings.HasPrefix(content, prefix) {
+ return true, strings.TrimSpace(strings.TrimPrefix(content, prefix))
+ }
+ }
+
+ return false, content
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 6af9438a5..03dfe3133 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -77,6 +77,7 @@ type ChannelsConfig struct {
DingTalk DingTalkConfig `json:"dingtalk"`
Slack SlackConfig `json:"slack"`
LINE LINEConfig `json:"line"`
+ OneBot OneBotConfig `json:"onebot"`
}
type WhatsAppConfig struct {
@@ -145,6 +146,15 @@ type LINEConfig struct {
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
}
+type OneBotConfig struct {
+ Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
+ WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
+ AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
+ ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
+ GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
+ AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
+}
+
type HeartbeatConfig struct {
Enabled bool `json:"enabled" env:"PICOCLAW_HEARTBEAT_ENABLED"`
Interval int `json:"interval" env:"PICOCLAW_HEARTBEAT_INTERVAL"` // minutes, min 5
@@ -265,6 +275,14 @@ func DefaultConfig() *Config {
WebhookPath: "/webhook/line",
AllowFrom: FlexibleStringSlice{},
},
+ OneBot: OneBotConfig{
+ Enabled: false,
+ WSUrl: "ws://127.0.0.1:3001",
+ AccessToken: "",
+ ReconnectInterval: 5,
+ GroupTriggerPrefix: []string{},
+ AllowFrom: FlexibleStringSlice{},
+ },
},
Providers: ProvidersConfig{
Anthropic: ProviderConfig{},
From 1fcf3fde0f6e3d89ad753f05e413ab82ee7e62ee Mon Sep 17 00:00:00 2001
From: Hoshina
Date: Sat, 14 Feb 2026 17:23:18 +0800
Subject: [PATCH 03/18] style: fix format
---
pkg/channels/onebot.go | 55 +++++++++++++++++++++---------------------
1 file changed, 27 insertions(+), 28 deletions(-)
diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go
index fe6edbeea..7f48ef6c0 100644
--- a/pkg/channels/onebot.go
+++ b/pkg/channels/onebot.go
@@ -18,19 +18,18 @@ import (
type OneBotChannel struct {
*BaseChannel
- config config.OneBotConfig
- conn *websocket.Conn
- ctx context.Context
- cancel context.CancelFunc
- dedup map[string]struct{}
- dedupRing []string
- dedupIdx int
- mu sync.Mutex
- writeMu sync.Mutex
- echoCounter int64
+ config config.OneBotConfig
+ conn *websocket.Conn
+ ctx context.Context
+ cancel context.CancelFunc
+ dedup map[string]struct{}
+ dedupRing []string
+ dedupIdx int
+ mu sync.Mutex
+ writeMu sync.Mutex
+ echoCounter int64
}
-
type oneBotRawEvent struct {
PostType string `json:"post_type"`
MessageType string `json:"message_type"`
@@ -44,9 +43,9 @@ type oneBotRawEvent struct {
SelfID json.RawMessage `json:"self_id"`
Time json.RawMessage `json:"time"`
MetaEventType string `json:"meta_event_type"`
- Echo string `json:"echo"`
- RetCode json.RawMessage `json:"retcode"`
- Status string `json:"status"`
+ Echo string `json:"echo"`
+ RetCode json.RawMessage `json:"retcode"`
+ Status string `json:"status"`
}
type oneBotSender struct {
@@ -56,19 +55,19 @@ type oneBotSender struct {
}
type oneBotEvent struct {
- PostType string
- MessageType string
- SubType string
- MessageID string
- UserID int64
- GroupID int64
- Content string
- RawContent string
- IsBotMentioned bool
- Sender oneBotSender
- SelfID int64
- Time int64
- MetaEventType string
+ PostType string
+ MessageType string
+ SubType string
+ MessageID string
+ UserID int64
+ GroupID int64
+ Content string
+ RawContent string
+ IsBotMentioned bool
+ Sender oneBotSender
+ SelfID int64
+ Time int64
+ MetaEventType string
}
type oneBotAPIRequest struct {
@@ -264,7 +263,7 @@ func (c *OneBotChannel) buildSendRequest(msg bus.OutboundMessage) (string, inter
return "", nil, fmt.Errorf("invalid user ID in chatID: %s", chatID)
}
return "send_private_msg", oneBotSendPrivateMsgParams{
- UserID: userID,
+ UserID: userID,
Message: msg.Content,
}, nil
}
From f7421128a0074fe7577ada0614a2eb58afe3d082 Mon Sep 17 00:00:00 2001
From: Hoshina
Date: Sat, 14 Feb 2026 17:46:47 +0800
Subject: [PATCH 04/18] style: remove unnecessary log messages in WebSocket
listener
---
pkg/channels/onebot.go | 2 --
1 file changed, 2 deletions(-)
diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go
index 7f48ef6c0..ae563b831 100644
--- a/pkg/channels/onebot.go
+++ b/pkg/channels/onebot.go
@@ -280,11 +280,9 @@ func (c *OneBotChannel) buildSendRequest(msg bus.OutboundMessage) (string, inter
}
func (c *OneBotChannel) listen() {
- logger.InfoC("onebot", "WebSocket listener started")
for {
select {
case <-c.ctx.Done():
- logger.InfoC("onebot", "WebSocket listener stopping (context cancelled)")
return
default:
c.mu.Lock()
From 773ce9bcb6ba6a9cd59d0ff51053b2e31d610912 Mon Sep 17 00:00:00 2001
From: Hoshina
Date: Sat, 14 Feb 2026 19:58:19 +0800
Subject: [PATCH 05/18] fix: update BotStatus type in oneBotRawEvent and adjust
status checks
---
pkg/channels/onebot.go | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go
index ae563b831..dadc9bc02 100644
--- a/pkg/channels/onebot.go
+++ b/pkg/channels/onebot.go
@@ -45,7 +45,12 @@ type oneBotRawEvent struct {
MetaEventType string `json:"meta_event_type"`
Echo string `json:"echo"`
RetCode json.RawMessage `json:"retcode"`
- Status string `json:"status"`
+ Status BotStatus `json:"status"`
+}
+
+type BotStatus struct {
+ Online bool `json:"online"`
+ Good bool `json:"good"`
}
type oneBotSender struct {
@@ -322,7 +327,7 @@ func (c *OneBotChannel) listen() {
continue
}
- if raw.Echo != "" || raw.Status != "" {
+ if raw.Echo != "" || raw.Status.Online || raw.Status.Good {
logger.DebugCF("onebot", "Received API response, skipping", map[string]interface{}{
"echo": raw.Echo,
"status": raw.Status,
From 60c3d96b5e5dbe155e2e4fdafea0a25bd78579fe Mon Sep 17 00:00:00 2001
From: Hoshina
Date: Sat, 14 Feb 2026 20:02:30 +0800
Subject: [PATCH 06/18] style: fix alignment of fields in BotStatus struct
---
pkg/channels/onebot.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go
index dadc9bc02..674acf520 100644
--- a/pkg/channels/onebot.go
+++ b/pkg/channels/onebot.go
@@ -50,7 +50,7 @@ type oneBotRawEvent struct {
type BotStatus struct {
Online bool `json:"online"`
- Good bool `json:"good"`
+ Good bool `json:"good"`
}
type oneBotSender struct {
From 8cff6cf3123c71bacf0ee534352a3cac55e2b3f0 Mon Sep 17 00:00:00 2001
From: Hoshina
Date: Sat, 14 Feb 2026 20:25:55 +0800
Subject: [PATCH 07/18] fix: improve connection handling in OneBotChannel
---
pkg/channels/onebot.go | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go
index 674acf520..5d97fab9c 100644
--- a/pkg/channels/onebot.go
+++ b/pkg/channels/onebot.go
@@ -116,13 +116,20 @@ func (c *OneBotChannel) Start(ctx context.Context) error {
c.ctx, c.cancel = context.WithCancel(ctx)
if err := c.connect(); err != nil {
- return fmt.Errorf("failed to connect to OneBot: %w", err)
+ logger.WarnCF("onebot", "Initial connection failed, will retry in background", map[string]interface{}{
+ "error": err.Error(),
+ })
+ } else {
+ go c.listen()
}
- go c.listen()
-
if c.config.ReconnectInterval > 0 {
go c.reconnectLoop()
+ } else {
+ // If reconnect is disabled but initial connection failed, we cannot recover
+ if c.conn == nil {
+ return fmt.Errorf("failed to connect to OneBot and reconnect is disabled")
+ }
}
c.setRunning(true)
From a24cbd43855b19c4bb7defe1774e6dc17ff4c6db Mon Sep 17 00:00:00 2001
From: Artem Yadelskyi
Date: Sat, 14 Feb 2026 14:44:19 +0200
Subject: [PATCH 08/18] feat(docker): Added curl for Docker image
---
.gitignore | 6 +++++-
Dockerfile | 4 ++--
go.mod | 2 +-
go.sum | 4 ++--
4 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/.gitignore b/.gitignore
index 6ba411758..6a61e26b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,4 +35,8 @@ coverage.html
# Ralph workspace
ralph/
.ralph/
-tasks/
\ No newline at end of file
+tasks/
+
+# Editors
+.vscode/
+.idea/
diff --git a/Dockerfile b/Dockerfile
index 8db995545..5587198fe 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,9 +18,9 @@ RUN make build
# ============================================================
# Stage 2: Minimal runtime image
# ============================================================
-FROM alpine:3.21
+FROM alpine:3.23
-RUN apk add --no-cache ca-certificates tzdata
+RUN apk add --no-cache ca-certificates tzdata curl
# Copy binary
COPY --from=builder /src/build/picoclaw /usr/local/bin/picoclaw
diff --git a/go.mod b/go.mod
index f4c233ea8..78683203f 100644
--- a/go.mod
+++ b/go.mod
@@ -13,7 +13,7 @@ require (
github.com/larksuite/oapi-sdk-go/v3 v3.5.3
github.com/mymmrac/telego v1.6.0
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1
- github.com/openai/openai-go/v3 v3.21.0
+ github.com/openai/openai-go/v3 v3.22.0
github.com/slack-go/slack v0.17.3
github.com/tencent-connect/botgo v0.2.1
golang.org/x/oauth2 v0.35.0
diff --git a/go.sum b/go.sum
index 9174d2889..80e40a33f 100644
--- a/go.sum
+++ b/go.sum
@@ -92,8 +92,8 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 h1:Lb/Uzkiw2Ugt2Xf03J5wmv81PdkYOiWbI8CNBi1boC8=
github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU=
-github.com/openai/openai-go/v3 v3.21.0 h1:3GpIR/W4q/v1uUOVuK3zYtQiF3DnRrZag/sxbtvEdtc=
-github.com/openai/openai-go/v3 v3.21.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
+github.com/openai/openai-go/v3 v3.22.0 h1:6MEoNoV8sbjOVmXdvhmuX3BjVbVdcExbVyGixiyJ8ys=
+github.com/openai/openai-go/v3 v3.22.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
From 5faa67b77d9ceba411eb8de2627a43319a17b3dd Mon Sep 17 00:00:00 2001
From: Lixeer <1612655510@qq.com>
Date: Sun, 15 Feb 2026 05:23:42 +0800
Subject: [PATCH 09/18] feat: add `Github Copilot` provider
---
go.mod | 5 ++
go.sum | 4 ++
pkg/config/config.go | 4 ++
pkg/providers/github_copilot_provider.go | 85 ++++++++++++++++++++++++
4 files changed, 98 insertions(+)
create mode 100644 pkg/providers/github_copilot_provider.go
diff --git a/go.mod b/go.mod
index f4c233ea8..206f1aafd 100644
--- a/go.mod
+++ b/go.mod
@@ -19,12 +19,16 @@ require (
golang.org/x/oauth2 v0.35.0
)
+
+
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
+ github.com/github/copilot-sdk/go v0.1.23
+ github.com/google/jsonschema-go v0.4.2 // indirect
github.com/go-resty/resty/v2 v2.17.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/grbit/go-json v0.11.0 // indirect
@@ -43,4 +47,5 @@ require (
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
+
)
diff --git a/go.sum b/go.sum
index 9174d2889..8f9575122 100644
--- a/go.sum
+++ b/go.sum
@@ -32,6 +32,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/github/copilot-sdk/go v0.1.23 h1:uExtO/inZQndCZMiSAA1hvXINiz9tqo/MZgQzFzurxw=
+github.com/github/copilot-sdk/go v0.1.23/go.mod h1:GdwwBfMbm9AABLEM3x5IZKw4ZfwCYxZ1BgyytmZenQ0=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-resty/resty/v2 v2.6.0/go.mod h1:PwvJS6hvaPkjtjNg9ph+VrSD92bi5Zq73w/BIH7cC3Q=
github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4=
@@ -56,6 +58,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
+github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
diff --git a/pkg/config/config.go b/pkg/config/config.go
index bbfa2e40b..eafdb006e 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -168,6 +168,7 @@ type ProvidersConfig struct {
Moonshot ProviderConfig `json:"moonshot"`
ShengSuanYun ProviderConfig `json:"shengsuanyun"`
DeepSeek ProviderConfig `json:"deepseek"`
+ GitHubCopilot ProviderConfig `json:"github_copilot"`
}
type ProviderConfig struct {
@@ -175,8 +176,11 @@ type ProviderConfig struct {
APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
+ ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
}
+
+
type GatewayConfig struct {
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
diff --git a/pkg/providers/github_copilot_provider.go b/pkg/providers/github_copilot_provider.go
new file mode 100644
index 000000000..67fbdd568
--- /dev/null
+++ b/pkg/providers/github_copilot_provider.go
@@ -0,0 +1,85 @@
+package providers
+
+import (
+ "context"
+ "fmt"
+
+ json "encoding/json"
+ copilot "github.com/github/copilot-sdk/go"
+)
+
+type GitHubCopilotProvider struct {
+ uri string
+ connectMode string // `stdio` or `grpc``
+
+ session *copilot.Session
+}
+
+func NewGitHubCopilotProvider(uri string, connectMode string, model string) *GitHubCopilotProvider {
+
+ var session *copilot.Session
+
+ switch connectMode {
+
+ case "stdio":
+
+ case "grpc":
+ client := copilot.NewClient(&copilot.ClientOptions{
+ CLIUrl: uri,
+ })
+ if err := client.Start(context.Background()); err != nil {
+ fmt.Errorf("Can't connect to Github Copilot, https://github.com/github/copilot-sdk/blob/main/docs/getting-started.md#connecting-to-an-external-cli-server for details")
+ }
+ defer client.Stop()
+ session, _ = client.CreateSession(context.Background(), &copilot.SessionConfig{
+ Model: model,
+ Hooks: &copilot.SessionHooks{
+
+ },
+ })
+ }
+
+ return &GitHubCopilotProvider{
+ uri: uri,
+ connectMode: connectMode,
+
+ session: session,
+ }
+}
+
+// Chat sends a chat request to GitHub Copilot
+func (p *GitHubCopilotProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
+ type tempMessage struct {
+ Role string `json:"role"`
+ Content string `json:"content"`
+ }
+ out := make([]tempMessage, 0, len(messages))
+
+ for _, msg := range messages {
+ out = append(out, tempMessage{
+ Role: msg.Role,
+ Content: msg.Content,
+ })
+ }
+
+ fullcontent,_ := json.Marshal(out)
+
+
+
+
+ content,_ := p.session.Send(ctx,copilot.MessageOptions{
+ Prompt: string(fullcontent),
+ })
+
+ return &LLMResponse{
+ FinishReason : "stop",
+ Content: content,
+ }, nil
+
+}
+
+
+func (p *GitHubCopilotProvider) GetDefaultModel() string {
+
+ return "gpt-4.1"
+}
From 0aab8d8afcb69659c663dbcbff2fd0ec77cb4208 Mon Sep 17 00:00:00 2001
From: Lixeer <1612655510@qq.com>
Date: Sun, 15 Feb 2026 06:10:00 +0800
Subject: [PATCH 10/18] feat:add `github_copilot` to providers factory
---
pkg/providers/http_provider.go | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go
index 6fcbd3055..013f787d1 100644
--- a/pkg/providers/http_provider.go
+++ b/pkg/providers/http_provider.go
@@ -314,7 +314,17 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
model = "deepseek-chat"
}
}
+ case "github_copilot", "copilot":
+ if cfg.Providers.GitHubCopilot.APIBase != "" {
+ apiBase = cfg.Providers.GitHubCopilot.APIBase
+ }else {
+ apiBase = "localhost:4321" // no `http://` beacause grpc mode`
+ }
+ return NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model), nil
+
}
+
+
}
// Fallback: detect provider from model name
From 6c392c338758053b928a92bff86769fbd6160c3a Mon Sep 17 00:00:00 2001
From: Lixeer <1612655510@qq.com>
Date: Sun, 15 Feb 2026 06:11:07 +0800
Subject: [PATCH 11/18] fix: openclaw crash because of empty config
---
pkg/providers/github_copilot_provider.go | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/pkg/providers/github_copilot_provider.go b/pkg/providers/github_copilot_provider.go
index 67fbdd568..4774ac703 100644
--- a/pkg/providers/github_copilot_provider.go
+++ b/pkg/providers/github_copilot_provider.go
@@ -4,8 +4,10 @@ import (
"context"
"fmt"
- json "encoding/json"
+ json "encoding/json"
+
copilot "github.com/github/copilot-sdk/go"
+
)
type GitHubCopilotProvider struct {
@@ -18,7 +20,9 @@ type GitHubCopilotProvider struct {
func NewGitHubCopilotProvider(uri string, connectMode string, model string) *GitHubCopilotProvider {
var session *copilot.Session
-
+ if connectMode == "" {
+ connectMode = "grpc"
+ }
switch connectMode {
case "stdio":
@@ -37,6 +41,7 @@ func NewGitHubCopilotProvider(uri string, connectMode string, model string) *Git
},
})
+
}
return &GitHubCopilotProvider{
@@ -66,10 +71,11 @@ func (p *GitHubCopilotProvider) Chat(ctx context.Context, messages []Message, to
-
+ fmt.Println("[DEBUG] call p.session.Send")
content,_ := p.session.Send(ctx,copilot.MessageOptions{
Prompt: string(fullcontent),
})
+ fmt.Println("[DEBUG] end cal")
return &LLMResponse{
FinishReason : "stop",
From ecbe31599efae9f622a6c4a502503be005b59eab Mon Sep 17 00:00:00 2001
From: Lixeer <1612655510@qq.com>
Date: Sun, 15 Feb 2026 06:30:31 +0800
Subject: [PATCH 12/18] chore: remove redundant debug output
---
pkg/providers/github_copilot_provider.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/providers/github_copilot_provider.go b/pkg/providers/github_copilot_provider.go
index 4774ac703..a6cd20d9f 100644
--- a/pkg/providers/github_copilot_provider.go
+++ b/pkg/providers/github_copilot_provider.go
@@ -71,11 +71,11 @@ func (p *GitHubCopilotProvider) Chat(ctx context.Context, messages []Message, to
- fmt.Println("[DEBUG] call p.session.Send")
+
content,_ := p.session.Send(ctx,copilot.MessageOptions{
Prompt: string(fullcontent),
})
- fmt.Println("[DEBUG] end cal")
+
return &LLMResponse{
FinishReason : "stop",
From 7a9659971df856f9dd91b302dcb33ebde5d0e4a3 Mon Sep 17 00:00:00 2001
From: Lixeer <1612655510@qq.com>
Date: Sun, 15 Feb 2026 06:55:20 +0800
Subject: [PATCH 13/18] make fmt
---
pkg/config/config.go | 34 ++++++------
pkg/providers/github_copilot_provider.go | 66 +++++++++++-------------
pkg/providers/http_provider.go | 9 ++--
3 files changed, 49 insertions(+), 60 deletions(-)
diff --git a/pkg/config/config.go b/pkg/config/config.go
index eafdb006e..b6d4e0b7f 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -157,30 +157,28 @@ type DevicesConfig struct {
}
type ProvidersConfig struct {
- Anthropic ProviderConfig `json:"anthropic"`
- OpenAI ProviderConfig `json:"openai"`
- OpenRouter ProviderConfig `json:"openrouter"`
- Groq ProviderConfig `json:"groq"`
- Zhipu ProviderConfig `json:"zhipu"`
- VLLM ProviderConfig `json:"vllm"`
- Gemini ProviderConfig `json:"gemini"`
- Nvidia ProviderConfig `json:"nvidia"`
- Moonshot ProviderConfig `json:"moonshot"`
- ShengSuanYun ProviderConfig `json:"shengsuanyun"`
- DeepSeek ProviderConfig `json:"deepseek"`
+ Anthropic ProviderConfig `json:"anthropic"`
+ OpenAI ProviderConfig `json:"openai"`
+ OpenRouter ProviderConfig `json:"openrouter"`
+ Groq ProviderConfig `json:"groq"`
+ Zhipu ProviderConfig `json:"zhipu"`
+ VLLM ProviderConfig `json:"vllm"`
+ Gemini ProviderConfig `json:"gemini"`
+ Nvidia ProviderConfig `json:"nvidia"`
+ Moonshot ProviderConfig `json:"moonshot"`
+ ShengSuanYun ProviderConfig `json:"shengsuanyun"`
+ DeepSeek ProviderConfig `json:"deepseek"`
GitHubCopilot ProviderConfig `json:"github_copilot"`
}
type ProviderConfig struct {
- APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
- APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
- Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
- AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
- ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
+ APIKey string `json:"api_key" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_KEY"`
+ APIBase string `json:"api_base" env:"PICOCLAW_PROVIDERS_{{.Name}}_API_BASE"`
+ Proxy string `json:"proxy,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_PROXY"`
+ AuthMethod string `json:"auth_method,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_AUTH_METHOD"`
+ ConnectMode string `json:"connect_mode,omitempty" env:"PICOCLAW_PROVIDERS_{{.Name}}_CONNECT_MODE"` //only for Github Copilot, `stdio` or `grpc`
}
-
-
type GatewayConfig struct {
Host string `json:"host" env:"PICOCLAW_GATEWAY_HOST"`
Port int `json:"port" env:"PICOCLAW_GATEWAY_PORT"`
diff --git a/pkg/providers/github_copilot_provider.go b/pkg/providers/github_copilot_provider.go
index a6cd20d9f..39a3f7e9f 100644
--- a/pkg/providers/github_copilot_provider.go
+++ b/pkg/providers/github_copilot_provider.go
@@ -7,21 +7,20 @@ import (
json "encoding/json"
copilot "github.com/github/copilot-sdk/go"
-
)
type GitHubCopilotProvider struct {
uri string
connectMode string // `stdio` or `grpc``
-
- session *copilot.Session
+
+ session *copilot.Session
}
func NewGitHubCopilotProvider(uri string, connectMode string, model string) *GitHubCopilotProvider {
var session *copilot.Session
- if connectMode == "" {
- connectMode = "grpc"
+ if connectMode == "" {
+ connectMode = "grpc"
}
switch connectMode {
@@ -37,55 +36,48 @@ func NewGitHubCopilotProvider(uri string, connectMode string, model string) *Git
defer client.Stop()
session, _ = client.CreateSession(context.Background(), &copilot.SessionConfig{
Model: model,
- Hooks: &copilot.SessionHooks{
-
- },
+ Hooks: &copilot.SessionHooks{},
})
-
+
}
return &GitHubCopilotProvider{
uri: uri,
connectMode: connectMode,
-
- session: session,
+
+ session: session,
}
}
// Chat sends a chat request to GitHub Copilot
func (p *GitHubCopilotProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
- type tempMessage struct {
- Role string `json:"role"`
- Content string `json:"content"`
- }
- out := make([]tempMessage, 0, len(messages))
+ type tempMessage struct {
+ Role string `json:"role"`
+ Content string `json:"content"`
+ }
+ out := make([]tempMessage, 0, len(messages))
- for _, msg := range messages {
- out = append(out, tempMessage{
- Role: msg.Role,
- Content: msg.Content,
- })
- }
+ for _, msg := range messages {
+ out = append(out, tempMessage{
+ Role: msg.Role,
+ Content: msg.Content,
+ })
+ }
- fullcontent,_ := json.Marshal(out)
-
+ fullcontent, _ := json.Marshal(out)
+ content, _ := p.session.Send(ctx, copilot.MessageOptions{
+ Prompt: string(fullcontent),
+ })
+
+ return &LLMResponse{
+ FinishReason: "stop",
+ Content: content,
+ }, nil
-
- content,_ := p.session.Send(ctx,copilot.MessageOptions{
- Prompt: string(fullcontent),
- })
-
-
- return &LLMResponse{
- FinishReason : "stop",
- Content: content,
- }, nil
-
}
-
func (p *GitHubCopilotProvider) GetDefaultModel() string {
- return "gpt-4.1"
+ return "gpt-4.1"
}
diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go
index 013f787d1..1bd440fc0 100644
--- a/pkg/providers/http_provider.go
+++ b/pkg/providers/http_provider.go
@@ -317,13 +317,12 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
case "github_copilot", "copilot":
if cfg.Providers.GitHubCopilot.APIBase != "" {
apiBase = cfg.Providers.GitHubCopilot.APIBase
- }else {
- apiBase = "localhost:4321" // no `http://` beacause grpc mode`
- }
+ } else {
+ apiBase = "localhost:4321" // no `http://` beacause grpc mode`
+ }
return NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model), nil
-
+
}
-
}
From f6d6221c09d67ce3c395c9281b17d8547cc61bbb Mon Sep 17 00:00:00 2001
From: Lixeer <1612655510@qq.com>
Date: Sun, 15 Feb 2026 07:00:40 +0800
Subject: [PATCH 14/18] make fmt
---
pkg/providers/http_provider.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go
index 1bd440fc0..d723ba872 100644
--- a/pkg/providers/http_provider.go
+++ b/pkg/providers/http_provider.go
@@ -318,7 +318,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
if cfg.Providers.GitHubCopilot.APIBase != "" {
apiBase = cfg.Providers.GitHubCopilot.APIBase
} else {
- apiBase = "localhost:4321" // no `http://` beacause grpc mode`
+ apiBase = "localhost:4321"
}
return NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model), nil
From 5a6ad37dab6edf3068fea334c407ad234dee8382 Mon Sep 17 00:00:00 2001
From: Lixeer <1612655510@qq.com>
Date: Sun, 15 Feb 2026 07:34:12 +0800
Subject: [PATCH 15/18] code fmt
---
cmd/picoclaw/main.go | 66 +++++++++++++++++-----------------
pkg/providers/http_provider.go | 2 +-
2 files changed, 33 insertions(+), 35 deletions(-)
diff --git a/cmd/picoclaw/main.go b/cmd/picoclaw/main.go
index 191ff8239..e424fb7d3 100644
--- a/cmd/picoclaw/main.go
+++ b/cmd/picoclaw/main.go
@@ -9,6 +9,7 @@ package main
import (
"bufio"
"context"
+ "embed"
"fmt"
"io"
"io/fs"
@@ -18,7 +19,6 @@ import (
"runtime"
"strings"
"time"
- "embed"
"github.com/chzyer/readline"
"github.com/sipeed/picoclaw/pkg/agent"
@@ -41,7 +41,6 @@ import (
//go:embed workspace
var embeddedFiles embed.FS
-
var (
version = "dev"
gitCommit string
@@ -214,7 +213,6 @@ func printHelp() {
fmt.Println(" version Show version information")
}
-
func onboard() {
configPath := getConfigPath()
@@ -246,50 +244,50 @@ func onboard() {
}
func copyEmbeddedToTarget(targetDir string) error {
- // Ensure target directory exists
- if err := os.MkdirAll(targetDir, 0755); err != nil {
- return fmt.Errorf("Failed to create target directory: %w", err)
- }
+ // Ensure target directory exists
+ if err := os.MkdirAll(targetDir, 0755); err != nil {
+ return fmt.Errorf("Failed to create target directory: %w", err)
+ }
- // Walk through all files in embed.FS
- err := fs.WalkDir(embeddedFiles, "workspace", func(path string, d fs.DirEntry, err error) error {
- if err != nil {
- return err
- }
+ // Walk through all files in embed.FS
+ err := fs.WalkDir(embeddedFiles, "workspace", func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
- // Skip directories
- if d.IsDir() {
- return nil
- }
+ // Skip directories
+ if d.IsDir() {
+ return nil
+ }
- // Read embedded file
- data, err := embeddedFiles.ReadFile(path)
- if err != nil {
- return fmt.Errorf("Failed to read embedded file %s: %w", path, err)
- }
+ // Read embedded file
+ data, err := embeddedFiles.ReadFile(path)
+ if err != nil {
+ return fmt.Errorf("Failed to read embedded file %s: %w", path, err)
+ }
new_path, err := filepath.Rel("workspace", path)
if err != nil {
return fmt.Errorf("Failed to get relative path for %s: %v\n", path, err)
}
- // Build target file path
- targetPath := filepath.Join(targetDir, new_path)
+ // Build target file path
+ targetPath := filepath.Join(targetDir, new_path)
- // Ensure target file's directory exists
- if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
- return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err)
- }
+ // Ensure target file's directory exists
+ if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
+ return fmt.Errorf("Failed to create directory %s: %w", filepath.Dir(targetPath), err)
+ }
- // Write file
- if err := os.WriteFile(targetPath, data, 0644); err != nil {
- return fmt.Errorf("Failed to write file %s: %w", targetPath, err)
- }
+ // Write file
+ if err := os.WriteFile(targetPath, data, 0644); err != nil {
+ return fmt.Errorf("Failed to write file %s: %w", targetPath, err)
+ }
- return nil
- })
+ return nil
+ })
- return err
+ return err
}
func createWorkspaceTemplates(workspace string) {
diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go
index d723ba872..52a380239 100644
--- a/pkg/providers/http_provider.go
+++ b/pkg/providers/http_provider.go
@@ -318,7 +318,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
if cfg.Providers.GitHubCopilot.APIBase != "" {
apiBase = cfg.Providers.GitHubCopilot.APIBase
} else {
- apiBase = "localhost:4321"
+ apiBase = "localhost:4321"
}
return NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model), nil
From 0a4bf32e81fb8bb93b78c76b5dbbce5a84cb8b30 Mon Sep 17 00:00:00 2001
From: Lixeer <1612655510@qq.com>
Date: Sun, 15 Feb 2026 07:54:04 +0800
Subject: [PATCH 16/18] try after workflow fix
---
pkg/providers/github_copilot_provider.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/providers/github_copilot_provider.go b/pkg/providers/github_copilot_provider.go
index 39a3f7e9f..ec9b1b68d 100644
--- a/pkg/providers/github_copilot_provider.go
+++ b/pkg/providers/github_copilot_provider.go
@@ -25,7 +25,7 @@ func NewGitHubCopilotProvider(uri string, connectMode string, model string) *Git
switch connectMode {
case "stdio":
-
+ //todo
case "grpc":
client := copilot.NewClient(&copilot.ClientOptions{
CLIUrl: uri,
From 4b886b65739a91a771a38d793b89b2c2c250a50f Mon Sep 17 00:00:00 2001
From: Lixeer <1612655510@qq.com>
Date: Sun, 15 Feb 2026 08:02:23 +0800
Subject: [PATCH 17/18] fix: not used result
---
pkg/providers/github_copilot_provider.go | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/pkg/providers/github_copilot_provider.go b/pkg/providers/github_copilot_provider.go
index ec9b1b68d..5058819f5 100644
--- a/pkg/providers/github_copilot_provider.go
+++ b/pkg/providers/github_copilot_provider.go
@@ -16,7 +16,7 @@ type GitHubCopilotProvider struct {
session *copilot.Session
}
-func NewGitHubCopilotProvider(uri string, connectMode string, model string) *GitHubCopilotProvider {
+func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*GitHubCopilotProvider, error) {
var session *copilot.Session
if connectMode == "" {
@@ -31,7 +31,7 @@ func NewGitHubCopilotProvider(uri string, connectMode string, model string) *Git
CLIUrl: uri,
})
if err := client.Start(context.Background()); err != nil {
- fmt.Errorf("Can't connect to Github Copilot, https://github.com/github/copilot-sdk/blob/main/docs/getting-started.md#connecting-to-an-external-cli-server for details")
+ return nil, fmt.Errorf("Can't connect to Github Copilot, https://github.com/github/copilot-sdk/blob/main/docs/getting-started.md#connecting-to-an-external-cli-server for details")
}
defer client.Stop()
session, _ = client.CreateSession(context.Background(), &copilot.SessionConfig{
@@ -44,9 +44,8 @@ func NewGitHubCopilotProvider(uri string, connectMode string, model string) *Git
return &GitHubCopilotProvider{
uri: uri,
connectMode: connectMode,
-
- session: session,
- }
+ session: session,
+ }, nil
}
// Chat sends a chat request to GitHub Copilot
From 16e5a0295386c034b8812c16831dda17b7a209b1 Mon Sep 17 00:00:00 2001
From: Lixeer <1612655510@qq.com>
Date: Sun, 15 Feb 2026 08:06:29 +0800
Subject: [PATCH 18/18] fix(http_provider): Remove extra parameter from
CreateProvider function.
---
pkg/providers/http_provider.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go
index 52a380239..0bea16d6a 100644
--- a/pkg/providers/http_provider.go
+++ b/pkg/providers/http_provider.go
@@ -320,7 +320,7 @@ func CreateProvider(cfg *config.Config) (LLMProvider, error) {
} else {
apiBase = "localhost:4321"
}
- return NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model), nil
+ return NewGitHubCopilotProvider(apiBase, cfg.Providers.GitHubCopilot.ConnectMode, model)
}