center">
-
-
- |
-
- |
-
-
- |
-
-
-> 📋 **[Hardware Compatibility List](docs/hardware-compatibility.md)** — See all tested boards, from $5 RISC-V to Raspberry Pi to Android phones. Your board not listed? Submit a PR!
-
-## 🦾 Demonstration
-
-### 🛠️ Standard Assistant Workflows
-
-🧩 Full-Stack Engineer |
- 🗂️ Logging & Planning Management |
- 🔎 Web Search & Learning |
-
|---|---|---|
|
-
|
-
|
-
| Develop • Deploy • Scale | -Schedule • Automate • Memory | -Discovery • Insights • Trends | -
+PicoClaw stores data in your configured workspace (default: `~/.picoclaw/workspace`):
-### 🐜 Innovative Low-Footprint Deploy
+```
+~/.picoclaw/workspace/
+├── sessions/ # Conversation sessions and history
+├── memory/ # Long-term memory (MEMORY.md)
+├── state/ # Persistent state (last channel, etc.)
+├── cron/ # Scheduled jobs database
+├── skills/ # Workspace-specific skills
+├── AGENT.md # Structured agent definition and system prompt
+├── SOUL.md # Agent soul
+├── USER.md # User profile and preferences for this workspace
+├── HEARTBEAT.md # Periodic task prompts (checked every 30 min)
+└── ...
+```
-PicoClaw can be deployed on almost any Linux device!
+### Skill Sources
-- $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
+By default, skills are loaded from:
-
-
-##
+WeChat:
+
diff --git a/README.zh.md b/README.zh.md
index db34f57d..1bc5d1a4 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -218,6 +218,7 @@ make install
| 命令 | 说明 |
| ------------------------- | ---------------------- |
| `picoclaw onboard` | 初始化配置与工作区 |
+| `picoclaw onboard weixin` | 扫码连接微信个人号 |
| `picoclaw agent -m "..."` | 与 Agent 对话 |
| `picoclaw agent` | 交互式对话模式 |
| `picoclaw gateway` | 启动网关 |
diff --git a/assets/wechat.png b/assets/wechat.png
index 6512421e..effb4dab 100644
Binary files a/assets/wechat.png and b/assets/wechat.png differ
diff --git a/cmd/picoclaw/internal/agent/helpers.go b/cmd/picoclaw/internal/agent/helpers.go
index c3ddbb77..0af743bb 100644
--- a/cmd/picoclaw/internal/agent/helpers.go
+++ b/cmd/picoclaw/internal/agent/helpers.go
@@ -23,16 +23,16 @@ func agentCmd(message, sessionKey, model string, debug bool) error {
sessionKey = "cli:default"
}
- if debug {
- logger.SetLevel(logger.DEBUG)
- fmt.Println("🔍 Debug mode enabled")
- }
-
cfg, err := internal.LoadConfig()
if err != nil {
return fmt.Errorf("error loading config: %w", err)
}
+ if debug {
+ logger.SetLevel(logger.DEBUG)
+ fmt.Println("🔍 Debug mode enabled")
+ }
+
if model != "" {
cfg.Agents.Defaults.ModelName = model
}
diff --git a/cmd/picoclaw/internal/helpers.go b/cmd/picoclaw/internal/helpers.go
index 6b2d65c9..ae1d58c2 100644
--- a/cmd/picoclaw/internal/helpers.go
+++ b/cmd/picoclaw/internal/helpers.go
@@ -5,6 +5,7 @@ import (
"path/filepath"
"github.com/sipeed/picoclaw/pkg/config"
+ "github.com/sipeed/picoclaw/pkg/logger"
)
const Logo = "🦞"
@@ -27,7 +28,12 @@ func GetConfigPath() string {
}
func LoadConfig() (*config.Config, error) {
- return config.LoadConfig(GetConfigPath())
+ cfg, err := config.LoadConfig(GetConfigPath())
+ if err != nil {
+ return nil, err
+ }
+ logger.SetLevelFromString(cfg.Agents.Defaults.LogLevel)
+ return cfg, nil
}
// FormatVersion returns the version string with optional git commit
diff --git a/cmd/picoclaw/internal/onboard/command.go b/cmd/picoclaw/internal/onboard/command.go
index 9f8b288c..1f94c671 100644
--- a/cmd/picoclaw/internal/onboard/command.go
+++ b/cmd/picoclaw/internal/onboard/command.go
@@ -16,14 +16,22 @@ func NewOnboardCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "onboard",
Aliases: []string{"o"},
- Short: "Initialize picoclaw configuration and workspace",
+ Short: "Initialize picoclaw configuration, workspace, and channel accounts",
+ // Run without subcommands → original onboard flow
Run: func(cmd *cobra.Command, args []string) {
- onboard(encrypt)
+ if len(args) == 0 {
+ onboard(encrypt)
+ } else {
+ _ = cmd.Help()
+ }
},
}
cmd.Flags().BoolVar(&encrypt, "enc", false,
"Enable credential encryption (generates SSH key and prompts for passphrase)")
+ // Channel onboarding subcommands
+ cmd.AddCommand(newWeixinCommand())
+
return cmd
}
diff --git a/cmd/picoclaw/internal/onboard/command_test.go b/cmd/picoclaw/internal/onboard/command_test.go
index 56936190..6b9fb6e9 100644
--- a/cmd/picoclaw/internal/onboard/command_test.go
+++ b/cmd/picoclaw/internal/onboard/command_test.go
@@ -13,7 +13,7 @@ func TestNewOnboardCommand(t *testing.T) {
require.NotNil(t, cmd)
assert.Equal(t, "onboard", cmd.Use)
- assert.Equal(t, "Initialize picoclaw configuration and workspace", cmd.Short)
+ assert.Equal(t, "Initialize picoclaw configuration, workspace, and channel accounts", cmd.Short)
assert.Len(t, cmd.Aliases, 1)
assert.True(t, cmd.HasAlias("o"))
@@ -28,5 +28,6 @@ func TestNewOnboardCommand(t *testing.T) {
encFlag := cmd.Flags().Lookup("enc")
require.NotNil(t, encFlag, "expected --enc flag to be registered")
assert.Equal(t, "false", encFlag.DefValue, "--enc should default to false")
- assert.False(t, cmd.HasSubCommands())
+ assert.True(t, cmd.HasSubCommands())
+ assert.NotNil(t, cmd.Commands())
}
diff --git a/cmd/picoclaw/internal/onboard/helpers_test.go b/cmd/picoclaw/internal/onboard/helpers_test.go
index f3e0c92e..23fc97c5 100644
--- a/cmd/picoclaw/internal/onboard/helpers_test.go
+++ b/cmd/picoclaw/internal/onboard/helpers_test.go
@@ -6,20 +6,32 @@ import (
"testing"
)
-func TestCopyEmbeddedToTargetUsesAgentsMarkdown(t *testing.T) {
+func TestCopyEmbeddedToTargetUsesStructuredAgentFiles(t *testing.T) {
targetDir := t.TempDir()
if err := copyEmbeddedToTarget(targetDir); err != nil {
t.Fatalf("copyEmbeddedToTarget() error = %v", err)
}
- agentsPath := filepath.Join(targetDir, "AGENTS.md")
- if _, err := os.Stat(agentsPath); err != nil {
- t.Fatalf("expected %s to exist: %v", agentsPath, err)
+ agentPath := filepath.Join(targetDir, "AGENT.md")
+ if _, err := os.Stat(agentPath); err != nil {
+ t.Fatalf("expected %s to exist: %v", agentPath, err)
}
- legacyPath := filepath.Join(targetDir, "AGENT.md")
- if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
- t.Fatalf("expected legacy file %s to be absent, got err=%v", legacyPath, err)
+ soulPath := filepath.Join(targetDir, "SOUL.md")
+ if _, err := os.Stat(soulPath); err != nil {
+ t.Fatalf("expected %s to exist: %v", soulPath, err)
+ }
+
+ userPath := filepath.Join(targetDir, "USER.md")
+ if _, err := os.Stat(userPath); err != nil {
+ t.Fatalf("expected %s to exist: %v", userPath, err)
+ }
+
+ for _, legacyName := range []string{"AGENTS.md", "IDENTITY.md"} {
+ legacyPath := filepath.Join(targetDir, legacyName)
+ if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
+ t.Fatalf("expected legacy file %s to be absent, got err=%v", legacyPath, err)
+ }
}
}
diff --git a/cmd/picoclaw/internal/onboard/weixin.go b/cmd/picoclaw/internal/onboard/weixin.go
new file mode 100644
index 00000000..721b4f0e
--- /dev/null
+++ b/cmd/picoclaw/internal/onboard/weixin.go
@@ -0,0 +1,124 @@
+package onboard
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/spf13/cobra"
+
+ "github.com/sipeed/picoclaw/cmd/picoclaw/internal"
+ "github.com/sipeed/picoclaw/pkg/channels/weixin"
+ "github.com/sipeed/picoclaw/pkg/config"
+)
+
+func newWeixinCommand() *cobra.Command {
+ var baseURL string
+ var proxy string
+ var timeout int
+
+ cmd := &cobra.Command{
+ Use: "weixin",
+ Short: "Connect a WeChat personal account via QR code",
+ Long: `Start the interactive Weixin (WeChat personal) QR code login flow.
+
+A QR code is displayed in the terminal. Scan it with the WeChat mobile app
+to authorize your account. On success, the bot token is saved to the picoclaw
+config so you can start the gateway immediately.
+
+Example:
+ picoclaw onboard weixin`,
+ RunE: func(cmd *cobra.Command, _ []string) error {
+ return runWeixinOnboard(baseURL, proxy, time.Duration(timeout)*time.Second)
+ },
+ }
+
+ cmd.Flags().StringVar(&baseURL, "base-url", "https://ilinkai.weixin.qq.com/", "iLink API base URL")
+ cmd.Flags().StringVar(&proxy, "proxy", "", "HTTP proxy URL (e.g. http://localhost:7890)")
+ cmd.Flags().IntVar(&timeout, "timeout", 300, "Login timeout in seconds")
+
+ return cmd
+}
+
+func runWeixinOnboard(baseURL, proxy string, timeout time.Duration) error {
+ fmt.Println("Starting Weixin (WeChat personal) login...")
+ fmt.Println()
+
+ botToken, userID, accountID, returnedBaseURL, err := weixin.PerformLoginInteractive(
+ context.Background(),
+ weixin.AuthFlowOpts{
+ BaseURL: baseURL,
+ Timeout: timeout,
+ Proxy: proxy,
+ },
+ )
+ if err != nil {
+ return fmt.Errorf("login failed: %w", err)
+ }
+
+ fmt.Println()
+ fmt.Println("✅ Login successful!")
+ fmt.Printf(" Account ID : %s\n", accountID)
+ if userID != "" {
+ fmt.Printf(" User ID : %s\n", userID)
+ }
+ fmt.Println()
+
+ // Prefer the server-returned base URL (may be region-specific)
+ effectiveBaseURL := returnedBaseURL
+ if effectiveBaseURL == "" {
+ effectiveBaseURL = baseURL
+ }
+
+ if err := saveWeixinConfig(botToken, effectiveBaseURL, proxy); err != nil {
+ fmt.Printf("⚠️ Could not auto-save to config: %v\n", err)
+ printManualWeixinConfig(botToken, effectiveBaseURL)
+ return nil
+ }
+
+ fmt.Println("✓ Config updated. Start the gateway with:")
+ fmt.Println()
+ fmt.Println(" picoclaw gateway")
+ fmt.Println()
+ fmt.Println("To restrict which WeChat users can send messages, add their user IDs")
+ fmt.Println("to channels.weixin.allow_from in your config.")
+
+ return nil
+}
+
+// saveWeixinConfig patches channels.weixin in the config and saves it.
+func saveWeixinConfig(token, baseURL, proxy string) error {
+ cfgPath := internal.GetConfigPath()
+
+ cfg, err := config.LoadConfig(cfgPath)
+ if err != nil {
+ return fmt.Errorf("failed to load config: %w", err)
+ }
+
+ cfg.Channels.Weixin.Enabled = true
+ cfg.Channels.Weixin.Token = token
+ const defaultBase = "https://ilinkai.weixin.qq.com/"
+ if baseURL != "" && baseURL != defaultBase {
+ cfg.Channels.Weixin.BaseURL = baseURL
+ }
+ if proxy != "" {
+ cfg.Channels.Weixin.Proxy = proxy
+ }
+
+ return config.SaveConfig(cfgPath, cfg)
+}
+
+func printManualWeixinConfig(token, baseURL string) {
+ fmt.Println()
+ fmt.Println("Add the following to the channels section of your picoclaw config:")
+ fmt.Println()
+ fmt.Println(` "weixin": {`)
+ fmt.Println(` "enabled": true,`)
+ fmt.Printf(" \"token\": %q,\n", token)
+ const defaultBase = "https://ilinkai.weixin.qq.com/"
+ if baseURL != "" && baseURL != defaultBase {
+ fmt.Printf(" \"base_url\": %q,\n", baseURL)
+ }
+ fmt.Println(` "allow_from": []`)
+ fmt.Println(` }`)
+}
diff --git a/config/config.example.json b/config/config.example.json
index 81c9014e..28b29dfa 100644
--- a/config/config.example.json
+++ b/config/config.example.json
@@ -1,10 +1,12 @@
{
"agents": {
"defaults": {
+ "log_level": "fatal",
"workspace": "~/.picoclaw/workspace",
"restrict_to_workspace": true,
"model_name": "gpt-5.4",
"max_tokens": 8192,
+ "context_window": 131072,
"temperature": 0.7,
"max_tool_iterations": 20,
"summarize_message_threshold": 20,
@@ -548,6 +550,14 @@
"voice": {
"echo_transcription": false
},
+ "hooks": {
+ "enabled": true,
+ "defaults": {
+ "observer_timeout_ms": 500,
+ "interceptor_timeout_ms": 5000,
+ "approval_timeout_ms": 60000
+ }
+ },
"gateway": {
"host": "127.0.0.1",
"port": 18790,
diff --git a/docs/agent-refactor/context.md b/docs/agent-refactor/context.md
new file mode 100644
index 00000000..2269d925
--- /dev/null
+++ b/docs/agent-refactor/context.md
@@ -0,0 +1,164 @@
+# Context
+
+## What this document covers
+
+This document makes explicit the boundaries of context management in the agent loop:
+
+- what fills the context window and how space is divided
+- what is stored in session history vs. built at request time
+- when and how context compression happens
+- how token budgets are estimated
+
+These are existing concepts. This document clarifies their boundaries rather than introducing new ones.
+
+---
+
+## Context window regions
+
+The context window is the model's total input capacity. Four regions fill it:
+
+| Region | Assembled by | Stored in session? |
+|---|---|---|
+| System prompt | `BuildMessages()` — static + dynamic parts | No |
+| Summary | `SetSummary()` stores it; `BuildMessages()` injects it | Separate from history |
+| Session history | User / assistant / tool messages | Yes |
+| Tool definitions | Provider adapter injects at call time | No |
+
+`MaxTokens` (the output generation limit) must also be reserved from the total budget.
+
+The available space for history is therefore:
+
+```
+history_budget = ContextWindow - system_prompt - summary - tool_definitions - MaxTokens
+```
+
+---
+
+## ContextWindow vs MaxTokens
+
+These serve different purposes:
+
+- **MaxTokens** — maximum tokens the LLM may generate in one response. Sent as the `max_tokens` request parameter.
+- **ContextWindow** — the model's total input context capacity.
+
+These were previously set to the same value, which caused the summarization threshold to fire either far too early (at the default 32K) or not at all (when a user raised `max_tokens`).
+
+Current default when not explicitly configured: `ContextWindow = MaxTokens * 4`.
+
+---
+
+## Session history
+
+Session history stores only conversation messages:
+
+- `user` — user input
+- `assistant` — LLM response (may include `ToolCalls`)
+- `tool` — tool execution results
+
+Session history does **not** contain:
+
+- System prompts — assembled at request time by `BuildMessages`
+- Summary content — stored separately via `SetSummary`, injected by `BuildMessages`
+
+This distinction matters: any code that operates on session history — compression, boundary detection, token estimation — must not assume a system message is present.
+
+---
+
+## Turn
+
+A **Turn** is one complete cycle:
+
+> user message -> LLM iterations (possibly including tool calls) -> final assistant response
+
+This definition comes from the agent loop design (#1316). In session history, Turn boundaries are identified by `user`-role messages.
+
+Turn is the atomic unit for compression. Cutting inside a Turn can orphan tool-call sequences — an assistant message with `ToolCalls` separated from its corresponding `tool` results. Compressing at Turn boundaries avoids this by construction.
+
+`parseTurnBoundaries(history)` returns the starting index of each Turn.
+`findSafeBoundary(history, targetIndex)` snaps a target cut point to the nearest Turn boundary.
+
+---
+
+## Compression paths
+
+Three compression paths exist, in order of preference:
+
+### 1. Async summarization
+
+`maybeSummarize` runs after each Turn completes.
+
+Triggers when message count exceeds a threshold, or when estimated history tokens exceed a percentage of `ContextWindow`. If triggered, a background goroutine calls the LLM to produce a summary of the oldest messages. The summary is stored via `SetSummary`; `BuildMessages` injects it into the system prompt on the next call.
+
+Cut point uses `findSafeBoundary` so no Turn is split.
+
+### 2. Proactive budget check
+
+`isOverContextBudget` runs before each LLM call.
+
+Uses the full budget formula: `message_tokens + tool_def_tokens + MaxTokens > ContextWindow`. If over budget, triggers `forceCompression` and rebuilds messages before calling the LLM.
+
+This prevents wasted (and billed) LLM calls that would otherwise fail with a context-window error.
+
+### 3. Emergency compression (reactive)
+
+`forceCompression` runs when the LLM returns a context-window error despite the proactive check.
+
+Drops the oldest ~50% of Turns. If the history is a single Turn with no safe split point (e.g. one user message followed by a massive tool response), falls back to keeping only the most recent user message — breaking Turn atomicity as a last resort to avoid a context-exceeded loop.
+
+Stores a compression note in the session summary (not in history messages) so `BuildMessages` can include it in the next system prompt.
+
+This is the fallback for when the token estimate undershoots reality.
+
+---
+
+## Token estimation
+
+Estimation uses a heuristic of ~2.5 characters per token (`chars * 2 / 5`).
+
+`estimateMessageTokens` counts:
+
+- `Content` (rune count, for multibyte correctness)
+- `ReasoningContent` (extended thinking / chain-of-thought)
+- `ToolCalls` — ID, type, function name, arguments
+- `ToolCallID` (tool result metadata)
+- Per-message overhead (role label, JSON structure)
+- `Media` items — flat per-item token estimate, added directly to the final count (not through the character heuristic, since actual cost depends on resolution and provider-specific image tokenization)
+
+`estimateToolDefsTokens` counts tool definition overhead: name, description, JSON schema of parameters.
+
+These are deliberately heuristic. The proactive check handles the common case; the reactive path catches estimation errors.
+
+---
+
+## Interface boundaries
+
+Context budget functions (`parseTurnBoundaries`, `findSafeBoundary`, `estimateMessageTokens`, `isOverContextBudget`) are **pure functions**. They take `[]providers.Message` and integer parameters. They have no dependency on `AgentLoop` or any other runtime struct.
+
+`BuildMessages` is the sole assembler of the final message array sent to the LLM. Budget functions inform compression decisions but do not construct messages.
+
+`forceCompression` and `summarizeSession` mutate session state (history and summary). `BuildMessages` reads that state to construct context. The flow is:
+
+```
+budget check --> compression decision --> mutate session --> BuildMessages reads session --> LLM call
+```
+
+---
+
+## Known gaps
+
+These are recognized limitations in the current implementation, documented here for visibility:
+
+- **Summarization trigger does not use the full budget formula.** `maybeSummarize` compares estimated history tokens against a percentage of `ContextWindow`. It does not account for system prompt size, tool definition overhead, or `MaxTokens` reserve. The proactive check covers the critical path (preventing 400 errors), but the summarization trigger could be aligned with the same budget model for more accurate early compression.
+
+- **Token estimation is heuristic.** It does not account for provider-specific tokenization, exact system prompt size (assembled separately), or variable image token costs. The two-path design (proactive + reactive) is intended to tolerate this imprecision.
+
+- **Reactive retry does not preserve media.** When the reactive path rebuilds context after compression, it currently passes empty values for media references. This is a pre-existing issue in the main loop, not introduced by the budget system.
+
+---
+
+## What this document does not cover
+
+- How `AGENT.md` frontmatter configures context parameters — that is part of the Agent definition work
+- How the context builder assembles context in the new architecture — that is upcoming work
+- How compression events surface through the event system — that is part of the event model (#1316)
+- Subagent context isolation — that is a separate track
diff --git a/docs/channels/weixin/README.md b/docs/channels/weixin/README.md
new file mode 100644
index 00000000..22687fec
--- /dev/null
+++ b/docs/channels/weixin/README.md
@@ -0,0 +1,58 @@
+# 💬 Weixin (WeChat Personal) Channel
+
+PicoClaw supports connecting to your personal WeChat account using the official Tencent iLink API.
+
+## 🚀 Quick Onboarding
+
+The easiest way to set up the Weixin channel is using the interactive onboarding command:
+
+```bash
+picoclaw onboard weixin
+```
+
+This command will:
+1. Request a QR code from the iLink API and display it in your terminal.
+2. Wait for you to scan the QR code with your WeChat mobile app.
+3. Upon approval, automatically save the generated access token to your `~/.picoclaw/config.json`.
+
+After onboarding, you can start the gateway:
+
+```bash
+picoclaw gateway
+```
+
+---
+
+## ⚙️ Configuration
+
+You can also manually configure the filter rules in `config.json` under the `channels.weixin` section.
+
+```json
+{
+ "channels": {
+ "weixin": {
+ "enabled": true,
+ "token": "YOUR_WEIXIN_TOKEN",
+ "allow_from": [
+ "user_id_1",
+ "user_id_2"
+ ],
+ "proxy": ""
+ }
+ }
+}
+```
+
+### Configuration Fields
+
+| Field | Description |
+|---|---|
+| `enabled` | Set to `true` to enable the channel at startup. |
+| `token` | The authentication token obtained via QR login. |
+| `allow_from` | (Optional) List of WeChat User IDs permitted to interact with the bot. If empty, anyone who can send messages to the connected account can trigger the bot. |
+| `proxy` | (Optional) HTTP proxy address (e.g. `http://localhost:7890`) for environments where connection to `ilinkai.weixin.qq.com` is restricted. |
+
+## ⚠️ Important Notes
+
+- **One Account Only**: The iLink token binds to a single session. Starting a new interaction generally invalidates older tokens if another device authorizes.
+- **Message Rate Limits**: To avoid getting your account restricted by WeChat anti-spam systems, avoid loop triggers or high-frequency broadcasts.
diff --git a/docs/channels/weixin/README.zh.md b/docs/channels/weixin/README.zh.md
new file mode 100644
index 00000000..d5e6f0a4
--- /dev/null
+++ b/docs/channels/weixin/README.zh.md
@@ -0,0 +1,58 @@
+# 💬 微信个人号渠道 (Weixin)
+
+PicoClaw 支持使用腾讯官方 iLink API 连接您的个人微信账号。
+
+## 🚀 快速激活
+
+最简单的方法是使用交互式 onboarding 命令进行一键激活:
+
+```bash
+picoclaw onboard weixin
+```
+
+该命令将:
+1. 从 iLink API 获取二维码并在终端中打印。
+2. 等待您使用手机微信 App 扫码。
+3. 扫码确认后,自动将生成的 Access Token 保存至您的 `~/.picoclaw/config.json` 中。
+
+配置完成后,即可启动网关:
+
+```bash
+picoclaw gateway
+```
+
+---
+
+## ⚙️ 配置说明
+
+您也可以在 `config.json` 的 `channels.weixin` 段目下进行手动维护。
+
+```json
+{
+ "channels": {
+ "weixin": {
+ "enabled": true,
+ "token": "YOUR_WEIXIN_TOKEN",
+ "allow_from": [
+ "user_id_1",
+ "user_id_2"
+ ],
+ "proxy": ""
+ }
+ }
+}
+```
+
+### 字段解析
+
+| 字段 | 说明 |
+|---|---|
+| `enabled` | 设置为 `true` 以在启动时激活该频道。 |
+| `token` | 通过扫码获取的认证令牌。 |
+| `allow_from` | (可选) 允许与机器人交互的微信 User ID 列表。如果为空,任何能给此微信号发消息的人都可以触发机器人。 |
+| `proxy` | (可选) HTTP 代理地址(例如 `http://localhost:7890`),适合网络访问受限环境。 |
+
+## ⚠️ 注意事项
+
+- **单端绑定**: iLink 令牌通常与单个会话绑定。在其他地方重新扫码激活可能会导致旧令牌失效。
+- **频率控制**: 为避免触发微信的风控反垃圾机制,请避免设置死循环触发、高频广播等恶意行为。
diff --git a/docs/chat-apps.md b/docs/chat-apps.md
index 3ed37e81..07297952 100644
--- a/docs/chat-apps.md
+++ b/docs/chat-apps.md
@@ -13,6 +13,7 @@ Talk to your picoclaw through Telegram, Discord, WhatsApp, Matrix, QQ, DingTalk,
| **Telegram** | ⭐ Easy | Recommended, voice-to-text, long polling (no public IP needed) | [Docs](../channels/telegram/README.md) |
| **Discord** | ⭐ Easy | Socket Mode, group/DM support, rich bot ecosystem | [Docs](../channels/discord/README.md) |
| **WhatsApp** | ⭐ Easy | Native (QR scan) or Bridge URL | [Docs](#whatsapp) |
+| **Weixin** | ⭐ Easy | Native QR scan (Tencent iLink API) | [Docs](../channels/weixin/README.md) |
| **Slack** | ⭐ Easy | **Socket Mode** (no public IP needed), enterprise | [Docs](../channels/slack/README.md) |
| **Matrix** | ⭐⭐ Medium | Federated protocol, self-hosting supported | [Docs](../channels/matrix/README.md) |
| **QQ** | ⭐⭐ Medium | Official bot API, Chinese community | [Docs](../channels/qq/README.md) |
@@ -169,6 +170,39 @@ If `session_store_path` is empty, the session is stored in `